async-scoped-0.9.0/.cargo_vcs_info.json0000644000000001360000000000100134320ustar { "git": { "sha1": "15fa11deb2a5d26fbd00d6cf26e97f73763ce2ba" }, "path_in_vcs": "" }async-scoped-0.9.0/.gitignore000064400000000000000000000000361046102023000142110ustar 00000000000000/target **/*.rs.bk Cargo.lock async-scoped-0.9.0/CHANGELOG.md000064400000000000000000000010331046102023000140300ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. ## [0.9.0] - 2024-01-24 - Use `FuturesOrdered` inside `Scope`. [#19](https://github.com/rmanoka/async-scoped/pull/19) - Dropping an empty scope no longer requires blocking call to the runtime. [#23](https://github.com/rmanoka/async-scoped/pull/23) ### Miscellaneous Tasks - Using "in parallel" in document to be more natural. [#26](https://github.com/rmanoka/async-scoped/pull/26) ## [0.8.0] - 2023-11-16 ### Cargo.toml - Rm unused slab dependency async-scoped-0.9.0/Cargo.toml0000644000000032450000000000100114340ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "async-scoped" version = "0.9.0" authors = ["Rajsekar Manokaran "] description = "Spawn scoped (non 'static) asynchronous futures for async_std and tokio runtimes" homepage = "https://github.com/rmanoka/async-scoped" documentation = "https://docs.rs/async-scoped" readme = "README.md" keywords = [ "async", "async-std", "tokio", "scoped", "spawn", ] categories = [ "asynchronous", "concurrency", ] license = "Apache-2.0/MIT" repository = "https://github.com/rmanoka/async-scoped" [package.metadata.docs.rs] features = [ "use-async-std", "use-tokio", ] [[bench]] name = "spawner" path = "benches/spawner.rs" harness = false [dependencies.async-std] version = "1.12.0" optional = true [dependencies.futures] version = "0.3.15" [dependencies.pin-project] version = "1.0" [dependencies.tokio] version = "1.0" features = [ "rt-multi-thread", "macros", "sync", ] optional = true [dev-dependencies.async-std] version = "1.12.0" features = ["attributes"] [dev-dependencies.femme] version = "2.2.1" [dev-dependencies.log] version = "0.4.20" features = ["kv_unstable"] [features] use-async-std = ["async-std"] use-tokio = ["tokio"] async-scoped-0.9.0/Cargo.toml.orig000064400000000000000000000023321046102023000151110ustar 00000000000000[package] name = "async-scoped" version = "0.9.0" authors = ["Rajsekar Manokaran "] edition = "2018" documentation = "https://docs.rs/async-scoped" description = "Spawn scoped (non 'static) asynchronous futures for async_std and tokio runtimes" homepage = "https://github.com/rmanoka/async-scoped" repository = "https://github.com/rmanoka/async-scoped" readme = "README.md" categories = [ "asynchronous", "concurrency" ] keywords = [ "async", "async-std", "tokio", "scoped", "spawn" ] license = "Apache-2.0/MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] pin-project = "1.0" async-std = { version = "1.12.0", optional = true } futures = "0.3.15" tokio = {version = "1.0", features = ["rt-multi-thread", "macros", "sync"], optional = true} [features] # Verify package.metadata.docs.rs when updating use-async-std = ["async-std"] use-tokio = ["tokio"] [dev-dependencies] femme = "2.2.1" log = { version = "0.4.20", features = ["kv_unstable"] } async-std = { version = "1.12.0", features = ["attributes"] } [package.metadata.docs.rs] features = ["use-async-std", "use-tokio"] [[bench]] name = "spawner" path = "benches/spawner.rs" harness = false async-scoped-0.9.0/README.md000064400000000000000000000027671046102023000135150ustar 00000000000000# Async-scoped Enables controlled spawning of non-`'static` futures when using the [async-std](//github.com/async-rs/async-std) or [tokio](//github.com/tokio-rs/tokio) executors. ## Motivation Present executors (such as async-std, tokio, etc.) all support spawning `'static` futures onto a thread-pool. However, they do not support spawning futures with lifetime smaller than `'static`. While the future combinators such as `for_each_concurrent` offer concurrency, they are bundled as a single `Task` structure by the executor, and hence are not driven in parallel. This can be seen when benchmarking a reasonable number (> ~1K) of I/O futures, or a few CPU heavy futures. ## Usage The API is meant to be a minimal wrapper around efficient executors. Users may use "use-async-std", or the "use-tokio" features, to obtain a specific global executor implementation. These features provide `TokioScope` and `AsyncScope` that support spawning, and blocking. However, none of those features are necessary - you may freely implement your own executor. See [docs.rs](https://docs.rs/async-scoped) for detailed documentation. ## License Licensed under either of [Apache License, Version 2.0](//www.apache.org/licenses/LICENSE-2.0) or [MIT license](//opensource.org/licenses/MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. async-scoped-0.9.0/benches/spawner.rs000064400000000000000000000031021046102023000156520ustar 00000000000000use async_scoped::{ spawner::{Blocker, Spawner}, Scope, }; use futures::future::pending; use std::hint::black_box; fn spawn_and_collect + Blocker>(s: &mut Scope) { const INPUT_SIZE: usize = 1000000; const MAX_DELAY: usize = 1 << 8; for i in 0..INPUT_SIZE { let delay = i & MAX_DELAY; s.spawn(async move { let _ = async_std::future::timeout( std::time::Duration::from_millis((MAX_DELAY - delay) as u64), pending::<()>(), ) .await; i }); } } fn main() { let r = { #[cfg(feature = "async-std")] { // Async-std runtime does not have a straightforward way to configure multi-threaded // runtime: https://docs.rs/async-std/latest/async_std/index.html#runtime-configuration // ASYNC_STD_THREAD_COUNT environment variable must be used to match the Tokio benchmark use async_scoped::AsyncStdScope; let (_, r) = AsyncStdScope::scope_and_block(spawn_and_collect); r } #[cfg(feature = "use-tokio")] { use async_scoped::TokioScope; tokio::runtime::Builder::new_multi_thread() .worker_threads(4) .build() .unwrap() .block_on(async move { let (_, r) = TokioScope::scope_and_block(spawn_and_collect); r }) } }; r.into_iter().for_each(|v| { let _ = black_box(v); }) } async-scoped-0.9.0/src/lib.rs000064400000000000000000000131661046102023000141340ustar 00000000000000//! Enables controlled spawning of non-`'static` futures //! when using the [async-std] or [tokio] //! executors. Note that this idea is similar to //! `crossbeam::scope`, and `rayon::scope` but asynchronous. //! //! ## Motivation //! //! Executors like async_std, tokio, etc. support spawning //! `'static` futures onto a thread-pool. However, it is //! often useful to spawn futures that may not be `'static`. //! //! While the future combinators such as //! [`for_each_concurrent`][for_each_concurrent] offer //! concurrency, they are bundled as a single [`Task`][Task] //! structure by the executor, and hence are not driven //! in parallel. //! //! ## Scope API //! //! We propose an API similar to //! [`crossbeam::scope`](crossbeam::scope) to allow spawning //! futures that are not `'static`. The key API is approximately: //! //! ``` rust, ignore //! pub unsafe fn scope<'a, T: Send + 'static, //! F: FnOnce(&mut TokioScope<'a, T>)>(f: F) //! -> impl Stream { //! // ... //! } //! ``` //! //! See [`scope`][Scope::scope] for the exact definition, and //! safety guidelines. The simplest and safest API is //! [`scope_and_block`][Scope::scope_and_block], used as follows: //! //! ``` rust, ignore //! async fn scoped_futures() { //! let not_copy = String::from("hello world!"); //! let not_copy_ref = ¬_copy; //! let (foo, outputs) = async_scoped::AsyncStdScope::scope_and_block(|s| { //! for _ in 0..10 { //! let proc = || async { //! assert_eq!(not_copy_ref, "hello world!"); //! eprintln!("Hello world!") //! }; //! s.spawn(proc()); //! } //! 42 //! }); //! assert_eq!(foo, 42); //! assert_eq!(outputs.len(), 10); //! } //! ``` //! //! The [`scope_and_block`][Scope::scope_and_block] function above //! blocks the current thread until all spawned futures are //! driven in order to guarantee safety. //! //! We also provide an unsafe //! [`scope_and_collect`][Scope::scope_and_collect], which is //! asynchronous, and does not block the current thread. //! However, the user should ensure that the returned future //! _is not forgetten_ before being driven to completion. //! //! ## Executor Selection //! //! Users may use "use-async-std", or the //! "use-tokio" features to enable specific executor implementations. //! Those are not necessary, you may freely implement traits `Spawner`, `Blocker`, etc for your own //! runtime. Just ensure you follow the safety idea. //! //! Some notes on default implementations: //! 1. [`AsyncScope`] may run into a dead-lock if used in //! deep recursions (depth > #num-cores / 2). //! //! 2. [`TokioScope::scope_and_block`][Scope::scope_and_block] may only be used //! within a multi-threaded. An incompletely driven //! `TokioScope` also needs a multi-threaded context to be //! dropped. //! //! ## Cancellation //! //! To support cancellation, `Scope` provides a //! [`spawn_cancellable`][Scope::spawn_cancellable] which wraps a //! future to make it cancellable. When a `Scope` is //! dropped, (or if `cancel` method is invoked), all the //! cancellable futures are scheduled for cancellation. In //! the next poll of the futures, they are dropped and a //! default value (provided by a closure during spawn) is //! returned as the output of the future. //! //! **Note:** this is an abrupt, hard cancellation. It also //! requires a reasonable behaviour: futures that do not //! return control to the executor cannot be cancelled once //! it has started. //! //! ## Safety Considerations //! //! The [`scope`][Scope::scope] API provided in this crate is //! unsafe as it is possible to `forget` the stream received //! from the API without driving it to completion. The only //! completely (without any additional assumptions) safe API //! is the [`scope_and_block`][Scope::scope_and_block] function, //! which _blocks the current thread_ until all spawned //! futures complete. //! //! The [`scope_and_block`][Scope::scope_and_block] may not be //! convenient in an asynchronous setting. In this case, the //! [`scope_and_collect`][Scope::scope_and_collect] API may be //! used. Care must be taken to ensure the returned future //! is not forgotten before being driven to completion. //! //! Note that dropping this future will lead to it being //! driven to completion, while blocking the current thread //! to ensure safety. However, it is unsafe to forget this //! future before it is fully driven. //! //! ## Implementation //! //! Our current implementation simply uses _unsafe_ glue to //! `transmute` the lifetime, to actually spawn the futures //! in the executor. The original lifetime is recorded in //! the `Scope`. This allows the compiler to enforce the //! necessary lifetime requirements as long as this returned //! stream is not forgotten. //! //! For soundness, we drive the stream to completion in the //! [`Drop`][Drop] impl. The current thread is blocked until //! the stream is fully driven. //! //! Unfortunately, since the [`std::mem::forget`][forget] //! method is allowed in safe Rust, the purely asynchronous //! API here is _inherently unsafe_. //! //! [async-std]: /async_std //! [tokio]: /tokio //! [poll]: std::futures::Future::poll //! [Task]: std::task //! [forget]: std::mem::forget //! [Stream]: futures::Stream //! [for_each_concurrent]: futures::StreamExt::for_each_concurrent #[macro_use] mod utils; mod scoped; pub use scoped::Scope; #[cfg(feature = "use-tokio")] pub type TokioScope<'a, T> = Scope<'a, T, spawner::use_tokio::Tokio>; #[cfg(feature = "use-async-std")] pub type AsyncStdScope<'a, T> = Scope<'a, T, spawner::use_async_std::AsyncStd>; pub mod spawner; mod usage; #[cfg(test)] mod tests; async-scoped-0.9.0/src/scoped.rs000064400000000000000000000120511046102023000146330ustar 00000000000000use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; use futures::future::{AbortHandle, Abortable}; use futures::stream::FuturesOrdered; use futures::{Future, Stream}; use pin_project::*; use crate::spawner::*; /// A scope to allow controlled spawning of non 'static /// futures. Futures can be spawned using `spawn` or /// `spawn_cancellable` methods. /// /// # Safety /// /// This type uses `Drop` implementation to guarantee /// safety. It is not safe to forget this object unless it /// is driven to completion. #[pin_project(PinnedDrop)] pub struct Scope<'a, T, Sp: Spawner + Blocker> { spawner: Option, len: usize, #[pin] futs: FuturesOrdered, abort_handles: Vec, // Future proof against variance changes _marker: PhantomData &'a ()>, _spawn_marker: PhantomData, } impl<'a, T: Send + 'static, Sp: Spawner + Blocker> Scope<'a, T, Sp> { /// Create a Scope object. /// /// This function is unsafe as `futs` may hold futures /// which have to be manually driven to completion. pub unsafe fn create(spawner: Sp) -> Self { Scope { spawner: Some(spawner), len: 0, futs: FuturesOrdered::new(), abort_handles: vec![], _marker: PhantomData, _spawn_marker: PhantomData, } } fn spawner(&self) -> &Sp { self.spawner .as_ref() .expect("invariant:spawner is always available until scope is dropped") } /// Spawn a future with the executor's `task::spawn` functionality. The /// future is expected to be driven to completion before 'a expires. pub fn spawn + Send + 'a>(&mut self, f: F) { let handle = self.spawner().spawn(unsafe { std::mem::transmute::<_, Pin + Send>>>( Box::pin(f) as Pin>> ) }); self.futs.push_back(handle); self.len += 1; } /// Spawn a cancellable future with the executor's `task::spawn` /// functionality. /// /// The future is cancelled if the `Scope` is dropped /// pre-maturely. It can also be cancelled by explicitly /// calling (and awaiting) the `cancel` method. #[inline] pub fn spawn_cancellable + Send + 'a, Fu: FnOnce() -> T + Send + 'a>( &mut self, f: F, default: Fu, ) { let (h, reg) = AbortHandle::new_pair(); self.abort_handles.push(h); let fut = Abortable::new(f, reg); self.spawn(async { fut.await.unwrap_or_else(|_| default()) }) } /// Spawn a function as a blocking future with executor's `spawn_blocking` /// functionality. /// /// The future is cancelled if the `Scope` is dropped /// pre-maturely. It can also be cancelled by explicitly /// calling (and awaiting) the `cancel` method. pub fn spawn_blocking T + Send + 'a>(&mut self, f: F) where Sp: FuncSpawner>::SpawnHandle>, { let handle = self.spawner().spawn_func(unsafe { std::mem::transmute::<_, Box T + Send>>( Box::new(f) as Box T + Send> ) }); self.futs.push_back(handle); self.len += 1; } } impl<'a, T, Sp: Spawner + Blocker> Scope<'a, T, Sp> { /// Cancel all futures spawned with cancellation. #[inline] pub fn cancel(&mut self) { for h in self.abort_handles.drain(..) { h.abort(); } } /// Total number of futures spawned in this scope. #[inline] pub fn len(&self) -> usize { self.len } /// Number of futures remaining in this scope. #[inline] pub fn remaining(&self) -> usize { self.futs.len() } /// A slighly optimized `collect` on the stream. Also /// useful when we can not move out of self. pub async fn collect(&mut self) -> Vec { let mut proc_outputs = Vec::with_capacity(self.remaining()); use futures::StreamExt; while let Some(item) = self.next().await { proc_outputs.push(item); } proc_outputs } } impl<'a, T, Sp: Spawner + Blocker> Stream for Scope<'a, T, Sp> { type Item = Sp::FutureOutput; fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { self.project().futs.poll_next(cx) } fn size_hint(&self) -> (usize, Option) { (self.remaining(), Some(self.remaining())) } } #[pinned_drop] impl<'a, T, Sp: Spawner + Blocker> PinnedDrop for Scope<'a, T, Sp> { fn drop(mut self: Pin<&mut Self>) { if self.remaining() > 0 { let spawner = self .spawner .take() .expect("invariant:spawner must be taken only on drop"); spawner.block_on(async { self.cancel(); self.collect().await; }); } } } async-scoped-0.9.0/src/spawner.rs000064400000000000000000000052741046102023000150460ustar 00000000000000//! There you can find traits that are necessary for implementing executor. //! They are mostly unsafe, as we can't ensure the executor allows intended manipulations //! without causing UB. use futures::Future; pub unsafe trait Spawner { type FutureOutput; type SpawnHandle: Future + Send; fn spawn + Send + 'static>(&self, f: F) -> Self::SpawnHandle; } pub unsafe trait FuncSpawner { type FutureOutput; type SpawnHandle: Future + Send; fn spawn_func T + Send + 'static>(&self, f: F) -> Self::SpawnHandle; } pub unsafe trait Blocker { fn block_on>(&self, f: F) -> T; } #[cfg(feature = "use-async-std")] pub mod use_async_std { use super::*; use async_std::task::{block_on, spawn, spawn_blocking, JoinHandle}; #[derive(Default)] pub struct AsyncStd; unsafe impl Spawner for AsyncStd { type FutureOutput = T; type SpawnHandle = JoinHandle; fn spawn + Send + 'static>(&self, f: F) -> Self::SpawnHandle { spawn(f) } } unsafe impl FuncSpawner for AsyncStd { type FutureOutput = T; type SpawnHandle = JoinHandle; fn spawn_func T + Send + 'static>(&self, f: F) -> Self::SpawnHandle { spawn_blocking(f) } } unsafe impl Blocker for AsyncStd { fn block_on>(&self, f: F) -> T { block_on(f) } } } #[cfg(feature = "use-tokio")] pub mod use_tokio { use super::*; use tokio::task as tokio_task; #[derive(Default)] pub struct Tokio; unsafe impl Spawner for Tokio { type FutureOutput = Result; type SpawnHandle = tokio_task::JoinHandle; fn spawn + Send + 'static>(&self, f: F) -> Self::SpawnHandle { tokio_task::spawn(f) } } unsafe impl FuncSpawner for Tokio { type FutureOutput = Result; type SpawnHandle = tokio_task::JoinHandle; fn spawn_func T + Send + 'static>(&self, f: F) -> Self::SpawnHandle { tokio_task::spawn_blocking(f) } } unsafe impl Blocker for Tokio { fn block_on>(&self, f: F) -> T { tokio_task::block_in_place(|| { tokio::runtime::Builder::new_current_thread() .build() .unwrap() .block_on(f) }) } } } async-scoped-0.9.0/src/tests.rs000064400000000000000000000307441046102023000145310ustar 00000000000000macro_rules! test_fixtures { ($($item:item)*) => { $( #[cfg_attr(feature = "use-async-std", async_std::test)] #[cfg_attr(all(feature = "use-tokio", not( feature = "use-async-std" )), tokio::test(flavor = "multi_thread", worker_threads=1))] $item )* } } cfg_async_std! { use crate::AsyncStdScope as Scope; fn future_value(v: T) -> T { v } } cfg_async_std_or_else! { use crate::TokioScope as Scope; fn future_value(v: Result) -> T { v.expect("join error while unwrapping value") } } test_fixtures! { async fn test_scope() { let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let (stream, _) = unsafe { Scope::scope(|s| { for _ in 0..10 { let proc = || async move { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn(proc()); } })}; // })}; // Uncomment this for compile error // std::mem::drop(not_copy); use futures::StreamExt; let count = stream.collect::>().await.len(); // Drop here is okay, as stream has been consumed. std::mem::drop(not_copy); assert_eq!(count, 10); } // Test scope bounds: should allow any future with lifetime // larger than the scope's lifetime async fn scope_lifetime() { use std::future::Future; let static_fut = futures::future::ready(()); fn test_static(_: &F) {} test_static(&static_fut); let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let ((), vals) = unsafe { Scope::scope_and_collect(|s| { s.spawn(static_fut); for _ in 0..10 { let proc = || async { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn(proc()); } })}.await; assert_eq!(vals.len(), 11); } async fn scope_async() { let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let stream = unsafe { use async_std::future::{timeout, pending}; use std::time::Duration; let mut s = Scope::create(Default::default()); for _ in 0..10 { let proc = || async move { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn(proc()); let _ = timeout( Duration::from_millis(10), pending::<()>(), ).await; } s }; // Uncomment this for compile error // std::mem::drop(not_copy); use futures::StreamExt; let count = stream.collect::>().await.len(); // Drop here is okay, as stream has been consumed. std::mem::drop(not_copy); assert_eq!(count, 10); } async fn test_scope_and_collect() { let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let (_, vals) = unsafe { Scope::scope_and_collect(|s| { for _ in 0..10 { let proc = || async { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn(proc()); } }) }.await; assert_eq!(vals.len(), 10); } async fn test_scope_and_block() { let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let ((), vals) = Scope::scope_and_block(|s| { for _ in 0..10 { let proc = || async { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn(proc()); } }); assert_eq!(vals.len(), 10); } async fn test_scope_and_block_spawn_blocking() { let not_copy = String::from("hello world!"); let not_copy_ref = ¬_copy; let ((), vals) = Scope::scope_and_block(|s| { for _ in 0..10 { let proc = || { assert_eq!(not_copy_ref, "hello world!"); }; s.spawn_blocking(proc); } }); assert_eq!(vals.len(), 10); } // Check that a cancellable future works as the // contained future under normal circumstances. async fn test_cancellation_completeness() { use async_std::future; use std::time::*; // Represents a work future async fn proc() -> bool { future::timeout( Duration::from_millis(100), future::pending::<()>(), ).await.is_err() } let ((), items) = Scope::scope_and_block(|scope| { scope.spawn_cancellable(proc(), || false); }); assert_eq!(items.len(), 1); for i in items { assert_eq!(future_value(i), true); } } // Check that a cancellable future works as the // contained future under normal circumstances. async fn test_cancellation_works() { use async_std::future; use std::time::*; // Represents a work future async fn proc() -> bool { future::timeout( Duration::from_millis(10000), future::pending::<()>(), ).await.is_err() } let ((), items) = Scope::scope_and_block(|scope| { scope.spawn_cancellable(proc(), || false); scope.cancel(); }); assert_eq!(items.len(), 1); for i in items { assert_eq!(future_value(i), false); } } // This is a simplified version of the soundness bug // pointed out on [reddit][reddit-ref]. Here, we test that // it does not happen when using the `scope_and_collect`, // but the returned future is not forgotten. Forgetting the // future should lead to an invalid memory access. // // [reddit-ref]: https://www.reddit.com/r/rust/comments/ee3vsu/asyncscoped_spawn_non_static_futures_with_asyncstd/fbpis3c?utm_source=share&utm_medium=web2x async fn test_cancellation_soundness() { use async_std::future; use std::time::*; async fn inner() { let mut shared = true; let shared_ref = &mut shared; let start = Instant::now(); let mut fut = Box::pin( unsafe { Scope::scope_and_collect(|scope| { scope.spawn_cancellable(async { assert!(future::timeout( Duration::from_millis(500), future::pending::<()>(), ).await.is_err()); eprintln!("Trying to write to shared_ref"); *shared_ref = false; assert!(*shared_ref); }, || ()); })} ); let _ = future::timeout(Duration::from_millis(10), &mut fut).await; // Dropping explicitly to measure time taken to complete drop. // Change the drop to forget for panic due to invalid mem. access. std::mem::drop(fut); let elapsed = start.elapsed().as_millis(); // The cancelled future should have been polled // before the inner large timeout. assert!(elapsed < 100); eprintln!("Elapsed: {}ms", start.elapsed().as_millis()); } inner().await; // This timeout allows any (possible) invalid memory // access to actually take place. assert!(future::timeout(Duration::from_millis(600), future::pending::<()>()).await.is_err()); } // This test is resource consuming and ignored by default #[ignore] async fn backpressure() { let mut s = unsafe { Scope::create(Default::default()) }; let limit = 0x10; for i in 0..0x100 { s.spawn(async { // Allocate a large array (256 MB) let blob = vec![42u8; 0x10000000]; // Spend a lot of time on it asynchronously use async_std::future; use std::time::Duration; let _ = future::timeout( Duration::from_millis(100), future::pending::<()>() ).await; std::mem::drop(blob); }); while s.remaining() > limit { use futures::StreamExt; s.next().await; } eprintln!("Spawned {} futures", i); } } // Mutability test: should fail to compile. // TODO: use compiletest_rs // #[async_std::test] // async fn mutating_scope() { // let mut not_copy = String::from("hello world!"); // let not_copy_ref = &mut not_copy; // let mut count = 0; // crate::scope_and_block(|s| { // for _ in 0..10 { // let proc = || async { // not_copy_ref.push('.'); // }; // s.spawn(proc()); //~ ERROR // } // }); // assert_eq!(count, 10); // } // https://github.com/rmanoka/async-scoped/issues/2 // https://github.com/async-rs/async-std/issues/644 async fn test_async_deadlock() { use std::future::Future; use futures::FutureExt; fn nth(n: usize) -> impl Future + Send { eprintln!("nth({})", n); async move { let mut result: usize = 0; Scope::scope_and_block(|scope| { if n > 0 { scope.spawn(async { let rec = { nth(n-1).boxed() }.await; result = rec + 1; }); } }); eprintln!("nth({})={}", n, result); result } } let input = 4; assert_eq!(nth(input).await, input); } async fn test_ordered_collect() { use std::future::pending; const N: u64 = 10; let (_, r) = Scope::scope_and_block(|scope| { for i in 0..N { scope.spawn(async move { let _ = async_std::future::timeout( std::time::Duration::from_millis(100 - i), pending::<()>() ).await; i }); } }); let r = r.into_iter().map(|v| { #[cfg(feature = "use-tokio")] { v.unwrap() } #[cfg(feature = "use-async-std")] { v } }).collect::>(); assert_eq!((0..N).into_iter().collect::>(), r); } } #[cfg(feature = "use-tokio")] // https://github.com/rmanoka/async-scoped/issues/2 // https://github.com/async-rs/async-std/issues/644 #[tokio::test(flavor = "multi_thread", worker_threads=1)] async fn test_async_deadlock_tokio() { use std::future::Future; use futures::FutureExt; use crate::TokioScope; fn nth(n: usize) -> impl Future + Send { // eprintln!("nth({})", n); async move { let result = if n == 0 { 0 } else { TokioScope::scope_and_block(|scope| { scope.spawn(nth(n-1).boxed()); }).1[0].as_ref().unwrap() + 1 }; // eprintln!("nth({})={}", n, result); result } } // Tokio has a block_in_place functionality, that lets // us recurse without deadlocks. let input = 200; assert_eq!(nth(input).await, input); } /// Dropping an empty scope should be a no-op. #[test] fn test_empty_scope() { use crate::spawner::{Blocker, Spawner}; use std::future::Future; struct PanickingSpawner; unsafe impl Spawner for PanickingSpawner { type FutureOutput = T; type SpawnHandle = futures::future::Ready; fn spawn + Send + 'static>(&self, _f: F) -> Self::SpawnHandle { panic!("spawn should never be called."); } } unsafe impl Blocker for PanickingSpawner { fn block_on>(&self, _f: F) -> T { panic!("block_on should never be called."); } } let _ = unsafe { crate::Scope::<(), _>::create(PanickingSpawner) }; } async-scoped-0.9.0/src/usage.rs000064400000000000000000000072651046102023000144750ustar 00000000000000use crate::spawner::*; use crate::Scope; impl<'a, T, Sp: Spawner + Blocker + Default> Scope<'a, T, Sp> { /// Creates a `Scope` to spawn non-'static futures. The /// function is called with a block which takes an `&mut /// Scope`. The `spawn` method on this arg. can be used to /// spawn "local" futures. /// /// # Returns /// /// The function returns the created `Scope`, and the return /// value of the block passed to it. The returned stream and /// is expected to be driven completely before being /// forgotten. Dropping this stream causes the stream to be /// driven _while blocking the current thread_. The values /// returned from the stream are the output of the futures /// spawned. /// /// # Safety /// /// The returned stream is expected to be run to completion /// before being forgotten. Dropping it is okay, but blocks /// the current thread until all spawned futures complete. pub unsafe fn scope(f: F) -> (Self, R) where T: Send + 'static, Sp: Spawner + Blocker, F: FnOnce(&mut Scope<'a, T, Sp>) -> R, { let mut scope = Scope::create(Default::default()); let op = f(&mut scope); (scope, op) } /// A function that creates a scope and immediately awaits, /// _blocking the current thread_ for spawned futures to /// complete. The outputs of the futures are collected as a /// `Vec` and returned along with the output of the block. /// /// # Safety /// /// This function is safe to the best of our understanding /// as it blocks the current thread until the stream is /// driven to completion, implying that all the spawned /// futures have completed too. However, care must be taken /// to ensure a recursive usage of this function doesn't /// lead to deadlocks. /// /// When scope is used recursively, you may also use the /// unsafe `scope_and_*` functions as long as this function /// is used at the top level. In this case, either the /// recursively spawned should have the same lifetime as the /// top-level scope, or there should not be any spurious /// future cancellations within the top level scope. pub fn scope_and_block(f: F) -> (R, Vec) where T: Send + 'static, Sp: Spawner + Blocker, F: FnOnce(&mut Scope<'a, T, Sp>) -> R, { let (mut stream, block_output) = unsafe { Self::scope(f) }; let proc_outputs = Sp::default().block_on(stream.collect()); (block_output, proc_outputs) } /// An asynchronous function that creates a scope and /// immediately awaits the stream. The outputs of the /// futures are collected as a `Vec` and returned along with /// the output of the block. /// /// # Safety /// /// This function is _not completely safe_: please see /// `cancellation_soundness` in [tests.rs][tests-src] for a /// test-case that suggests how this can lead to invalid /// memory access if not dealt with care. /// /// The caller must ensure that the lifetime 'a is valid /// until the returned future is fully driven. Dropping the /// future is okay, but blocks the current thread until all /// spawned futures complete. /// /// [tests-src]: https://github.com/rmanoka/async-scoped/blob/master/src/tests.rs pub async unsafe fn scope_and_collect(f: F) -> (R, Vec) where T: Send + 'static, F: FnOnce(&mut Scope<'a, T, Sp>) -> R, { let (mut stream, block_output) = Self::scope(f); let proc_outputs = stream.collect().await; (block_output, proc_outputs) } } async-scoped-0.9.0/src/utils.rs000064400000000000000000000016031046102023000145170ustar 00000000000000macro_rules! cfg_async_std { ($($item:item)*) => { $( #[cfg(feature = "use-async-std")] $item )* } } #[allow(unused_macros)] macro_rules! cfg_async_std_or_else { ($($item:item)*) => { $( #[cfg(all(feature = "use-tokio", not( feature = "use-async-std" ) ))] $item )* } } macro_rules! cfg_tokio { ($($item:item)*) => { $( #[cfg(feature = "use-tokio")] $item )* } } macro_rules! cfg_any_spawner { ($($item:item)*) => { $( #[cfg(any(feature = "use-async-std", feature = "use-tokio"))] $item )* } } #[allow(unused_macros)] macro_rules! cfg_no_spawner { ($($item:item)*) => { $( #[cfg(not( any(feature = "use-async-std", feature = "use-tokio") ))] $item )* } }