スレッドの生成と終了(_beginthreadex, _endthreadex ソースコード分析)
スレッドの生成と終了 WIN32 API を分析しました。
スレッド生成#
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
)c- dwStatckSize
- スレッドスタックのサイズ
- プロセスが開始されると内部的にCreateThread関数を呼び出してプロセスの主スレッドを初期化する。
- この時、CreateProcessは実行ファイル内部に保存されている値を利用してdwStackSizeのパラメータ値を決定する。
- 0を引数として渡すとプロセスのデフォルトサイズで割り当てられる。
- lpStartAddress
- 新しく生成されるスレッドが呼び出すスレッド関数のアドレス。
- lpParameter
- スレッドが実行される時に渡すパラメータ値
- dwCreationFlags
- 0を渡すとすぐにスケジュール可能な対象になり、
CREATE_SUSPENDEDフラグを使用すると、スレッドを生成し初期化を完了した後、SUSPEND状態になる。
- 0を渡すとすぐにスケジュール可能な対象になり、
- lpThreadId
- スレッド固有のIDを受け取るポインタ
- スレッドIDは自分自身が内部で使用する用途であり、外部で使用することは多くない。
_beginthreadex#
_beginthreadexはCreateThreadと同じパラメータを要求するが、Cランタイムライブラリのための領域を初期化するコードが入っており、データ型が標準型に変更されている。
_beginthreadex ソースコード分析#
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);
// 指定したparameterに指定したThread開始地点関数ポインタを結合する。
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>, // thread_startという関数でCreateThreadを呼び出す。
parameter.get(), // 開始点関数 + パラメータ
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- _beginthreadexがすることは簡単だ。単にCreateThreadを行うが、指定した開始点関数をparameterの前に付ける。
- そしてCランタイムで作成したthread_startという関数を開始点としてCreateThreadを実行してスレッドを作成する。
typedef struct __acrt_thread_parameter
{
// スレッド開始関数とパラメータ
void* _procedure; // 関数開始点アドレス (指定した)
void* _context; // 関数パラメータ (渡した)
// 新しく生成されたスレッドのハンドル。_beginthreadで実行した時のみ初期化される。(_beginthreadex)では行わない
// _beginthreadで生成されたスレッドがあれば、このハンドルを通じて返す。
HANDLE _thread_handle;
// ユーザースレッドプロシージャで定義されたモジュールのハンドル
// ハンドルを取得できない場合はnullである。このハンドルを使用するとユーザーモジュールの参照回数を増加させ
// スレッドが実行されている間にモジュールがアンロードされないようにすることができる。
// スレッドが終了するとこのハンドルが解放される。
HMODULE _module_handle;
// MTA(マルチスレッドアパートメント)として初期化するためにスレッドでRoInitializedが呼び出された場合、このフラグはtrueである。
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;
// ユーザースレッドプロシージャが定義されたモジュールのカウントを増加させ、スレッドが実行されている間にモジュールが継続的にロードされるようにする。
// スレッドプロシージャが返されるか_endthreadexが呼び出されるとこのHMODULEを解放する。
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
reinterpret_cast<LPCWSTR>(procedure),
¶meter.get()->_module_handle);
return parameter.detach();
}
c- __acrt_thread_parameterという構造体を作成して関数とパラメータ値を一つの構造体にまとめて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);
// 新しいスレッドで使用するptd領域の動的割り当ておよび初期化
__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)
{
// 指定した開始点関数の実行および関数が終了すると_endthreadexを呼び出す
_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- getptd()関数を呼び出した時、ptdがなければ、ptd領域を割り当てて初期化する過程を経る。その後、スレッドの関数とパラメータをptdに登録する。
- その後、定義した関数を実行する。
ptd(Per-Thread Data)#
- CRTでスレッドごとにランタイム関数が実行される時に必要な値を保存しておくために作成された構造体である。
typedef struct __acrt_ptd
{
// シグナルハンドリングとランタイムエラーを支援するデータメンバー3種
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;
// このスレッドが_beginthreadまたは_beginthreadexによって開始された場合、これはスレッドが生成されたコンテキストを指す。
// このスレッドがCRTによって実行されなかった場合はnullである。
__acrt_thread_parameter* _beginthread_context;
} __acrt_ptd;plaintextスレッド終了#
TerminateThread#
- TerminateThreadは最も極端な場合にのみ使用すべき危険な関数である。対象スレッドが実行する作業を正確に知っており、終了時に対象スレッドが実行できるすべてのコードを制御している場合にのみ呼び出すべきである。
- 次のような問題が発生する可能性がある。
- 対象スレッドがクリティカルセクションを所有している場合、クリティカルセクションは解放されない。(CriticalSectionのこと)
- 対象スレッドがヒープからメモリを割り当てている場合、ヒープロックが解放されない。
- 対象スレッドが終了時に特定のkernel32呼び出しを実行している場合、スレッドプロセスに対するkernel32状態が一致しない可能性がある。
- 対象スレッドが共有DLLのグローバル状態を操作している場合、DLLの状態が削除され、DLLの他のユーザーに影響を与える可能性がある。
- スレッドを終了したからといって、システムからスレッドオブジェクトが必ず削除されるわけではない。最後のスレッドハンドルを閉じるとスレッドオブジェクトが削除される。
ExitThread#
- Cコードでスレッドを終了する基本的な方法である。ただしC++コードではデストラクタを呼び出したり他の自動クリーンアップを実行する前にスレッドが終了する。したがってC++ではスレッド関数をreturnしなければならない。
- この関数を明示的に呼び出すか、スレッドプロシージャからreturnすると現在のスレッドスタックが解放され、スレッドが終了する。関連するすべてのDLLのエントリポイント関数はスレッドがDLLから切り離されていることを示す値で呼び出される。
- CRTに接続されたスレッドは_endthreadを呼び出さなければならない。これをしないとスレッドがExitThreadを呼び出す時にメモリリークが発生する。(ptdがクリーンアップされない。)
_endthreadex#
- ptdとCRTリソースを解放してExitThreadを呼び出す。
- したがってCRTスレッドはendthreadexを呼び出さなければならない
_endthreadex コード分析#
thread.c
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();
//ptd割り当て情報がなければすぐにExitThreadを呼び出す
if (!ptd)
{
ExitThread(return_code);
}
__acrt_thread_parameter* const parameter = ptd->_beginthread_context;
//_beginthread_contextがなければExitThreadを呼び出す
if (!parameter)
{
ExitThread(return_code);
}
// ここまで_beginthreadexでスレッドを生成しなかった場合に発生する状況
----------------------------------------------------
// RoInitializeが呼び出された場合、RoUninitializeを呼び出す
if (parameter->_initialized_apartment)
{
__acrt_RoUninitialize();
}
// threadハンドルを返す (_beginthreadexで生成した場合はスキップ)
if (parameter->_thread_handle != INVALID_HANDLE_VALUE && parameter->_thread_handle != nullptr)
{
CloseHandle(parameter->_thread_handle);
}
// DLL参照カウント1減少後ExitThread
if (parameter->_module_handle != INVALID_HANDLE_VALUE && parameter->_module_handle != nullptr)
{
FreeLibraryAndExitThread(parameter->_module_handle, return_code);
}
else
{
ExitThread(return_code);
}
}
cスレッド終了のケース#
- スレッドがExitThreadを呼び出す
- 他のスレッドが関数を呼び出すスレッドのカーネルオブジェクトに対するハンドルを持っていない場合、スレッドカーネルオブジェクトは削除される。
- 持っている場合はすべてのハンドルを返すまで残っている。
- ptdがクリーンアップされない。
- スレッドが_endthreadexを呼び出す
- ptdがクリーンアップされる。
- スレッドがreturnする
- ローカル変数に対するデストラクタも呼び出される。