use std::sync::{Arc, Condvar, Mutex};
#[derive(Clone)]
pub struct ThreadSynchronizer {
inner: Arc<Inner>,
}
struct Inner {
counter: Mutex<usize>,
condvar: Condvar,
#[cfg(feature = "async")]
waker: Mutex<Option<std::task::Waker>>,
}
impl ThreadSynchronizer {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Inner {
counter: Mutex::new(0),
condvar: Condvar::new(),
#[cfg(feature = "async")]
waker: Mutex::new(None),
}),
}
}
pub(crate) fn thread_start(&self) {
let mut counter = self
.inner
.counter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*counter += 1;
}
pub(crate) fn thread_done_with(&self, on_last: impl FnOnce()) {
#[cfg(feature = "async")]
let mut waker_to_fire: Option<std::task::Waker> = None;
{
let mut counter = self
.inner
.counter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*counter -= 1;
if *counter == 0 {
on_last();
self.inner.condvar.notify_one();
#[cfg(feature = "async")]
{
waker_to_fire = self
.inner
.waker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
}
}
#[cfg(feature = "async")]
if let Some(waker) = waker_to_fire {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || waker.wake()));
}
}
pub(crate) fn wait_for_all_threads(&self) {
let mut counter = self
.inner
.counter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while *counter > 0 {
counter = self
.inner
.condvar
.wait(counter)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
#[cfg(feature = "async")]
pub(crate) fn all_threads_done(&self) -> bool {
*self
.inner
.counter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
== 0
}
#[cfg(feature = "async")]
pub(crate) fn set_waker(&self, waker: std::task::Waker) {
let mut waker_slot = self
.inner
.waker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*waker_slot = Some(waker);
}
}
pub(crate) struct ThreadDoneGuard {
thread_sync: ThreadSynchronizer,
scheduler_status: std::sync::Arc<std::sync::atomic::AtomicUsize>,
scheduler_result: std::sync::Arc<std::sync::Mutex<Option<crate::error::Result<()>>>>,
}
impl ThreadDoneGuard {
pub(crate) fn adopt(
thread_sync: ThreadSynchronizer,
scheduler_status: std::sync::Arc<std::sync::atomic::AtomicUsize>,
scheduler_result: std::sync::Arc<std::sync::Mutex<Option<crate::error::Result<()>>>>,
) -> Self {
Self {
thread_sync,
scheduler_status,
scheduler_result,
}
}
}
impl Drop for ThreadDoneGuard {
fn drop(&mut self) {
if std::thread::panicking() {
let name = std::thread::current()
.name()
.unwrap_or("worker")
.to_string();
crate::core::scheduler::ffmpeg_scheduler::set_scheduler_error(
&self.scheduler_status,
&self.scheduler_result,
crate::error::Error::WorkerPanicked(name),
);
}
let status = &self.scheduler_status;
self.thread_sync.thread_done_with(|| {
status.store(
crate::core::scheduler::ffmpeg_scheduler::STATUS_END,
std::sync::atomic::Ordering::Release,
);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
#[test]
fn on_last_runs_before_waiters_wake() {
let sync = ThreadSynchronizer::new();
sync.thread_start();
let flag = Arc::new(AtomicBool::new(false));
let sync_clone = sync.clone();
let flag_clone = Arc::clone(&flag);
let handle = thread::spawn(move || {
sync_clone.wait_for_all_threads();
flag_clone.load(Ordering::Acquire)
});
thread::sleep(Duration::from_millis(50));
let flag_clone = Arc::clone(&flag);
sync.thread_done_with(move || flag_clone.store(true, Ordering::Release));
assert!(
handle.join().unwrap(),
"the last-thread closure must run before any waiter wakes"
);
}
}
#[cfg(test)]
mod panic_tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn panicking_worker_records_error_before_releasing_the_slot() {
let sync = ThreadSynchronizer::new();
let status = Arc::new(std::sync::atomic::AtomicUsize::new(
crate::core::scheduler::ffmpeg_scheduler::STATUS_RUN,
));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
sync.thread_start();
let guard = ThreadDoneGuard::adopt(sync.clone(), status.clone(), result.clone());
let handle = std::thread::Builder::new()
.name("panicky-worker".to_string())
.spawn(move || {
let _guard = guard;
panic!("test-injected worker panic");
})
.unwrap();
sync.wait_for_all_threads();
let recorded = result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
match recorded {
Some(Err(crate::error::Error::WorkerPanicked(name))) => {
assert_eq!(name, "panicky-worker");
}
other => panic!("expected WorkerPanicked, got {other:?}"),
}
assert!(crate::core::scheduler::ffmpeg_scheduler::is_stopping(
status.load(std::sync::atomic::Ordering::Acquire)
));
let _ = handle.join();
}
#[test]
fn clean_worker_records_no_error() {
let sync = ThreadSynchronizer::new();
let status = Arc::new(std::sync::atomic::AtomicUsize::new(
crate::core::scheduler::ffmpeg_scheduler::STATUS_RUN,
));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
sync.thread_start();
let guard = ThreadDoneGuard::adopt(sync.clone(), status.clone(), result.clone());
std::thread::spawn(move || {
let _guard = guard;
})
.join()
.unwrap();
assert!(result.lock().unwrap().is_none());
}
#[cfg(feature = "async")]
#[test]
fn panicking_waker_is_contained_and_does_not_poison_the_counter() {
use std::task::{RawWaker, RawWakerVTable, Waker};
unsafe fn clone_panic(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &PANIC_VTABLE)
}
unsafe fn wake_panic(_: *const ()) {
panic!("test-injected waker panic");
}
unsafe fn drop_noop(_: *const ()) {}
static PANIC_VTABLE: RawWakerVTable =
RawWakerVTable::new(clone_panic, wake_panic, wake_panic, drop_noop);
let sync = ThreadSynchronizer::new();
sync.thread_start();
sync.set_waker(unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &PANIC_VTABLE)) });
sync.thread_done_with(|| {});
sync.thread_start();
sync.thread_done_with(|| {});
sync.wait_for_all_threads();
}
#[cfg(feature = "async")]
#[test]
fn all_threads_done_tracks_the_counter() {
let sync = ThreadSynchronizer::new();
assert!(sync.all_threads_done(), "no live threads => done");
sync.thread_start();
assert!(!sync.all_threads_done(), "one live thread => not done");
sync.thread_start();
sync.thread_done_with(|| {});
assert!(
!sync.all_threads_done(),
"one of two threads still live => not done"
);
sync.thread_done_with(|| {});
assert!(sync.all_threads_done(), "all slots released => done");
}
}