https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128197
--- Comment #2 from 13330742392 at wo dot cn ---
## 1. Reproducibility
100% reproducible here, and independent of language standard and optimization
level
(6 runs per configuration, all hung; 24/24 total):
| flags | result |
|---|---|
| `-std=c++20 -O0` | hangs 6/6 |
| `-std=c++20 -O2` | hangs 6/6 |
| `-std=c++23 -O0` | hangs 6/6 |
| `-std=c++23 -O2` | hangs 6/6 |
Also hangs with `-static-libstdc++ -static-libgcc` (6/6 so far: 10/10 in my
run),
so it is not a dynamic-link/DLL artifact.
## 2. Host / Target / Build (please fill the respective fields)
```
Host : x86_64-w64-mingw32
Target : x86_64-w64-mingw32
Build : x86_64-w64-mingw32
OS : Microsoft Windows 11 Home China, version 10.0.26300, build 26300
gcc : 16.2.0 (Rev5, Built by MSYS2 project)
Thread model: posix (--enable-threads=posix)
Runtimes: libstdc++-6.dll, libwinpthread-1.dll 1.0.0.0 (unmodified MSYS2 UCRT64
builds)
Debugger: GNU gdb 18.1
```
## 3. Backtraces of the two involved threads (gdb, attached while hung)
**Worker thread — blocked inside `request_stop()`:**
```
#12 t::{lambda()#1}::operator()() const t.cpp:7
#11 std::stop_source::request_stop stop_token:538
#10 std::stop_token::_Stop_state_t::_M_request_stop stop_token:265 <--
__cb->_M_done.release()
#9 std::counting_semaphore<1>::release semaphore:73
#8 std::__semaphore_impl::_M_release semaphore_base.h:149
#7 std::__atomic_notify_address<long long>
(__addr=..., __all=true, __bare_wait=true) atomic_wait.h:399
#6 ?? libstdc++-6.dll
#5 ucrtbase!_register_onexit_function ucrtbase.dll
#4 ucrtbase!_register_onexit_function ucrtbase.dll
#3 ntdll!RtlEnterCriticalSection <-- blocks here
#2 ntdll!RtlSleepConditionVariableCS
#1 ntdll!RtlRaiseException
#0 ntdll!ZwWaitForAlertByThreadId
```
**Main thread — running static destructors inside the UCRT exit-table
executor:**
```
#4 std::jthread::join thread:209
#5 std::jthread::~jthread thread:179
#6 __tcft () t.cpp:3
#7 ucrtbase!bsearch_s ucrtbase.dll
#8 ucrtbase!_execute_onexit_table ucrtbase.dll <-- holds
the onexit lock
#9 ucrtbase!_execute_onexit_table ucrtbase.dll
#10 ucrtbase!_wcsnicmp_l ucrtbase.dll
#11 ucrtbase!_wcsnicmp_l ucrtbase.dll
#12 ucrtbase!logbf ucrtbase.dll
```
## 4. Hypothesis
`_M_request_stop()` (`stop_token:265`) calls `__cb->_M_done.release()` on a
`counting_semaphore<1>` (`_M_done` is a `binary_semaphore` member of
`_Stop_cb`,
`stop_token:141`). `_M_release()` notifies only when the counter goes 0→1
(`semaphore_base.h:146-149`), so this is the **first** release on that
semaphore and
therefore the **first use of the platform notify path in this process**.
mingw-w64/UCRT has no futex, so `__atomic_notify_address()` uses the generic
wait-table
implementation (`__wait_impl` / `__notify_impl`, declared in
`bits/atomic_wait.h:343/346`,
defined inside `libstdc++-6.dll`). The backtrace shows that path ending up in
ucrtbase onexit-family code and blocking on a CRITICAL_SECTION that the
**exiting thread
already owns** inside `_execute_onexit_table`.
This looks like *lazy first-use initialization inside a notification*
(plausibly an
`__cxa_atexit`/onexit registration) being performed at a moment when the
process is already
running the exit table. `_M_request_stop()` is `noexcept` and is only supposed
to notify,
so it must not block.
## 5. Control experiments (each 10 runs, same machine)
| # | Variant | Result |
|---|---|---|
| A | testcase as reported | **hangs 10/10** |
| B | `main` first sleeps 50 ms so the worker finishes before exit | OK 10/10 |
| C | `std::jthread` made a local object in `main` (joined inside `main`, not
during the exit-table phase) | OK 10/10 |
| D | `std::stop_callback` removed (only `request_stop()` remains) | OK 10/10 |
| E | callback body replaced with `printf`, and the 1 ms sleep removed | still
**hangs 10/10** (so unrelated to iostream/sleep) |
| F | **`std::jthread` replaced by a plain global object holding `std::thread`,
joined in its destructor** | still **hangs 10/10** |
| G | testcase built with `-static-libstdc++ -static-libgcc` | still **hangs
10/10** |
Variant F shows this is **not specific to `std::jthread`**: any thread joined
during the
static-destruction phase reproduces it. Variant D shows a registered
`stop_callback` is
required (it is what reaches `_M_done.release()`).
Variant F/G sources can be provided as attachments if useful.
## 6. Suggested fix directions
1. Do not perform lazy initialization (or anything that can take a UCRT lock)
inside the
notify path; initialize the generic wait table eagerly during libstdc++
startup.
2. Prefer `WaitOnAddress` / `WakeByAddressSingle/All` (Windows 8+) over the
generic table
on this target so that notification never enters the CRT exit machinery.
3. Ensure `__atomic_notify_address()` is non-blocking by contract.
## 7. User-visible workaround
- Join / finish the affected thread **before** returning from `main`, or
- avoid keeping a live `std::stop_callback` on a `stop_source` that may be
`request_stop()`-ed while other threads are still running at process exit
(e.g. replace the callback-based wake-up with an atomic flag plus polling).