//! Utilities for improved cooperative scheduling. //! //! ### Cooperative scheduling //! //! A single call to [`poll`] on a top-level task may potentially do a lot of //! work before it returns `Poll::Pending`. If a task runs for a long period of //! time without yielding back to the executor, it can starve other tasks //! waiting on that executor to execute them, or drive underlying resources. //! Since Rust does not have a runtime, it is difficult to forcibly preempt a //! long-running task. Instead, this module provides an opt-in mechanism for //! futures to collaborate with the executor to avoid starvation. //! //! Consider a future like this one: //! //! ``` //! # use tokio_stream::{Stream, StreamExt}; //! async fn drop_all<I: Stream + Unpin>(mut input: I) { //! while let Some(_) = input.next().await {} //! } //! ``` //! //! It may look harmless, but consider what happens under heavy load if the //! input stream is _always_ ready. If we spawn `drop_all`, the task will never //! yield, and will starve other tasks and resources on the same executor. //! //! To account for this, Tokio has explicit yield points in a number of library //! functions, which force tasks to return to the executor periodically. //! //! //! #### unconstrained //! //! If necessary, [`task::unconstrained`] lets you opt a future out of Tokio's cooperative //! scheduling. When a future is wrapped with `unconstrained`, it will never be forced to yield to //! Tokio. For example: //! //! ``` //! # #[tokio::main(flavor = "current_thread")] //! # async fn main() { //! use tokio::{task, sync::mpsc}; //! //! let fut = async { //! let (tx, mut rx) = mpsc::unbounded_channel(); //! //! for i in 0..1000 { //! let _ = tx.send(()); //! // This will always be ready. If coop was in effect, this code would be forced to yield //! // periodically. However, if left unconstrained, then this code will never yield. //! rx.recv().await; //! } //! }; //! //! task::coop::unconstrained(fut).await; //! # } //! ``` //! [`poll`]: method@std::future::Future::poll //! [`task::unconstrained`]: crate::task::unconstrained()
cfg_rt! { mod consume_budget; pubuse consume_budget::consume_budget;
mod unconstrained; pubuse unconstrained::{unconstrained, Unconstrained};
}
// ```ignore // # use tokio_stream::{Stream, StreamExt}; // async fn drop_all<I: Stream + Unpin>(mut input: I) { // while let Some(_) = input.next().await { // tokio::coop::proceed().await; // } // } // ``` // // The `proceed` future will coordinate with the executor to make sure that // every so often control is yielded back to the executor so it can run other // tasks. // // # Placing yield points // // Voluntary yield points should be placed _after_ at least some work has been // done. If they are not, a future sufficiently deep in the task hierarchy may // end up _never_ getting to run because of the number of yield points that // inevitably appear before it is reached. In general, you will want yield // points to only appear in "leaf" futures -- those that do not themselves poll // other futures. By doing this, you avoid double-counting each iteration of // the outer future against the cooperating budget.
usecrate::runtime::context;
/// Opaque type tracking the amount of "work" a task may still do before /// yielding back to the scheduler. #[derive(Debug, Copy, Clone)] pub(crate) struct Budget(Option<u8>);
impl Budget { /// Budget assigned to a task on each poll. /// /// The value itself is chosen somewhat arbitrarily. It needs to be high /// enough to amortize wakeup and scheduling costs, but low enough that we /// do not starve other tasks for too long. The value also needs to be high /// enough that particularly deep tasks are able to do at least some useful /// work at all. /// /// Note that as more yield points are added in the ecosystem, this value /// will probably also have to be raised. constfn initial() -> Budget {
Budget(Some(128))
}
/// Returns an unconstrained budget. Operations will not be limited. pub(crate) constfn unconstrained() -> Budget {
Budget(None)
}
/// Runs the given closure with a cooperative task budget. When the function /// returns, the budget is reset to the value prior to calling the function. #[inline(always)] pub(crate) fn budget<R>(f: impl FnOnce() -> R) -> R {
with_budget(Budget::initial(), f)
}
/// Runs the given closure with an unconstrained task budget. When the function returns, the budget /// is reset to the value prior to calling the function. #[inline(always)] pub(crate) fn with_unconstrained<R>(f: impl FnOnce() -> R) -> R {
with_budget(Budget::unconstrained(), f)
}
impl Drop for ResetGuard { fn drop(&mutself) { let _ = context::budget(|cell| {
cell.set(self.prev);
});
}
}
#[allow(unused_variables)] let maybe_guard = context::budget(|cell| { let prev = cell.get();
cell.set(budget);
ResetGuard { prev }
});
// The function is called regardless even if the budget is not successfully // set due to the thread-local being destroyed.
f()
}
/// Returns `true` if there is still budget left on the task. /// /// # Examples /// /// This example defines a `Timeout` future that requires a given `future` to complete before the /// specified duration elapses. If it does, its result is returned; otherwise, an error is returned /// and the future is canceled. /// /// Note that the future could exhaust the budget before we evaluate the timeout. Using `has_budget_remaining`, /// we can detect this scenario and ensure the timeout is always checked. /// /// ``` /// # use std::future::Future; /// # use std::pin::{pin, Pin}; /// # use std::task::{ready, Context, Poll}; /// # use tokio::task::coop; /// # use tokio::time::Sleep; /// pub struct Timeout<T> { /// future: T, /// delay: Pin<Box<Sleep>>, /// } /// /// impl<T> Future for Timeout<T> /// where /// T: Future + Unpin, /// { /// type Output = Result<T::Output, ()>; /// /// fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { /// let this = Pin::into_inner(self); /// let future = Pin::new(&mut this.future); /// let delay = Pin::new(&mut this.delay); /// /// // check if the future is ready /// let had_budget_before = coop::has_budget_remaining(); /// if let Poll::Ready(v) = future.poll(cx) { /// return Poll::Ready(Ok(v)); /// } /// let has_budget_now = coop::has_budget_remaining(); /// /// // evaluate the timeout /// if let (true, false) = (had_budget_before, has_budget_now) { /// // it is the underlying future that exhausted the budget /// ready!(pin!(coop::unconstrained(delay)).poll(cx)); /// } else { /// ready!(delay.poll(cx)); /// } /// return Poll::Ready(Err(())); /// } /// } ///``` #[inline(always)] #[cfg_attr(docsrs, doc(cfg(feature = "rt")))] pubfn has_budget_remaining() -> bool { // If the current budget cannot be accessed due to the thread-local being // shutdown, then we assume there is budget remaining.
context::budget(|cell| cell.get().has_remaining()).unwrap_or(true)
}
cfg_rt_multi_thread! { /// Sets the current task's budget. pub(crate) fn set(budget: Budget) { let _ = context::budget(|cell| cell.set(budget));
}
}
cfg_coop! { use pin_project_lite::pin_project; use std::cell::Cell; use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::task::{ready, Context, Poll};
/// Value returned by the [`poll_proceed`] method. #[derive(Debug)] #[must_use] pubstruct RestoreOnPending(Cell<Budget>, PhantomData<*mut ()>);
/// Signals that the task that obtained this `RestoreOnPending` was able to make /// progress. This prevents the task budget from being restored to the value /// it had prior to obtaining this instance when it is dropped. pubfn made_progress(&self) { self.0.set(Budget::unconstrained());
}
}
impl Drop for RestoreOnPending { fn drop(&mutself) { // Don't reset if budget was unconstrained or if we made progress. // They are both represented as the remembered budget being unconstrained. let budget = self.0.get(); if !budget.is_unconstrained() { let _ = context::budget(|cell| {
cell.set(budget);
});
}
}
}
/// Decrements the task budget and returns [`Poll::Pending`] if the budget is depleted. /// This indicates that the task should yield to the scheduler. Otherwise, returns /// [`RestoreOnPending`] which can be used to commit the budget consumption. /// /// The returned [`RestoreOnPending`] will revert the budget to its former /// value when dropped unless [`RestoreOnPending::made_progress`] /// is called. It is the caller's responsibility to do so when it _was_ able to /// make progress after the call to [`poll_proceed`]. /// Restoring the budget automatically ensures the task can try to make progress in some other /// way. /// /// Note that [`RestoreOnPending`] restores the budget **as it was before [`poll_proceed`]**. /// Therefore, if the budget is _further_ adjusted between when [`poll_proceed`] returns and /// [`RestoreOnPending`] is dropped, those adjustments are erased unless the caller indicates /// that progress was made. /// /// # Examples /// /// This example wraps the `futures::channel::mpsc::UnboundedReceiver` to /// cooperate with the Tokio scheduler. Each time a value is received, task budget /// is consumed. If no budget is available, the task yields to the scheduler. /// /// ``` /// use std::pin::Pin; /// use std::task::{ready, Context, Poll}; /// use tokio::task::coop; /// use futures::stream::{Stream, StreamExt}; /// use futures::channel::mpsc::UnboundedReceiver; /// /// struct CoopUnboundedReceiver<T> { /// receiver: UnboundedReceiver<T>, /// } /// /// impl<T> Stream for CoopUnboundedReceiver<T> { /// type Item = T; /// fn poll_next( /// mut self: Pin<&mut Self>, /// cx: &mut Context<'_> /// ) -> Poll<Option<T>> { /// let coop = ready!(coop::poll_proceed(cx)); /// match self.receiver.poll_next_unpin(cx) { /// Poll::Ready(v) => { /// // We received a value, so consume budget. /// coop.made_progress(); /// Poll::Ready(v) /// } /// Poll::Pending => Poll::Pending, /// } /// } /// } /// ``` #[inline] pubfn poll_proceed(cx: &mut Context<'_>) -> Poll<RestoreOnPending> {
context::budget(|cell| { letmut budget = cell.get();
let decrement = budget.decrement();
if decrement.success { let restore = RestoreOnPending::new(cell.get());
cell.set(budget);
// avoid double counting if decrement.hit_zero {
inc_budget_forced_yield_count();
}
/// Returns `Poll::Ready` if the current task has budget to consume, and `Poll::Pending` otherwise. /// /// Note that in contrast to `poll_proceed`, this method does not consume any budget and is used when /// polling for budget availability. #[inline] pub(crate) fn poll_budget_available(cx: &mut Context<'_>) -> Poll<()> { if has_budget_remaining() {
Poll::Ready(())
} else {
register_waker(cx);
/// Creates a wrapper future that makes the inner future cooperate with the Tokio scheduler. /// /// When polled, the wrapper will first call [`poll_proceed`] to consume task budget, and /// immediately yield if the budget has been depleted. If budget was available, the inner future /// is polled. The budget consumption will be made final using [`RestoreOnPending::made_progress`] /// if the inner future resolves to its final value. /// /// # Examples /// /// When you call `recv` on the `Receiver` of a [`tokio::sync::mpsc`](crate::sync::mpsc) /// channel, task budget will automatically be consumed when the next value is returned. /// This makes tasks that use Tokio mpsc channels automatically cooperative. /// /// If you're using [`futures::channel::mpsc`](https://docs.rs/futures/latest/futures/channel/mpsc/index.html) /// instead, automatic task budget consumption will not happen. This example shows how can use /// `cooperative` to make `futures::channel::mpsc` channels cooperate with the scheduler in the /// same way Tokio channels do. /// /// ``` /// use tokio::task::coop::cooperative; /// use futures::channel::mpsc::Receiver; /// use futures::stream::StreamExt; /// /// async fn receive_next<T>(receiver: &mut Receiver<T>) -> Option<T> { /// // Use `StreamExt::next` to obtain a `Future` that resolves to the next value /// let recv_future = receiver.next(); /// // Wrap it a cooperative wrapper /// let coop_future = cooperative(recv_future); /// // And await /// coop_future.await /// } #[inline] pubfn cooperative<F: Future>(fut: F) -> Coop<F> {
Coop { fut }
}
}
#[cfg(all(test, not(loom)))] mod test { usesuper::*;
#[cfg(all(target_family = "wasm", not(target_os = "wasi")))] use wasm_bindgen_test::wasm_bindgen_test as test;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.