use std::collections::VecDeque;
use crate::compat::{Arc, Mutex, lock};
const STEAL_BATCH_LIMIT: usize = 8;
pub(super) struct WorkQueue<T> {
queue: Arc<Queue<T>>,
}
struct Queue<T> {
values: Mutex<VecDeque<T>>,
capacity: Option<usize>,
}
impl<T> Clone for WorkQueue<T> {
fn clone(&self) -> Self {
Self {
queue: self.queue.clone(),
}
}
}
impl<T> WorkQueue<T> {
pub(super) fn bounded(capacity: usize) -> Self {
Self {
queue: Arc::new(Queue {
values: Mutex::new(VecDeque::with_capacity(capacity)),
capacity: Some(capacity),
}),
}
}
pub(super) fn unbounded() -> Self {
Self {
queue: Arc::new(Queue {
values: Mutex::new(VecDeque::new()),
capacity: None,
}),
}
}
#[cfg(test)]
#[inline]
pub(super) fn push(&self, value: T) {
self.push_batch(std::iter::once(value));
}
#[inline]
pub(super) fn push_batch(&self, values: impl IntoIterator<Item = T>) {
let mut queue = lock(&self.queue.values);
for value in values {
if self
.queue
.capacity
.is_some_and(|capacity| queue.len() >= capacity)
{
unreachable!("work queue capacity matches the maximum staged batch");
}
queue.push_back(value);
}
}
#[inline]
pub(super) fn pop(&self) -> Option<T> {
lock(&self.queue.values).pop_front()
}
pub(super) fn steal_batch_into(&self, destination: &Self) -> Option<T> {
let (value, stolen) = {
let mut source = lock(&self.queue.values);
let value = source.pop_front()?;
let transfer = source.len().div_ceil(2).min(STEAL_BATCH_LIMIT - 1);
let mut stolen = std::array::from_fn::<_, { STEAL_BATCH_LIMIT - 1 }, _>(|_| None);
for slot in &mut stolen[..transfer] {
*slot = source.pop_front();
}
(value, stolen)
};
destination.push_batch(stolen.into_iter().flatten());
Some(value)
}
pub(super) fn drain_into(&self, destination: &Self) -> bool {
let drained = {
let mut source = lock(&self.queue.values);
std::mem::take(&mut *source)
};
let moved = !drained.is_empty();
destination.push_batch(drained);
moved
}
#[inline]
pub(super) fn len(&self) -> usize {
lock(&self.queue.values).len()
}
}
impl<T> std::fmt::Debug for WorkQueue<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkQueue")
.field("len", &self.len())
.finish_non_exhaustive()
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::WorkQueue;
#[test]
fn steal_returns_one_and_moves_half_the_remainder() {
let source = WorkQueue::bounded(8);
let destination = WorkQueue::bounded(8);
for value in 0..7 {
source.push(value);
}
assert_eq!(source.steal_batch_into(&destination), Some(0));
assert_eq!(source.len(), 3);
assert_eq!(destination.len(), 3);
let mut values = Vec::new();
while let Some(value) = source.pop() {
values.push(value);
}
while let Some(value) = destination.pop() {
values.push(value);
}
values.sort_unstable();
assert_eq!(values, (1..7).collect::<Vec<_>>());
}
#[test]
fn steal_batch_is_capped_at_eight_values() {
let source = WorkQueue::bounded(64);
let destination = WorkQueue::bounded(64);
for value in 0..64 {
source.push(value);
}
assert_eq!(source.steal_batch_into(&destination), Some(0));
assert_eq!(source.len(), 56);
assert_eq!(destination.len(), 7);
}
#[test]
fn drain_moves_every_value() {
let source = WorkQueue::bounded(4);
let destination = WorkQueue::unbounded();
for value in 0..4 {
source.push(value);
}
assert!(source.drain_into(&destination));
assert_eq!(source.len(), 0);
assert_eq!(destination.len(), 4);
assert!(!source.drain_into(&destination));
}
}
#[cfg(all(test, loom, target_pointer_width = "64"))]
mod loom_tests {
use loom::thread;
use super::WorkQueue;
#[test]
fn concurrent_steals_preserve_every_value() {
loom::model(|| {
let source = WorkQueue::bounded(2);
source.push(1);
source.push(2);
let first_source = source.clone();
let first = thread::spawn(move || drain_steal(&first_source));
let second_source = source.clone();
let second = thread::spawn(move || drain_steal(&second_source));
let mut values = first.join().unwrap();
values.extend(second.join().unwrap());
while let Some(value) = source.pop() {
values.push(value);
}
values.sort_unstable();
assert_eq!(values, [1, 2]);
});
}
#[test]
fn drain_racing_steal_preserves_every_value() {
loom::model(|| {
let source = WorkQueue::bounded(2);
source.push(1);
source.push(2);
let orphaned = WorkQueue::unbounded();
let drain_source = source.clone();
let drain_orphaned = orphaned.clone();
let drain = thread::spawn(move || {
drain_source.drain_into(&drain_orphaned);
});
let steal_source = source.clone();
let steal = thread::spawn(move || drain_steal(&steal_source));
drain.join().unwrap();
let mut values = steal.join().unwrap();
while let Some(value) = source.pop() {
values.push(value);
}
while let Some(value) = orphaned.pop() {
values.push(value);
}
values.sort_unstable();
assert_eq!(values, [1, 2]);
});
}
fn drain_steal(source: &WorkQueue<usize>) -> Vec<usize> {
let destination = WorkQueue::bounded(2);
let mut values = source
.steal_batch_into(&destination)
.into_iter()
.collect::<Vec<_>>();
while let Some(value) = destination.pop() {
values.push(value);
}
values
}
}