Thread Basics
August 10, 2011 Leave a comment
A thread consists of two components:
- A kernel object that the operating system uses to manage the thread. The kernel object is also where the system keeps statistical information about the thread.
- A thread stack that maintains all the function parameters and local variables required as the thread executes code.
Threads are always created in the context of some process and live their entire life within that process. What this really means is that the thread executes code and manipulates data within its process’ address space. So if you have two or more threads running in the context of a single process, the threads share a single address space. The threads can execute the same code and manipulate the same data. Threads can also share kernel object handles because the handle table exists for each process, not each thread.
Your First Thread Function
Every thread must have an entry-point function where it begins execution. We already discussed this entry-point function for your primary thread: _tmain or _tWinMain. If you want to create a secondary thread in your process, it must also have an entry-point function, which should look something like this:
DWORD WINAPI ThreadFunc(PVOID pvParam){
DWORD dwResult = 0;
...
return(dwResult);
}
The CreateThread Function
If you want to create one or more secondary threads, you simply have an already running thread call CreateThread.
HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, DWORD cbStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD dwCreateFlags, PDWORD pdwThreadID);
PDWORD pdwThreadID);
The CreateThread function is the Windows function that creates a thread. However, if you are writing C/C++ code, you should never call CreateThread. Instead, you should use the Microsoft C++ run-time library function _beginthreadex
Thread Stack Size
The cbStackSize parameter specifies how much address space the thread can use for its own stack. Every thread owns its own stack. When CreateProcess starts a process, it internally calls CreateThread to initialize the process’ primary thread. For the cbStackSize parameter, CreateProcess uses a value stored inside the executable file. You can control this value using the linker’s /STACK switch:
/STACK:[reserve][,commit]
The reserve argument sets the amount of address space the system should reserve for the thread’s stack. The default is 1 MB. The commit argument specifies the amount of physical storage that should be initially committed to the stack’s reserved region.
When you call CreateThread, passing a value other than 0 causes the function to reserve and commit all storage for the thread’s stack. The amount of reserved space is either the amount specified by the /STACK linker switch or the value of cbStack, whichever is larger. If you pass 0 to the cbStack parameter, CreateThread reserves a region and commits the amount of storage indicated by the /STACK linker switch information embedded in the .exe file by the linker.
Thread Termination
A thread can be terminated in four ways:
- The thread function returns. (This is highly recommended.)
- The thread kills itself by calling the ExitThread function. (Avoid this method.)
- A thread in the same process or in another one calls the TerminateThread function. (Avoid this method.)
- The process containing the thread terminates. (Avoid this method.)
The Thread Function Returns
You should always design your thread functions so that they return when you want the thread to terminate. This is the only way to guarantee that all your thread’s resources are cleaned up properly.
Having your thread function return ensures the following:
- All C++ objects created in your thread function will be destroyed properly via their destructors.
- The operating system will properly free the memory used by the thread’s stack.
- The system will set the thread’s exit code (maintained in the thread’s kernel object) to your thread function’s return value.
- The system will decrement the usage count of the thread’s kernel object.
When a thread dies by returning or calling ExitThread, the stack for the thread is destroyed. However, if TerminateThread is used, the system does not destroy the thread’s stack until the process that owned the thread terminates.
if several threads run concurrently in your application, you need to explicitly handle how each one stops before the main thread returns. Otherwise, all other running threads will die abruptly and silently.
When a Thread Terminates
The following actions occur when a thread terminates:
- All User object handles owned by the thread are freed. In Windows, most objects are owned by the process containing the thread that creates the objects. However, a thread owns two User objects: windows and hooks. When a thread dies, the system automatically destroys any windows and uninstalls any hooks that were created or installed by the thread. Other objects are destroyed only when the owning process terminates.
- The thread’s exit code changes from STILL_ACTIVE to the code passed to ExitThread or TerminateThread.
- The state of the thread kernel object becomes signaled.
- If the thread is the last active thread in the process, the system considers the process terminated as well.
- The thread kernel object’s usage count is decremented by 1
Working with C/C++ Run Time Libraries
To create a new thread, you must not call the operating system’s CreateThread function—you must call the C/C++ run-time library function _beginthreadex:
unsigned long _beginthreadex( void *security, unsigned stack_size, unsigned (*start_address)(void *), void *arglist, unsigned initflag, unsigned *thrdaddr);
The _beginthreadex function has the same parameter list as the CreateThread function, but the parameter names and types are not exactly the same.
If you really want to forcibly kill your thread, you can have it call _endthreadex (instead of ExitThread)
The C/C++ run-time library also places synchronization primitives around certain functions. For example, if two threads simultaneously call malloc, the heap can become corrupted. The C/C++ run-time library prevents two threads from allocating memory from the heap at the same time. It does this by making the second thread wait until the first has returned from malloc. Then the second thread is allowed to enter.Obviously, all this additional work affects the performance of the multithreaded version of the C/C++ run-time library.
Gaining a Sense of One’s Own Identity
Windows offers functions that make it easy for a thread to refer to its process kernel object or to its own thread kernel object:
HANDLE GetCurrentProcess(); HANDLE GetCurrentThread();
The following functions allow a thread to query its process’ unique ID or its own unique ID:
DWORD GetCurrentProcessId(); DWORD GetCurrentThreadId();
Converting a Pseudohandle to a Real Handle
Usually, you use DuplicateHandle function to create a new process-relative handle from a kernel object handle that is relative to another process. However, we can use it in an unusual way convert a Pseudohandle to a Real Handle:
DWORD WINAPI ParentThread(PVOID pvParam) {
HANDLE hThreadParent;
DuplicateHandle(
GetCurrentProcess(), // Handle of process that thread
// pseudohandle is relative to
GetCurrentThread(), // Parent thread's pseudohandle
GetCurrentProcess(), // Handle of process that the new, real,
// thread handle is relative to
&hThreadParent, // Will receive the new, real, handle
// identifying the parent thread
0, // Ignored due to DUPLICATE_SAME_ACCESS
FALSE, // New thread handle is not inheritable
DUPLICATE_SAME_ACCESS); // New thread handle has same
// access as pseudohandle
CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, NULL);
// Function continues...
}
DWORD WINAPI ChildThread(PVOID pvParam) {
HANDLE hThreadParent = (HANDLE) pvParam;
FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
GetThreadTimes(hThreadParent,
&ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);
CloseHandle(hThreadParent);
// Function continues...
}
Now when the parent thread executes, it converts the ambiguous pseudohandle identifying the parent thread to a new, real handle that unambiguously identifies the parent thread, and it passes this real handle to CreateThread. When the child thread starts executing, its pvParam parameter contains the real thread handle. Any calls to functions passing this handle will affect the parent thread, not the child thread.
Because DuplicateHandle increments the usage count of the specified kernel object, it is important to decrement the object’s usage count by passing the target handle to CloseHandle when you finish using the duplicated object handle.
API Table
|
Function |
Description |
HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, DWORD cbStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD dwCreateFlags, PDWORD pdwThreadID); |
If you want to create one or more secondary threads, you simply have an already running thread call CreateThread: |
VOID ExitThread(DWORD dwExitCode); |
You can force your thread to terminate |
BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode); |
Unlike ExitThread, which always kills the calling thread, TerminateThread can kill any thread |
BOOL GetExitCodeThread( HANDLE hThread, PDWORD pdwExitCode); |
Check whether the thread identified by hThread has terminated and, if it has, determine its exit code. The exit code value is returned in the DWORD pointed to by pdwExitCode. If the thread hasn’t terminated when GetExitCodeThread is called, the function fills the DWORD with the STILL_ACTIVE identifier (defined as 0×103). If the function is successful, TRUE is returned |
