The behavior comes from how MSVC implements thread_local and thread-safe initialization of local/static objects, and from OS-level limits and bugs around thread-local storage (TLS) and heap usage. The key points from the provided documentation:
-
thread_local objects and TLS implementation
- On Windows,
thread_local is functionally equivalent to __declspec(thread) for native C++ code.
-
thread_local can only be applied to objects with static storage duration (global, static local, static data members). Any block-scope thread_local is implicitly static.
- Each thread gets its own instance, created when the thread starts and destroyed when it exits.
Reference behavior:
"A variable declared with the thread_local specifier is accessible only on the thread on which it's created. The variable is created when the thread is created, and it's destroyed when the thread is destroyed. Each thread has its own copy of the variable. On Windows, thread_local is functionally equivalent to the Microsoft-specific __declspec( thread ) attribute."
This means the runtime and OS must set up TLS slots and run constructors early in thread startup, which can interact with heap allocation and OS TLS limits.
- Thread-safe initialization and TLS usage
MSVC’s implementation of thread-safe initialization for static locals uses TLS internally:
"Thread-safe static local variables use thread-local storage (TLS) internally to provide efficient execution when the static has already been initialized. The implementation of this feature relies on Windows operating system support functions in Windows Vista and later operating systems."
Even though the example uses a global static thread_local object, the same TLS machinery is involved. The initialization code runs very early and uses TLS and the heap. If there is an OS bug or a TLS limit/interaction in a specific Windows build, heap allocation during TLS initialization can trigger an access violation or similar crash.
- OS-level TLS limits and crashes
The documentation explicitly notes that exceeding TLS-related limits or issues in older OSes can cause crashes:
"These operating systems also have a lower limit on the number of TLS sections that can be loaded. Exceeding the TLS section limit can cause a crash. If this is a problem in your code … use /Zc:threadSafeInit- to disable the thread-safe initialization code."
While that remark is about older Windows versions, it shows that TLS initialization and heap use are sensitive to OS behavior and limits. A regression or change in specific Windows 11 builds can therefore manifest as crashes when a thread_local object’s constructor allocates from the heap.
- Managed/WinRT restriction (for completeness)
In managed (/clr) or WinRT (/ZW) code, dynamic initialization of thread_local or __declspec(thread) is not allowed:
"In managed or WinRT code, variables declared by using the __declspec(thread) storage class modifier attribute or the thread_local storage class specifier cannot be initialized with an expression that requires evaluation at run-time. A static expression is required to initialize __declspec(thread) or thread_local data in these runtime environments."
This is enforced by compiler error C2482. In pure native code (as in the example), this restriction does not apply, but it illustrates that dynamic initialization of TLS objects is a known fragile area.
- Practical mitigations based on the documented behavior
Given the above, and staying within the documented behavior:
- Avoid heavy or heap-allocating constructors for
thread_local globals that run at process or thread startup. Prefer:
- A trivially constructible
thread_local object plus an explicit init() that does heap work later, or
- A
thread_local pointer initialized to nullptr and lazily allocating on first use.
- If the crash is related to thread-safe initialization overhead and TLS usage, and thread-safety is not required in the specific scenario, the documentation suggests disabling thread-safe initialization:
- Compile with
/Zc:threadSafeInit- to revert to non-thread-safe static initialization and avoid the UCRT-based TLS helper code:
"To avoid taking a dependency on the UCRT, or to preserve the non-thread-safe initialization behavior of versions of Visual Studio prior to Visual Studio 2015, use the /Zc:threadSafeInit- option. If you know that thread-safety is not required, use this option to generate slightly smaller, faster code around static local declarations."
- Be aware that this option affects static local initialization semantics globally; it should only be used when thread-safety for static locals is not needed.
- Summary
-
thread_local objects are implemented via TLS and initialized at thread startup; MSVC’s thread-safe initialization also uses TLS internally.
- Heap allocation in a
thread_local constructor during this phase can expose OS-level TLS or heap bugs or limits, leading to crashes on specific Windows builds.
- Workarounds include restructuring initialization to avoid heap work in
thread_local constructors and, where safe, disabling thread-safe initialization with /Zc:threadSafeInit-.
References: