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).

Reply via email to