executor: rewrite the work-stealing thread pool (#1657)

This patch is a ground up rewrite of the existing work-stealing thread
pool. The goal is to reduce overhead while simplifying code when
possible.

At a high level, the following architectural changes were made:

- The local run queues were switched for bounded circle buffer queues.
- Reduce cross-thread synchronization.
- Refactor task constructs to use a single allocation and always include
  a join handle (#887).
- Simplify logic around putting workers to sleep and waking them up.

**Local run queues**

Move away from crossbeam's implementation of the Chase-Lev deque. This
implementation included unnecessary overhead as it supported
capabilities that are not needed for the work-stealing thread pool.
Instead, a fixed size circle buffer is used for the local queue. When
the local queue is full, half of the tasks contained in it are moved to
the global run queue.

**Reduce cross-thread synchronization**

This is done via many small improvements. Primarily, an upper bound is
placed on the number of concurrent stealers. Limiting the number of
stealers results in lower contention. Secondly, the rate at which
workers are notified and woken up is throttled. This also reduces
contention by preventing many threads from racing to steal work.

**Refactor task structure**

Now that Tokio is able to target a rust version that supports
`std::alloc` as well as `std::task`, the pool is able to optimize how
the task structure is laid out. Now, a single allocation per task is
required and a join handle is always provided enabling the spawner to
retrieve the result of the task (#887).

**Simplifying logic**

When possible, complexity is reduced in the implementation. This is done
by using locks and other simpler constructs in cold paths. The set of
sleeping workers is now represented as a `Mutex<VecDeque<usize>>`.
Instead of optimizing access to this structure, we reduce the amount the
pool must access this structure.

Secondly, we have (temporarily) removed `threadpool::blocking`. This
capability will come back later, but the original implementation was way
more complicated than necessary.

**Results**

The thread pool benchmarks have improved significantly:

Old thread pool:

```
test chained_spawn ... bench:   2,019,796 ns/iter (+/- 302,168)
test ping_pong     ... bench:   1,279,948 ns/iter (+/- 154,365)
test spawn_many    ... bench:  10,283,608 ns/iter (+/- 1,284,275)
test yield_many    ... bench:  21,450,748 ns/iter (+/- 1,201,337)
```

New thread pool:

```
test chained_spawn ... bench:     147,943 ns/iter (+/- 6,673)
test ping_pong     ... bench:     537,744 ns/iter (+/- 20,928)
test spawn_many    ... bench:   7,454,898 ns/iter (+/- 283,449)
test yield_many    ... bench:  16,771,113 ns/iter (+/- 733,424)
```

Real-world benchmarks improve significantly as well. This is testing the hyper hello
world server using: `wrk -t1 -c50 -d10`:

Old scheduler:

```
Running 10s test @ http://127.0.0.1:3000
  1 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   371.53us   99.05us   1.97ms   60.53%
    Req/Sec   114.61k     8.45k  133.85k    67.00%
  1139307 requests in 10.00s, 95.61MB read
Requests/sec: 113923.19
Transfer/sec:      9.56MB
```

New scheduler:

```
Running 10s test @ http://127.0.0.1:3000
  1 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   275.05us   69.81us   1.09ms   73.57%
    Req/Sec   153.17k    10.68k  171.51k    71.00%
  1522671 requests in 10.00s, 127.79MB read
Requests/sec: 152258.70
Transfer/sec:     12.78MB
```
This commit is contained in:
Carl Lerche
2019-10-19 11:09:40 -07:00
committed by GitHub
parent 2a181320b7
commit ed5a94eb2d
100 changed files with 7409 additions and 6796 deletions
+4
View File
@@ -1,4 +1,5 @@
#![warn(rust_2018_idioms)]
#![cfg(not(miri))]
use tokio::sync::oneshot;
use tokio_executor::current_thread::{self, block_on_all, CurrentThread, TaskExecutor};
@@ -141,6 +142,7 @@ mod from_block_on_future {
mod outstanding_tasks_are_dropped_when_executor_is_dropped {
use super::*;
#[allow(unreachable_code)] // TODO: remove this when https://github.com/rust-lang/rust/issues/64636 fixed.
async fn never(_rc: Rc<()>) {
loop {
yield_once().await;
@@ -241,6 +243,7 @@ mod run_in_future {
fn tick_on_infini_future() {
let num = Rc::new(Cell::new(0));
#[allow(unreachable_code)] // TODO: remove this when https://github.com/rust-lang/rust/issues/64636 fixed.
async fn infini(num: Rc<Cell<usize>>) {
loop {
num.set(1 + num.get());
@@ -259,6 +262,7 @@ fn tick_on_infini_future() {
mod tasks_are_scheduled_fairly {
use super::*;
#[allow(unreachable_code)] // TODO: remove this when https://github.com/rust-lang/rust/issues/64636 fixed.
async fn spin(state: Rc<RefCell<[i32; 2]>>, idx: usize) {
loop {
// borrow_mut scope
@@ -1,11 +1,9 @@
#![warn(rust_2018_idioms)]
use tokio_executor::park::{Park, Unpark};
use tokio_executor::threadpool;
use tokio_executor::threadpool::park::{DefaultPark, DefaultUnpark};
use tokio_executor::threadpool::*;
use tokio_test::assert_pending;
use tokio_executor::thread_pool::*;
use futures_util::future::poll_fn;
use std::cell::Cell;
use std::future::Future;
use std::pin::Pin;
@@ -18,74 +16,7 @@ use std::time::Duration;
thread_local!(static FOO: Cell<u32> = Cell::new(0));
#[test]
fn natural_shutdown_simple_futures() {
for _ in 0..1_000 {
let num_inc = Arc::new(AtomicUsize::new(0));
let num_dec = Arc::new(AtomicUsize::new(0));
FOO.with(|f| {
f.set(1);
let pool = {
let num_inc = num_inc.clone();
let num_dec = num_dec.clone();
Builder::new()
.around_worker(move |w| {
num_inc.fetch_add(1, Relaxed);
w.run();
num_dec.fetch_add(1, Relaxed);
})
.build()
};
let tx = pool.sender().clone();
let a = {
let (t, rx) = mpsc::channel();
tx.spawn(async move {
// Makes sure this runs on a worker thread
FOO.with(|f| assert_eq!(f.get(), 0));
t.send("one").unwrap();
})
.unwrap();
rx
};
let b = {
let (t, rx) = mpsc::channel();
tx.spawn(async move {
// Makes sure this runs on a worker thread
FOO.with(|f| assert_eq!(f.get(), 0));
t.send("two").unwrap();
})
.unwrap();
rx
};
drop(tx);
assert_eq!("one", a.recv().unwrap());
assert_eq!("two", b.recv().unwrap());
// Wait for the pool to shutdown
pool.shutdown().wait();
// Assert that at least one thread started
let num_inc = num_inc.load(Relaxed);
assert!(num_inc > 0);
// Assert that all threads shutdown
let num_dec = num_dec.load(Relaxed);
assert_eq!(num_inc, num_dec);
});
}
}
#[test]
fn force_shutdown_drops_futures() {
fn shutdown_drops_futures() {
for _ in 0..1_000 {
let num_inc = Arc::new(AtomicUsize::new(0));
let num_dec = Arc::new(AtomicUsize::new(0));
@@ -110,19 +41,20 @@ fn force_shutdown_drops_futures() {
let a = num_inc.clone();
let b = num_dec.clone();
let pool = Builder::new()
.around_worker(move |w| {
let mut pool = Builder::new()
.around_worker(move |_, work| {
a.fetch_add(1, Relaxed);
w.run();
work();
b.fetch_add(1, Relaxed);
})
.build();
let tx = pool.sender().clone();
tx.spawn(Never(num_drop.clone())).unwrap();
// let tx = pool.sender().clone();
pool.spawn(Never(num_drop.clone()));
// Wait for the pool to shutdown
pool.shutdown_now().wait();
pool.shutdown_now();
// Assert that only a single thread was spawned.
let a = num_inc.load(Relaxed);
@@ -140,6 +72,8 @@ fn force_shutdown_drops_futures() {
#[test]
fn drop_threadpool_drops_futures() {
const NUM_THREADS: usize = 10;
for _ in 0..1_000 {
let num_inc = Arc::new(AtomicUsize::new(0));
let num_dec = Arc::new(AtomicUsize::new(0));
@@ -165,24 +99,22 @@ fn drop_threadpool_drops_futures() {
let b = num_dec.clone();
let pool = Builder::new()
.max_blocking(2)
.pool_size(20)
.around_worker(move |w| {
.num_threads(NUM_THREADS)
.around_worker(move |_, work| {
a.fetch_add(1, Relaxed);
w.run();
work();
b.fetch_add(1, Relaxed);
})
.build();
let tx = pool.sender().clone();
tx.spawn(Never(num_drop.clone())).unwrap();
pool.spawn(Never(num_drop.clone()));
// Wait for the pool to shutdown
drop(pool);
// Assert that only a single thread was spawned.
// Assert that all the threads spawned
let a = num_inc.load(Relaxed);
assert!(a >= 1);
assert_eq!(a, NUM_THREADS);
// Assert that all threads shutdown
let b = num_dec.load(Relaxed);
@@ -196,26 +128,32 @@ fn drop_threadpool_drops_futures() {
#[test]
fn many_oneshot_futures() {
// used for notifying the main thread
const NUM: usize = 10_000;
for _ in 0..50 {
let pool = ThreadPool::new();
let tx = pool.sender().clone();
let (tx, rx) = mpsc::channel();
let mut pool = new_pool();
let cnt = Arc::new(AtomicUsize::new(0));
for _ in 0..NUM {
let cnt = cnt.clone();
tx.spawn(async move {
cnt.fetch_add(1, Relaxed);
})
.unwrap();
let tx = tx.clone();
pool.spawn(async move {
let num = cnt.fetch_add(1, Relaxed) + 1;
if num == NUM {
tx.send(()).unwrap();
}
});
}
// Wait for the pool to shutdown
pool.shutdown().wait();
rx.recv().unwrap();
let num = cnt.load(Relaxed);
assert_eq!(num, NUM);
// Wait for the pool to shutdown
pool.shutdown_now();
}
}
@@ -228,9 +166,7 @@ fn many_multishot_futures() {
const TRACKS: usize = 50;
for _ in 0..50 {
let pool = ThreadPool::new();
let pool_tx = pool.sender().clone();
let pool = new_pool();
let mut start_txs = Vec::with_capacity(TRACKS);
let mut final_rxs = Vec::with_capacity(TRACKS);
@@ -241,13 +177,11 @@ fn many_multishot_futures() {
let (mut next_tx, next_rx) = mpsc::channel(10);
// Forward all the messages
pool_tx
.spawn(async move {
while let Some(v) = chain_rx.recv().await {
next_tx.send(v).await.unwrap();
}
})
.unwrap();
pool.spawn(async move {
while let Some(v) = chain_rx.recv().await {
next_tx.send(v).await.unwrap();
}
});
chain_rx = next_rx;
}
@@ -257,21 +191,19 @@ fn many_multishot_futures() {
let mut cycle_tx = start_tx.clone();
let mut rem = CYCLES;
pool_tx
.spawn(async move {
for _ in 0..CYCLES {
let msg = chain_rx.recv().await.unwrap();
pool.spawn(async move {
for _ in 0..CYCLES {
let msg = chain_rx.recv().await.unwrap();
rem -= 1;
rem -= 1;
if rem == 0 {
final_tx.send(msg).await.unwrap();
} else {
cycle_tx.send(msg).await.unwrap();
}
if rem == 0 {
final_tx.send(msg).await.unwrap();
} else {
cycle_tx.send(msg).await.unwrap();
}
})
.unwrap();
}
});
start_txs.push(start_tx);
final_rxs.push(final_rx);
@@ -290,80 +222,36 @@ fn many_multishot_futures() {
}
});
}
// Shutdown the pool
pool.shutdown().wait();
}
}
#[test]
fn global_executor_is_configured() {
let pool = ThreadPool::new();
let tx = pool.sender().clone();
let pool = new_pool();
let (signal_tx, signal_rx) = mpsc::channel();
tx.spawn(async move {
pool.spawn(async move {
tokio_executor::spawn(async move {
signal_tx.send(()).unwrap();
});
})
.unwrap();
});
signal_rx.recv().unwrap();
pool.shutdown().wait();
}
#[test]
fn new_threadpool_is_idle() {
let pool = ThreadPool::new();
pool.shutdown_on_idle().wait();
}
#[test]
fn busy_threadpool_is_not_idle() {
use tokio_sync::oneshot;
// let pool = ThreadPool::new();
let pool = Builder::new().pool_size(4).max_blocking(2).build();
let tx = pool.sender().clone();
let (term_tx, term_rx) = oneshot::channel();
tx.spawn(async move {
term_rx.await.unwrap();
})
.unwrap();
let mut idle = pool.shutdown_on_idle();
struct IdleFut<'a>(&'a mut Shutdown);
impl Future for IdleFut<'_> {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
assert_pending!(Pin::new(&mut self.as_mut().0).poll(cx));
Poll::Ready(())
}
}
let idle_fut = IdleFut(&mut idle);
tokio_executor::enter().unwrap().block_on(idle_fut);
term_tx.send(()).unwrap();
let idle_fut = IdleFut(&mut idle);
tokio_executor::enter().unwrap().block_on(idle_fut);
let mut pool = new_pool();
pool.shutdown_now();
}
#[test]
fn panic_in_task() {
let pool = ThreadPool::new();
let tx = pool.sender().clone();
let pool = new_pool();
let (tx, rx) = mpsc::channel();
struct Boom;
struct Boom(mpsc::Sender<()>);
impl Future for Boom {
type Output = ();
@@ -376,37 +264,20 @@ fn panic_in_task() {
impl Drop for Boom {
fn drop(&mut self) {
assert!(::std::thread::panicking());
self.0.send(()).unwrap();
}
}
tx.spawn(Boom).unwrap();
pool.shutdown_on_idle().wait();
}
#[test]
fn count_panics() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_ = counter.clone();
let pool = threadpool::Builder::new()
.panic_handler(move |_err| {
// We caught a panic.
counter_.fetch_add(1, Relaxed);
})
.build();
// Spawn a future that will panic.
pool.spawn(async { panic!() });
pool.shutdown_on_idle().wait();
let counter = counter.load(Relaxed);
assert_eq!(counter, 1);
pool.spawn(Boom(tx));
rx.recv().unwrap();
}
#[test]
fn multi_threadpool() {
use tokio_sync::oneshot;
let pool1 = ThreadPool::new();
let pool2 = ThreadPool::new();
let pool1 = new_pool();
let pool2 = new_pool();
let (tx, rx) = oneshot::channel();
let (done_tx, done_rx) = mpsc::channel();
@@ -425,10 +296,11 @@ fn multi_threadpool() {
#[test]
fn eagerly_drops_futures() {
use std::sync::mpsc;
use std::sync::{mpsc, Mutex};
struct MyPark {
inner: DefaultPark,
rx: mpsc::Receiver<()>,
tx: Mutex<mpsc::Sender<()>>,
#[allow(dead_code)]
park_tx: mpsc::SyncSender<()>,
unpark_tx: mpsc::SyncSender<()>,
@@ -436,33 +308,35 @@ fn eagerly_drops_futures() {
impl Park for MyPark {
type Unpark = MyUnpark;
type Error = <DefaultPark as Park>::Error;
type Error = ();
fn unpark(&self) -> Self::Unpark {
MyUnpark {
inner: self.inner.unpark(),
tx: Mutex::new(self.tx.lock().unwrap().clone()),
unpark_tx: self.unpark_tx.clone(),
}
}
fn park(&mut self) -> Result<(), Self::Error> {
self.inner.park()
let _ = self.rx.recv();
Ok(())
}
fn park_timeout(&mut self, duration: Duration) -> Result<(), Self::Error> {
self.inner.park_timeout(duration)
let _ = self.rx.recv_timeout(duration);
Ok(())
}
}
struct MyUnpark {
inner: DefaultUnpark,
tx: Mutex<mpsc::Sender<()>>,
#[allow(dead_code)]
unpark_tx: mpsc::SyncSender<()>,
}
impl Unpark for MyUnpark {
fn unpark(&self) {
self.inner.unpark()
let _ = self.tx.lock().unwrap().send(());
}
}
@@ -471,13 +345,15 @@ fn eagerly_drops_futures() {
let (park_tx, park_rx) = mpsc::sync_channel(0);
let (unpark_tx, unpark_rx) = mpsc::sync_channel(0);
let pool = threadpool::Builder::new()
.custom_park(move |_| MyPark {
inner: DefaultPark::new(),
let pool = Builder::new().num_threads(4).build_with_park(move |_| {
let (tx, rx) = mpsc::channel();
MyPark {
tx: Mutex::new(tx),
rx,
park_tx: park_tx.clone(),
unpark_tx: unpark_tx.clone(),
})
.build();
}
});
struct MyTask {
task_tx: Option<mpsc::Sender<Waker>>,
@@ -523,3 +399,80 @@ fn eagerly_drops_futures() {
// Ensure `task` lives until after the test completes.
drop(task);
}
#[test]
fn park_called_at_interval() {
struct MyPark {
park_light: Arc<AtomicBool>,
}
struct MyUnpark {}
impl Park for MyPark {
type Unpark = MyUnpark;
type Error = ();
fn unpark(&self) -> Self::Unpark {
MyUnpark {}
}
fn park(&mut self) -> Result<(), Self::Error> {
use std::thread;
use std::time::Duration;
thread::sleep(Duration::from_millis(1));
Ok(())
}
fn park_timeout(&mut self, duration: Duration) -> Result<(), Self::Error> {
if duration == Duration::from_millis(0) {
self.park_light.store(true, Relaxed);
Ok(())
} else {
self.park()
}
}
}
impl Unpark for MyUnpark {
fn unpark(&self) {}
}
let park_light_1 = Arc::new(AtomicBool::new(false));
let park_light_2 = park_light_1.clone();
let (done_tx, done_rx) = mpsc::channel();
// Use 1 thread to ensure the worker stays busy.
let pool = Builder::new().num_threads(1).build_with_park(move |idx| {
assert_eq!(idx, 0);
MyPark {
park_light: park_light_2.clone(),
}
});
let mut cnt = 0;
pool.spawn(poll_fn(move |cx| {
let did_park_light = park_light_1.load(Relaxed);
if did_park_light {
// There is a bit of a race where the worker can tick a few times
// before seeing the task
assert!(cnt > 50);
done_tx.send(()).unwrap();
return Poll::Ready(());
}
cnt += 1;
cx.waker().wake_by_ref();
Poll::Pending
}));
done_rx.recv().unwrap();
}
fn new_pool() -> ThreadPool {
Builder::new().num_threads(4).build()
}
-412
View File
@@ -1,412 +0,0 @@
#![warn(rust_2018_idioms)]
use tokio_executor::threadpool::*;
use tokio_test::*;
use futures_core::ready;
use futures_util::future::poll_fn;
use rand::*;
use std::sync::atomic::Ordering::*;
use std::sync::atomic::*;
use std::sync::*;
use std::task::{Poll, Waker};
use std::thread;
use std::time::Duration;
#[test]
fn basic() {
let pool = Builder::new().pool_size(1).max_blocking(1).build();
let (tx1, rx1) = mpsc::channel();
let (tx2, rx2) = mpsc::channel();
pool.spawn(async move {
let res = blocking(|| {
let v = rx1.recv().unwrap();
tx2.send(v).unwrap();
});
assert_ready!(res).unwrap();
});
pool.spawn(async move {
tx1.send(()).unwrap();
});
rx2.recv().unwrap();
}
#[test]
fn other_executors_can_run_inside_blocking() {
let pool = Builder::new().pool_size(1).max_blocking(1).build();
let (tx, rx) = mpsc::channel();
pool.spawn(async move {
let res = blocking(|| {
let _e = tokio_executor::enter().expect("nested blocking enter");
tx.send(()).unwrap();
});
assert_ready!(res).unwrap();
});
rx.recv().unwrap();
}
#[test]
fn notify_task_on_capacity() {
const BLOCKING: usize = 10;
let pool = Builder::new().pool_size(1).max_blocking(1).build();
let rem = Arc::new(AtomicUsize::new(BLOCKING));
let (tx, rx) = mpsc::channel();
for _ in 0..BLOCKING {
let rem = rem.clone();
let tx = tx.clone();
pool.spawn(async move {
poll_fn(move |_| {
blocking(|| {
thread::sleep(Duration::from_millis(100));
let prev = rem.fetch_sub(1, SeqCst);
if prev == 1 {
tx.send(()).unwrap();
}
})
.map_err(|e| panic!("blocking err {:?}", e))
})
.await
.unwrap()
});
}
rx.recv().unwrap();
assert_eq!(0, rem.load(SeqCst));
}
#[test]
fn capacity_is_use_it_or_lose_it() {
use tokio_sync::oneshot;
// TODO: Run w/ bigger pool size
let pool = Builder::new().pool_size(1).max_blocking(1).build();
let (tx1, rx1) = mpsc::channel();
let (tx2, rx2) = oneshot::channel();
let (tx3, rx3) = mpsc::channel();
let (tx4, rx4) = mpsc::channel();
// First, fill the blocking capacity
pool.spawn(async move {
poll_fn(move |_| {
blocking(|| {
rx1.recv().unwrap();
})
})
.await
.unwrap()
});
pool.spawn(async move {
let task: Waker = rx2.await.unwrap();
poll_fn(move |_| {
blocking(|| {
// Notify the other task
task.wake_by_ref();
// Block until woken
rx3.recv().unwrap();
})
})
.await
.unwrap();
});
// Spawn a future that will try to block, get notified, then not actually
// use the blocking
let mut i = 0;
let mut tx2 = Some(tx2);
pool.spawn(async move {
poll_fn(move |cx| {
match i {
0 => {
i = 1;
let res = blocking(|| unreachable!()).map_err(|_| panic!());
assert_pending!(res);
// Unblock the first blocker
tx1.send(()).unwrap();
return Poll::Pending;
}
1 => {
i = 2;
// Skip blocking, and notify the second task that it should
// start blocking
let me = cx.waker().clone();
tx2.take().unwrap().send(me).unwrap();
return Poll::Pending;
}
2 => {
let res = blocking(|| unreachable!()).map_err(|_| panic!());
assert_pending!(res);
// Unblock the first blocker
tx3.send(()).unwrap();
tx4.send(()).unwrap();
Poll::Ready(())
}
_ => unreachable!(),
}
})
.await
});
rx4.recv().unwrap();
}
#[test]
fn blocking_thread_does_not_take_over_shutdown_worker_thread() {
let pool = Builder::new().pool_size(2).max_blocking(1).build();
let (enter_tx, enter_rx) = mpsc::channel();
let (exit_tx, exit_rx) = mpsc::channel();
let (try_tx, try_rx) = mpsc::channel();
let exited = Arc::new(AtomicBool::new(false));
{
let exited = exited.clone();
pool.spawn(async move {
poll_fn(move |_| {
blocking(|| {
enter_tx.send(()).unwrap();
exit_rx.recv().unwrap();
exited.store(true, SeqCst);
})
})
.await
.unwrap()
});
}
// Wait for the task to block
let _ = enter_rx.recv().unwrap();
// Spawn another task that attempts to block
pool.spawn(async move {
poll_fn(move |_| {
let res = blocking(|| {});
assert_eq!(res.is_ready(), exited.load(SeqCst));
try_tx.send(res.is_ready()).unwrap();
res.map(|_| ())
})
.await
});
// Wait for the second task to try to block (and not be ready).
let res = try_rx.recv().unwrap();
assert!(!res);
// Unblock the first task
exit_tx.send(()).unwrap();
// Wait for the second task to successfully block.
let res = try_rx.recv().unwrap();
assert!(res);
drop(pool);
}
#[test]
fn blocking_one_time_gets_capacity_for_multiple_blocks() {
const ITER: usize = 1;
const BLOCKING: usize = 2;
for _ in 0..ITER {
let pool = Builder::new().pool_size(4).max_blocking(1).build();
let rem = Arc::new(AtomicUsize::new(BLOCKING));
let (tx, rx) = mpsc::channel();
for _ in 0..BLOCKING {
let rem = rem.clone();
let tx = tx.clone();
pool.spawn(async move {
poll_fn(move |_| {
// First block
let res = blocking(|| {
thread::sleep(Duration::from_millis(100));
});
ready!(res).unwrap();
let res = blocking(|| {
thread::sleep(Duration::from_millis(100));
let prev = rem.fetch_sub(1, SeqCst);
if prev == 1 {
tx.send(()).unwrap();
}
});
assert!(res.is_ready());
Poll::Ready(())
})
.await
});
}
rx.recv().unwrap();
assert_eq!(0, rem.load(SeqCst));
}
}
#[test]
fn shutdown() {
const ITER: usize = 1_000;
const BLOCKING: usize = 10;
for _ in 0..ITER {
let num_inc = Arc::new(AtomicUsize::new(0));
let num_dec = Arc::new(AtomicUsize::new(0));
let (tx, rx) = mpsc::channel();
let pool = {
let num_inc = num_inc.clone();
let num_dec = num_dec.clone();
Builder::new()
.pool_size(1)
.max_blocking(BLOCKING)
.after_start(move || {
num_inc.fetch_add(1, SeqCst);
})
.before_stop(move || {
num_dec.fetch_add(1, SeqCst);
})
.build()
};
let barrier = Arc::new(Barrier::new(BLOCKING));
for _ in 0..BLOCKING {
let barrier = barrier.clone();
let tx = tx.clone();
pool.spawn(async move {
let res = blocking(|| {
barrier.wait();
Ok::<_, ()>(())
});
tx.send(()).unwrap();
assert!(res.is_ready());
});
}
for _ in 0..BLOCKING {
rx.recv().unwrap();
}
// Shutdown
drop(pool);
assert_eq!(11, num_inc.load(SeqCst));
assert_eq!(11, num_dec.load(SeqCst));
}
}
#[derive(Debug, Copy, Clone)]
enum Sleep {
Skip,
Yield,
Rand,
Fixed(Duration),
}
#[test]
fn hammer() {
use self::Sleep::*;
const ITER: usize = 5;
let combos = [
(2, 4, 1_000, Skip),
(2, 4, 1_000, Yield),
(2, 4, 100, Rand),
(2, 4, 100, Fixed(Duration::from_millis(3))),
(2, 4, 100, Fixed(Duration::from_millis(12))),
];
for &(size, max_blocking, n, sleep) in &combos {
for _ in 0..ITER {
let pool = Builder::new()
.pool_size(size)
.max_blocking(max_blocking)
.build();
let cnt_task = Arc::new(AtomicUsize::new(0));
let cnt_block = Arc::new(AtomicUsize::new(0));
for _ in 0..n {
let cnt_task = cnt_task.clone();
let cnt_block = cnt_block.clone();
pool.spawn(async move {
cnt_task.fetch_add(1, SeqCst);
poll_fn(move |_| {
blocking(|| {
match sleep {
Skip => {}
Yield => {
thread::yield_now();
}
Rand => {
let ms = thread_rng().gen_range(3, 12);
thread::sleep(Duration::from_millis(ms));
}
Fixed(dur) => {
thread::sleep(dur);
}
}
cnt_block.fetch_add(1, SeqCst);
})
.map_err(|_| panic!())
})
.await
.unwrap()
});
}
// Wait for the work to complete
pool.shutdown_on_idle().wait();
assert_eq!(n, cnt_task.load(SeqCst));
assert_eq!(n, cnt_block.load(SeqCst));
}
}
}
-126
View File
@@ -1,126 +0,0 @@
#![warn(rust_2018_idioms)]
use tokio_executor::threadpool::*;
use tokio_sync::{mpsc, oneshot};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::*;
use std::sync::Arc;
use std::task::{Context, Poll};
#[test]
fn hammer() {
const N: usize = 1000;
const ITER: usize = 20;
struct Counted<T> {
cnt: Arc<AtomicUsize>,
inner: T,
}
impl<T: Future> Future for Counted<T> {
type Output = T::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T::Output> {
unsafe {
let inner = &mut self.get_unchecked_mut().inner;
Pin::new_unchecked(inner).poll(cx)
}
}
}
impl<T> Drop for Counted<T> {
fn drop(&mut self) {
self.cnt.fetch_add(1, Relaxed);
}
}
for _ in 0..ITER {
let pool = Builder::new()
// .pool_size(30)
.build();
let cnt = Arc::new(AtomicUsize::new(0));
let (mut listen_tx, mut listen_rx) =
mpsc::unbounded_channel::<oneshot::Sender<oneshot::Sender<()>>>();
pool.spawn({
let c1 = cnt.clone();
let c2 = cnt.clone();
let pool = pool.sender().clone();
let task = async move {
while let Some(tx) = listen_rx.recv().await {
let task = async {
let (tx2, rx2) = oneshot::channel();
tx.send(tx2).unwrap();
rx2.await.unwrap()
};
pool.spawn(Counted {
inner: task,
cnt: c1.clone(),
})
.unwrap();
}
};
/*
let task = listen_rx
.map_err(|e| panic!("accept error = {:?}", e))
.for_each(move |tx| {
let task = future::lazy(|| {
let (tx2, rx2) = oneshot::channel();
tx.send(tx2).unwrap();
rx2
})
.map_err(|e| panic!("e={:?}", e))
.and_then(|_| Ok(()));
pool.spawn(Counted {
inner: task,
cnt: c1.clone(),
})
.unwrap();
Ok(())
});
*/
Counted {
inner: task,
cnt: c2,
}
});
for _ in 0..N {
let cnt = cnt.clone();
let (tx, rx) = oneshot::channel();
listen_tx.try_send(tx).unwrap();
pool.spawn(async {
let task = async {
let tx = rx.await.unwrap();
tx.send(()).unwrap();
};
/*
let task = rx.map_err(|e| panic!("rx err={:?}", e)).and_then(|tx| {
tx.send(()).unwrap();
Ok(())
});
*/
Counted { inner: task, cnt }.await
});
}
drop(listen_tx);
pool.shutdown_on_idle().wait();
assert_eq!(N * 2 + 1, cnt.load(Relaxed));
}
}