{"id":110604,"date":"2024-12-04T07:00:00","date_gmt":"2024-12-04T15:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=110604"},"modified":"2024-12-04T07:04:53","modified_gmt":"2024-12-04T15:04:53","slug":"20241204-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20241204-00\/?p=110604\/","title":{"rendered":"Why does my DLL reference count go up by one every time I create and exit a thread?"},"content":{"rendered":"<p>A customer reported that each time they created and exited a thread, their DLL reference count goes up by one. This was a problem because their code loads a DLL and then calls a function in it. That function creates a thread, waits for the thread to finish, and then returns. After the function returns, their main program tries to unload the DLL, but the DLL remains in memory. Their debugging (with the <tt>!dlls<\/tt> debugger extension) showed that when they created a thread, the DLL reference count went up by one, but it did not drop back down when the thread exited.<\/p>\n<p>The customer asked why <code>Create\u00adThread<\/code> increments the DLL reference count, but <code>Exit\u00adThread<\/code> doesn&#8217;t decrement it.<\/p>\n<p>This question struck the DLL loader team as odd, because <code>Create\u00adThread<\/code> doesn&#8217;t increment the DLL reference count, and a quick sanity test confirmed their recollection: Creating a thread with <code>Create\u00adThread<\/code> does not increment the DLL reference count. Consequently, <code>Exit\u00adThread<\/code> is correct in not decrementing it.<\/p>\n<p>We went back to the customer to get some more information.<\/p>\n<p>The customer said that they create the thread with <code>_beginthreadex<\/code>. Switching to <code>std::thread<\/code> didn&#8217;t help.<\/p>\n<p>Okay, that explains it.\u00b9 They are creating the thread with <code>_beginthreadex<\/code> and ending it with <code>ExitThread<\/code>.<\/p>\n<p><a href=\"https:\/\/learn.microsoft.com\/cpp\/c-runtime-library\/reference\/beginthread-beginthreadex?view=msvc-170\"> The documentation for <code>_beginthread<\/code> and <code>_beginthreadex<\/code><\/a> has a big banner that says<\/p>\n<blockquote class=\"q\"><p>For an executable file linked with Libcmt.lib, <span style=\"border: solid 1px currentcolor;\">do not call the Win32 <code>ExitThread<\/code> API<\/span> so that you don&#8217;t prevent the run-time system from reclaiming allocated resources. <code>_endthread<\/code> and <code>_endthreadex<\/code> reclaim allocated thread resources and then call <code>ExitThread<\/code>.<\/p><\/blockquote>\n<p>It is not <code>Create\u00adThread<\/code> that increments the DLL reference count. It&#8217;s <code>_beginthreadex<\/code>. You can see it in the code, which is provided in the Windows Platform SDK under <tt>C:\\<wbr \/>Program Files (x86)\\<wbr \/>Windows Kits\\<wbr \/>10\\<wbr \/>Source\\<\/tt><wbr \/>\u2329version\u232a<tt>\\ucrt\\<wbr \/>startup\\<wbr \/>thread.cpp<\/tt>:<\/p>\n<pre>extern \"C\" uintptr_t __cdecl _beginthreadex(\r\n    void*                    const security_descriptor,\r\n    unsigned int             const stack_size,\r\n    _beginthreadex_proc_type const procedure,\r\n    void*                    const context,\r\n    unsigned int             const creation_flags,\r\n    unsigned int*            const thread_id_result\r\n    )\r\n{\r\n    _VALIDATE_RETURN(procedure != nullptr, EINVAL, 0);\r\n\r\n    unique_thread_parameter parameter(\r\n        create_thread_parameter(procedure, context));\r\n\r\n    \u27e6 ... more stuff ... \u27e7\r\n<\/pre>\n<p>where<\/p>\n<pre>static __acrt_thread_parameter* __cdecl create_thread_parameter(\r\n    void* const procedure,\r\n    void* const context\r\n    ) throw()\r\n{\r\n    unique_thread_parameter parameter(\r\n        _calloc_crt_t(__acrt_thread_parameter, 1).detach());\r\n    if (!parameter)\r\n    {\r\n        return nullptr;\r\n    }\r\n\r\n    parameter.get()-&gt;_procedure = procedure;\r\n    parameter.get()-&gt;_context   = context;\r\n\r\n    \/\/ Attempt to bump the reference count of the module in which the user's\r\n    \/\/ thread procedure is defined, to ensure that the module will stay loaded\r\n    \/\/ as long as the thread is executing.  We will release this HMDOULE [sic]\r\n    \/\/ <span style=\"border: solid 1px currentcolor;\">when the thread procedure returns or _endthreadex is called.<\/span>\r\n    GetModuleHandleExW(\r\n        GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,\r\n        reinterpret_cast&lt;LPCWSTR&gt;(procedure),\r\n        &amp;parameter.get()-&gt;_module_handle);\r\n\r\n    return parameter.detach();\r\n}\r\n<\/pre>\n<p>So it&#8217;s not <code>Create\u00adThread<\/code> that is incrementing the DLL reference count. It&#8217;s <code>_beginthreadex<\/code>. And the DLL reference count decrements when the thread procedure returns or when you call <code>_endthreadex<\/code>:<\/p>\n<pre>static void __cdecl common_end_thread(unsigned int const return_code) throw()\r\n{\r\n    \u27e6 ... other stuff not relevant here ... \u27e7\r\n\r\n    if (parameter-&gt;_module_handle != INVALID_HANDLE_VALUE &amp;&amp;\r\n        parameter-&gt;_module_handle != nullptr)\r\n    {\r\n        <span style=\"border: solid 1px currentcolor;\">FreeLibraryAndExitThread(parameter-&gt;_module_handle, return_code);<\/span>\r\n    }\r\n    else\r\n    {\r\n        ExitThread(return_code);\r\n    }\r\n}\r\n\r\nextern \"C\" void __cdecl _endthread()\r\n{\r\n    return common_end_thread(0);\r\n}\r\n\r\nextern \"C\" void __cdecl _endthreadex(unsigned int const return_code)\r\n{\r\n    return common_end_thread(return_code);\r\n}\r\n<\/pre>\n<p>If you call <code>ExitThread<\/code> directly, then you bypass the cleanup code that <code>_endthreadex<\/code> performs, which means that you leak a bunch of stuff, including the DLL reference.<\/p>\n<p>The customer noted that they also tried <code>std::<wbr \/>thread<\/code>, and my guess is that they used <code>ExitThread<\/code> to exit a <code>std::thread<\/code>:<\/p>\n<pre>\/\/ Don't do this!\r\nauto thread = std::thread([] {\r\n    \u27e6 ... do some stuff ... \u27e7\r\n    ExitThread(42); \/\/ all done\r\n});\r\n<\/pre>\n<p>When you exit the thread with <code>Exit\u00adThread()<\/code>, the thread ends without allowing the active function calls to clean up. If we look at <a href=\"https:\/\/github.com\/microsoft\/STL\/blob\/8657d15b3ddfacb121e88e307f9a18b3fa379185\/stl\/inc\/thread#L56\"> <code>std::<wbr \/>thread<\/code>&#8216;s thread procedure<\/a>,<\/p>\n<pre>template &lt;class _Tuple, size_t... _Indices&gt;\r\nstatic unsigned int __stdcall _Invoke(void* _RawVals)\r\n    noexcept \/* terminates *\/ {\r\n    \/\/ adapt invoke of user's callable object to _beginthreadex's\r\n    \/\/ thread procedure\r\n    const unique_ptr&lt;_Tuple&gt; _FnVals(static_cast&lt;_Tuple*&gt;(_RawVals));\r\n    _Tuple&amp; _Tup = *_FnVals.get(); \/\/ avoid ADL, handle incomplete types\r\n    _STD invoke(_STD move(_STD get&lt;_Indices&gt;(_Tup))...);\r\n    _Cnd_do_broadcast_at_thread_exit(); \/\/ TRANSITION, ABI\r\n    return 0;\r\n}\r\n<\/pre>\n<p>we see that this bypasses the destructor of <code>_FnVals<\/code>, which means that we leak the <code>_Tuple<\/code> that holds the thread callable and parameters. We also bypass the call to <code>_Cnd_<wbr \/>do_<wbr \/>broadcast_<wbr \/>at_<wbr \/>thread_<wbr \/>exit()<\/code>, which lets other threads know that this thread has exited. This is used by functions like <a href=\"https:\/\/en.cppreference.com\/w\/cpp\/thread\/notify_all_at_thread_exit\"> <code>std::<wbr \/>notify_<wbr \/>all_<wbr \/>at_<wbr \/>thread_<wbr \/>exit<\/code><\/a> and <a href=\"https:\/\/en.cppreference.com\/w\/cpp\/thread\/promise\/set_value_at_thread_exit\"> <code>std::<wbr \/>future::<wbr \/>set_<wbr \/>value_<wbr \/>at_<wbr \/>thread_<wbr \/>exit<\/code><\/a>. Bypassing those calls means that things which are waiting for the thread to exit won&#8217;t ever run.<\/p>\n<p>Moral of the story: If you use <code>_beginthread<\/code> or <code>_beginthreadex<\/code>, you must exit the thread either by returning from your thread function or by calling <code>_endthread<\/code> or <code>_endthreadex<\/code>. Don&#8217;t go straight to <code>Exit\u00adThread()<\/code>.<\/p>\n<p>And if you use <code>std::<wbr \/>thread<\/code>, then you must exit by returning from your thread callable. Don&#8217;t call <code>_endthread<\/code> or <code>_endthreadex<\/code> or <code>Exit\u00adThread()<\/code>.<\/p>\n<p>\u00b9 It also points out that the customer&#8217;s question is misleading. They asked why <code>Create\u00adThread<\/code> increments the DLL reference count, but they aren&#8217;t calling <code>Create\u00adThread<\/code> themselves, so how do they know that it&#8217;s <code>Create\u00adThread<\/code> that&#8217;s doing it?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you use a wrapper, you need to follow the wrapper&#8217;s rules.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-110604","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>If you use a wrapper, you need to follow the wrapper&#8217;s rules.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=110604"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110604\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=110604"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=110604"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=110604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}