Thread Creation and Termination
Analyzed thread creation and termination WIN32 API.
Thread Creation#
CreateThread#
using Culrry.Core.Markdown;
using Microsoft.AspNetCore.Components;
namespace Web.Components.Pages;
public partial class Post
{
[Inject]
private MarkdownService Markdown { get; set; }
[Inject]
private IWebHostEnvironment WebHostEnvironment { get; set; }
private string html;
protected override Task OnInitializedAsync()
{
string filePath = Path.Combine(WebHostEnvironment.WebRootPath, "test.md");
var str = File.ReadAllText(filePath);
html = Markdown.ToHtml(str);
int a = 100;
return base.OnInitializedAsync();
}
}csharppublic static class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello");
}
}csharpHANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
)cpp- dwStatckSize
- Thread stack size
- When a process starts, it internally calls the CreateThread function to initialize the main thread of the process.
- At this time, CreateProcess uses the value stored inside the executable file to determine the dwStackSize parameter value.
- If you pass 0 as an argument, it is allocated with the process’s default size.
- lpStartAddress
- The address of the thread function that the newly created thread will call.
- lpParameter
- Parameter value passed when the thread executes
- dwCreationFlags
- If you pass 0, it immediately becomes a schedulable target. If you use the
CREATE_SUSPENDEDflag, the thread is created, initialization is completed, and then it enters the SUSPEND state.
- If you pass 0, it immediately becomes a schedulable target. If you use the
- lpThreadId
- Pointer to receive the unique thread ID
- The thread ID is used internally by the thread itself and is rarely used externally.
_beginthreadex#
_beginthreadex requires the same parameters as CreateThread, but includes code to initialize space for the C runtime library and the data types are changed to standard types.
_beginthreadex Source Code Analysis#
extern "C" uintptr_t __cdecl _beginthreadex(
void* const security_descriptor,
unsigned int const stack_size,
_beginthreadex_proc_type const procedure,
void* const context,
unsigned int const creation_flags,
unsigned int* const thread_id_result
)
{
_VALIDATE_RETURN(procedure != nullptr, EINVAL, 0);
// Combine the specified parameter with the specified Thread start point function pointer.
unique_thread_parameter parameter(create_thread_parameter(procedure, context));
if (!parameter)
{
return 0;
}
DWORD thread_id;
HANDLE const thread_handle = CreateThread(
reinterpret_cast<LPSECURITY_ATTRIBUTES>(security_descriptor),
stack_size,
thread_start<_beginthreadex_proc_type, true>, // Call CreateThread with a function called thread_start.
parameter.get(), // start point function + parameter
creation_flags,
&thread_id);
if (!thread_handle)
{
__acrt_errno_map_os_error(GetLastError());
return 0;
}
if (thread_id_result)
{
*thread_id_result = thread_id;
}
// If we successfully created the thread, the thread now owns its parameter:
parameter.detach();
return reinterpret_cast<uintptr_t>(thread_handle);
}c- What _beginthreadex does is simple. It just performs CreateThread but attaches the specified start point function in front of the parameter.
- Then it creates a thread by executing CreateThread with a function called thread_start created in the C runtime as the start point.
typedef struct __acrt_thread_parameter
{
// Thread start function and parameter
void* _procedure; // Function start point address (specified)
void* _context; // Function parameter (passed)
// Handle of the newly created thread. Initialized only when executed with _beginthread. (not in _beginthreadex)
// If there is a thread created with _beginthread, it returns through this handle.
HANDLE _thread_handle;
// Handle of the module defined in the user thread procedure
// It is null if the handle cannot be obtained. Using this handle increases the reference count of the user module
// so that the module is not unloaded while the thread is running.
// When the thread terminates, this handle is released.
HMODULE _module_handle;
// This flag is true if RoInitialized is called in the thread to initialize as MTA (Multi-threaded Apartment).
bool _initialized_apartment;
} __acrt_thread_parameter;
static __acrt_thread_parameter* __cdecl create_thread_parameter(
void* const procedure,
void* const context
) throw()
{
unique_thread_parameter parameter(_calloc_crt_t(__acrt_thread_parameter, 1).detach());
if (!parameter)
{
return nullptr;
}
parameter.get()->_procedure = procedure;
parameter.get()->_context = context;
// Increase the count of the module where the user thread procedure is defined so that the module remains loaded while the thread is running.
// This HMODULE is released when the thread procedure returns or _endthreadex is called.
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
reinterpret_cast<LPCWSTR>(procedure),
¶meter.get()->_module_handle);
return parameter.detach();
}
c- Creates a structure called __acrt_thread_parameter to combine the function and parameter value into one structure and passes it as an argument to thread_start.
template <typename ThreadProcedure, bool Ex>
static unsigned long WINAPI thread_start(void* const parameter) throw()
{
if (!parameter)
{
ExitThread(GetLastError());
}
__acrt_thread_parameter* const context = static_cast<__acrt_thread_parameter*>(parameter);
// Dynamic allocation and initialization of ptd space to be used in the new thread
__acrt_getptd()->_beginthread_context = context;
if (__acrt_get_begin_thread_init_policy() == begin_thread_init_policy_ro_initialize)
{
context->_initialized_apartment = __acrt_RoInitialize(RO_INIT_MULTITHREADED) == S_OK;
}
__try
{
ThreadProcedure const procedure = reinterpret_cast<ThreadProcedure>(context->_procedure);
if constexpr (Ex)
{
// Execute the specified start point function and call _endthreadex when the function terminates
_endthreadex(procedure(context->_context));
}
else
{
procedure(context->_context);
_endthreadex(0);
}
}
__except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
{
// Execution should never reach here:
_exit(GetExceptionCode());
}
// This return statement will never be reached. All execution paths result
// in the thread or process exiting.
return 0;
}c- When the getptd() function is called, if there is no ptd, it allocates and initializes the ptd space. After that, it registers the thread’s function and parameters in ptd.
- Then it executes the defined function.
ptd(Per-Thread Data)#
- A structure created by CRT to store values needed when runtime functions are executed for each thread.
typedef struct __acrt_ptd
{
// 3 data members to assist signal handling and runtime errors
struct __crt_signal_action_t* _pxcptacttab; // Pointer to the exception-action table
EXCEPTION_POINTERS* _tpxcptinfoptrs; // Pointer to the exception info pointers
int _tfpecode; // Last floating point exception code
terminate_handler _terminate; // terminate() routine
int _terrno; // errno value
unsigned long _tdoserrno; // _doserrno value
unsigned int _rand_state; // Previous value of rand()
// Per-thread strtok(), wcstok(), and mbstok() data:
char* _strtok_token;
unsigned char* _mbstok_token;
wchar_t* _wcstok_token;
// Per-thread tmpnam() data:
char* _tmpnam_narrow_buffer;
wchar_t* _tmpnam_wide_buffer;
// Per-thread time library data:
char* _asctime_buffer; // Pointer to asctime() buffer
wchar_t* _wasctime_buffer; // Pointer to _wasctime() buffer
struct tm* _gmtime_buffer; // Pointer to gmtime() structure
char* _cvtbuf; // Pointer to the buffer used by ecvt() and fcvt().
// Per-thread error message data:
char* _strerror_buffer; // Pointer to strerror() / _strerror() buffer
wchar_t* _wcserror_buffer; // Pointer to _wcserror() / __wcserror() buffer
// Locale data:
__crt_multibyte_data* _multibyte_info;
__crt_locale_data* _locale_info;
__crt_qualified_locale_data _setloc_data;
__crt_qualified_locale_data_downlevel* _setloc_downlevel_data;
int _own_locale; // See _configthreadlocale() and __acrt_should_sync_with_global_locale()
// The buffer used by _putch(), and the flag indicating whether the buffer
// is currently in use or not.
unsigned char _putch_buffer[MB_LEN_MAX];
unsigned short _putch_buffer_used;
// The thread-local invalid parameter handler
_invalid_parameter_handler _thread_local_iph;
// If this thread is started by _beginthread or _beginthreadex, this points to the context in which the thread was created.
// It is null if this thread was not executed by CRT.
__acrt_thread_parameter* _beginthread_context;
} __acrt_ptd;plaintextThread Termination#
TerminateThread#
- TerminateThread is a dangerous function that should only be used in the most extreme cases. It should only be called when you know exactly what work the target thread performs and you control all the code that the target thread can execute upon termination.
- The following problems can occur:
- If the target thread owns a critical section, the critical section is not released. (referring to CriticalSection)
- If the target thread allocates memory from the heap, the heap lock is not released.
- If the target thread executes certain kernel32 calls upon termination, the kernel32 state for the thread process may be inconsistent.
- If the target thread manipulates the global state of a shared DLL, the DLL’s state is deleted and may affect other users of the DLL.
- Terminating a thread does not necessarily remove the thread object from the system. The thread object is deleted when the last thread handle is closed.
ExitThread#
- This is the basic method to terminate a thread in C code. However, in C++ code, the thread terminates before calling destructors or performing other automatic cleanup. Therefore, in C++, the thread function must return.
- When you explicitly call this function or return from the thread procedure, the current thread stack is freed and the thread terminates. The entry point functions of all associated DLLs are called with a value indicating that the thread is being detached from the DLL.
- Threads linked to CRT must call _endthread. If this is not done, a memory leak occurs when the thread calls ExitThread. (ptd is not cleaned up.)
_endthreadex#
- Releases ptd and CRT resources and calls ExitThread.
- Therefore, CRT threads must call endthreadex
_endthreadex Code Analysis#
extern "C" void __cdecl _endthreadex(unsigned int const return_code)
{
return common_end_thread(return_code);
}
static void __cdecl common_end_thread(unsigned int const return_code) throw()
{
__acrt_ptd* const ptd = __acrt_getptd_noexit();
//If there is no ptd allocation information, call ExitThread immediately
if (!ptd)
{
ExitThread(return_code);
}
__acrt_thread_parameter* const parameter = ptd->_beginthread_context;
//If there is no _beginthread_context, call ExitThread
if (!parameter)
{
ExitThread(return_code);
}
// Up to here is the situation that occurs if the thread was not created with _beginthreadex
----------------------------------------------------
// If RoInitialize was called, call RoUninitialize
if (parameter->_initialized_apartment)
{
__acrt_RoUninitialize();
}
// Return thread handle (skip if created with _beginthreadex)
if (parameter->_thread_handle != INVALID_HANDLE_VALUE && parameter->_thread_handle != nullptr)
{
CloseHandle(parameter->_thread_handle);
}
// Decrease DLL reference count by 1 then ExitThread
if (parameter->_module_handle != INVALID_HANDLE_VALUE && parameter->_module_handle != nullptr)
{
FreeLibraryAndExitThread(parameter->_module_handle, return_code);
}
else
{
ExitThread(return_code);
}
}
cThread Termination Cases#
- Thread calls ExitThread
- If other threads do not have a handle to the kernel object of the thread calling the function, the thread kernel object is removed.
- If they have it, it remains until all handles are returned.
- ptd is not cleaned up.
- Thread calls _endthreadex
- ptd is cleaned up.
- Thread returns
- Destructors for local variables are also called.