CULRRY.NET

Back

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();
    }
}
csharp
public static class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello");
    }
}
csharp
HANDLE 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_SUSPENDED flag, the thread is created, initialization is completed, and then it enters the SUSPEND state.
  • 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),
        &parameter.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;
plaintext

Thread 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);
    }
}
c

Thread Termination Cases#

  1. 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.
  2. Thread calls _endthreadex
    • ptd is cleaned up.
  3. Thread returns
    • Destructors for local variables are also called.
Thread Creation and Termination
https://culrry.net/en/blog/test/
Author CULRRY
Published at January 29, 2024