The Windows Processes and Threads 7






Thread Stack Size


Each new thread or fiber receives its own stack space consisting of both reserved and initially committed memory. The reserved memory size represents the total stack allocation in virtual memory. As such, the reserved size is limited to the virtual address range. The initially committed pages do not utilize physical memory until they are referenced; however, they do remove pages from the system total commit limit, which is the size of the page file plus the size of the physical memory. The system commits additional pages from the reserved stack memory as they are needed, until either the stack reaches the reserved size minus one page (which is used as a guard page to prevent stack overflow) or the system is so low on memory that the operation fails. It is best to choose as small a stack size as possible and commit the stack that is needed for the thread or fiber to run reliably. Every page that is reserved for the stack cannot be used for any other purpose. A stack is freed when its thread exits. It is not freed if the thread is terminated by another thread.

The default size for the reserved and initially committed stack memory is specified in the executable file header. Thread or fiber creation fails if there is not enough memory to reserve or commit the number of bytes requested. The default stack reservation size used by the linker is 1 MB. To specify a different default stack reservation size for all threads and fibers, use the STACKSIZE statement in the module definition (.def) file. The operating system rounds up the specified size to the nearest multiple of the system's allocation granularity (typically 64 KB). To retrieve the allocation granularity of the current system, use the GetSystemInfo() function.

To change the initially committed stack space, use the dwStackSize parameter of the CreateThread(), CreateRemoteThread(), or CreateFiber() function. This value is rounded up to the nearest page. Generally, the reserve size is the default reserve size specified in the executable header. However, if the initially committed size specified by dwStackSize is larger than or equal to the default reserve size, the reserve size is this new commit size rounded up to the nearest multiple of 1 MB. To change the reserved stack size, set the dwCreationFlags parameter of CreateThread() or CreateRemoteThread() to STACK_SIZE_PARAM_IS_A_RESERVATION and use the dwStackSize parameter. In this case, the initially committed size is the default size specified in the executable header. For fibers, use the dwStackReserveSize parameter of CreateFiberEx(). The committed size is specified in the dwStackCommitSize parameter. The SetThreadStackGuarantee() function sets the minimum size of the stack associated with the calling thread or fiber that will be available during any stack overflow exceptions.


Thread Handles and Identifiers


When a new thread is created by the CreateThread() or CreateRemoteThread() function, a handle to the thread is returned. By default, this handle has full access rights, and, subject to security access checking, can be used in any of the functions that accept a thread handle. This handle can be inherited by child processes, depending on the inheritance flag specified when it is created. The handle can be duplicated by DuplicateHandle(), which enables you to create a thread handle with a subset of the access rights. The handle is valid until closed, even after the thread it represents has been terminated.

The CreateThread() and CreateRemoteThread() functions also return an identifier that uniquely identifies the thread throughout the system. A thread can use the GetCurrentThreadId() function to get its own thread identifier. The identifiers are valid from the time the thread is created until the thread has been terminated. Note that no thread identifier will ever be 0. If you have a thread identifier, you can get the thread handle by calling the OpenThread() function. OpenThread() enables you to specify the handle's access rights and whether it can be inherited. A thread can use the GetCurrentThread() function to retrieve a pseudo handle to its own thread object. This pseudo handle is valid only for the calling process; it cannot be inherited or duplicated for use by other processes. To get the real handle to the thread, given a pseudo handle, use the DuplicateHandle() function. To enumerate the threads of a process, use the Thread32First() and Thread32Next() functions.


Suspending Thread Execution


A thread can suspend and resume the execution of another thread. While a thread is suspended, it is not scheduled for time on the processor. If a thread is created in a suspended state (with the CREATE_SUSPENDED flag), it does not begin to execute until another thread calls the ResumeThread() function with a handle to the suspended thread. This can be useful for initializing the thread's state before it begins to execute. Suspending a thread at creation can be useful for one-time synchronization, because this ensures that the suspended thread will execute the starting point of its code when you call ResumeThread().

The SuspendThread() function is not intended to be used for thread synchronization because it does not control the point in the code at which the thread's execution is suspended. This function is primarily designed for use by debuggers. A thread can temporarily yield its execution for a specified interval by calling the Sleep() or SleepEx() functions This is useful particularly in cases where the thread responds to user interaction, because it can delay execution long enough to allow users to observe the results of their actions. During the sleep interval, the thread is not scheduled for time on the processor. The SwitchToThread() function is similar to Sleep() and SleepEx(), except that you cannot specify the interval. SwitchToThread allows the thread to give up its time slice.


Synchronizing Execution of Multiple Threads


To avoid race conditions and deadlocks, it is necessary to synchronize access by multiple threads to shared resources. Synchronization is also necessary to ensure that interdependent code is executed in the proper sequence. There are a number of objects whose handles can be used to synchronize multiple threads. These objects include:


  1. Console input buffers
  2. Events
  3. Mutexes
  4. Processes
  5. Semaphores
  6. Threads
  7. Timers


The state of each of these objects is either signaled or not signaled. When you specify a handle to any of these objects in a call to one of the wait functions, the execution of the calling thread is blocked until the state of the specified object becomes signaled. Some of these objects are useful in blocking a thread until some event occurs. For example, a console input buffer handle is signaled when there is unread input, such as a keystroke or mouse button click. Process and thread handles are signaled when the process or thread terminates. This allows a process, for example, to create a child process and then block its own execution until the new process has terminated.

Other objects are useful in protecting shared resources from simultaneous access. For example, multiple threads can each have a handle to a mutex object. Before accessing a shared resource, the threads must call one of the wait functions to wait for the state of the mutex to be signaled. When the mutex becomes signaled, only one waiting thread is released to access the resource. The state of the mutex is immediately reset to not signaled so any other waiting threads remain blocked. When the thread is finished with the resource, it must set the state of the mutex to signaled to allow other threads to access the resource. For the threads of a single process, critical-section objects provide a more efficient means of synchronization than mutexes. A critical section is used like a mutex to enable one thread at a time to use the protected resource. A thread can use the EnterCriticalSection() function to request ownership of a critical section. If it is already owned by another thread, the requesting thread is blocked. A thread can use the TryEnterCriticalSection() function to request ownership of a critical section, without blocking upon failure to obtain the critical section. After it receives ownership, the thread is free to use the protected resource. The execution of the other threads of the process is not affected unless they attempt to enter the same critical section. The WaitForInputIdle() function makes a thread wait until a specified process is initialized and waiting for user input with no input pending. Calling WaitForInputIdle() can be useful for synchronizing parent and child processes, because CreateProcess() returns without waiting for the child process to complete its initialization.


Multiple Threads and GDI Objects


To enhance performance, access to graphics device interface (GDI) objects (such as palettes, device contexts, regions, and the like) is not serialized. This creates a potential danger for processes that have multiple threads sharing these objects. For example, if one thread deletes a GDI object while another thread is using it, the results are unpredictable. This danger can be avoided simply by not sharing GDI objects. If sharing is unavoidable (or desirable), the application must provide its own mechanisms for synchronizing access.




< Processes & Threads 6 | Win32 Process & Thread Programming | Win32 Programming | Processes & Threads 8 >