add stdlib test for TLS dtor order

This commit is contained in:
Ralf Jung 2022-06-07 07:58:21 -04:00
parent ad576d8f9e
commit 61f5680da0

View File

@ -71,7 +71,116 @@ fn check_blocking() {
thread::yield_now();
}
// This test tests that TLS destructors have run before the thread joins. The
// test has no false positives (meaning: if the test fails, there's actually
// an ordering problem). It may have false negatives, where the test passes but
// join is not guaranteed to be after the TLS destructors. However, false
// negatives should be exceedingly rare due to judicious use of
// thread::yield_now and running the test several times.
fn join_orders_after_tls_destructors() {
use std::sync::atomic::{AtomicU8, Ordering};
// We emulate a synchronous MPSC rendezvous channel using only atomics and
// thread::yield_now. We can't use std::mpsc as the implementation itself
// may rely on thread locals.
//
// The basic state machine for an SPSC rendezvous channel is:
// FRESH -> THREAD1_WAITING -> MAIN_THREAD_RENDEZVOUS
// where the first transition is done by the “receiving” thread and the 2nd
// transition is done by the “sending” thread.
//
// We add an additional state `THREAD2_LAUNCHED` between `FRESH` and
// `THREAD1_WAITING` to block until all threads are actually running.
//
// A thread that joins on the “receiving” thread completion should never
// observe the channel in the `THREAD1_WAITING` state. If this does occur,
// we switch to the “poison” state `THREAD2_JOINED` and panic all around.
// (This is equivalent to “sending” from an alternate producer thread.)
const FRESH: u8 = 0;
const THREAD2_LAUNCHED: u8 = 1;
const THREAD1_WAITING: u8 = 2;
const MAIN_THREAD_RENDEZVOUS: u8 = 3;
const THREAD2_JOINED: u8 = 4;
static SYNC_STATE: AtomicU8 = AtomicU8::new(FRESH);
for _ in 0..10 {
SYNC_STATE.store(FRESH, Ordering::SeqCst);
let jh = thread::Builder::new()
.name("thread1".into())
.spawn(move || {
struct TlDrop;
impl Drop for TlDrop {
fn drop(&mut self) {
let mut sync_state = SYNC_STATE.swap(THREAD1_WAITING, Ordering::SeqCst);
loop {
match sync_state {
THREAD2_LAUNCHED | THREAD1_WAITING => thread::yield_now(),
MAIN_THREAD_RENDEZVOUS => break,
THREAD2_JOINED => panic!(
"Thread 1 still running after thread 2 joined on thread 1"
),
v => unreachable!("sync state: {}", v),
}
sync_state = SYNC_STATE.load(Ordering::SeqCst);
}
}
}
thread_local! {
static TL_DROP: TlDrop = TlDrop;
}
TL_DROP.with(|_| {});
loop {
match SYNC_STATE.load(Ordering::SeqCst) {
FRESH => thread::yield_now(),
THREAD2_LAUNCHED => break,
v => unreachable!("sync state: {}", v),
}
}
})
.unwrap();
let jh2 = thread::Builder::new()
.name("thread2".into())
.spawn(move || {
assert_eq!(SYNC_STATE.swap(THREAD2_LAUNCHED, Ordering::SeqCst), FRESH);
jh.join().unwrap();
match SYNC_STATE.swap(THREAD2_JOINED, Ordering::SeqCst) {
MAIN_THREAD_RENDEZVOUS => return,
THREAD2_LAUNCHED | THREAD1_WAITING => {
panic!("Thread 2 running after thread 1 join before main thread rendezvous")
}
v => unreachable!("sync state: {:?}", v),
}
})
.unwrap();
loop {
match SYNC_STATE.compare_exchange(
THREAD1_WAITING,
MAIN_THREAD_RENDEZVOUS,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(FRESH) => thread::yield_now(),
Err(THREAD2_LAUNCHED) => thread::yield_now(),
Err(THREAD2_JOINED) => {
panic!("Main thread rendezvous after thread 2 joined thread 1")
}
v => unreachable!("sync state: {:?}", v),
}
}
jh2.join().unwrap();
}
}
fn main() {
check_destructors();
check_blocking();
join_orders_after_tls_destructors();
}