Updated description
The primary change made in this PR is to restructure memory management and
notifications throughout the "task system" in the futures
crate. It is
intended that this will have very little impact, if any, on consumers of
futures. Implementations of runtimes of futures (e.g. crates like tokio-core)
are the target for this series of changes, enabling a suite of possible
optimizations that were not previously feasible. One major use case that is
now enabled is usage of the task
and executor
modules in the no_std
ecosystem.
This means that bare-metal applications of futures should be able to use the same
task system that the std-based futures ecosystem uses.
One of the largest changes being made to support this is an update to the memory management of
objects behind Task
handles. Previously it was required that Arc<Unpark>
instances were passed into the various Spawn::poll_*
functions, but new
functions Spawn::poll_*_notify
were added which operate with a NotifyHandle
instead. A NotifyHandle
is conceptually very similar to an Arc<Unpark>
instance, but it works through an unsafe trait UnsafeNotify
to manage memory
instead of requiring Arc
is used. You can still use Arc
safely, however, if
you'd like.
In addition to supporting more forms of memory management, the poll_*_notify
functions also take a new id
parameter. This parameter is intended to be an
opaque bag-of-bits to the futures
crate itself but runtimes can use this to
identify the future being notified. This is intended to enable situations where
the same instance of a NotifyHandle
can be used for all futures executed by
using the id
field to distinguish which future is ready when it gets
notified.
API Additions
- A
FuturesUnordered::push
method was added and the FuturesUnordered
type
itself was completely rewritten to efficiently track a large number of
futures.
- A
Task::will_notify_current
method was added with a slightly different
implementation than Task::is_current
but with stronger guarantees and
documentation wording about its purpose.
Compatibility Notes
As with all 0.1.x releases this PR is intended to be 100% backwards compatible.
All code that previously compiled should continue to do so with these changes.
As with other changes, though, there are also some updates to be aware of:
- The
task::park
function has been renamed to task::current
.
- The
Task::unpark
function has been renamed to Task::notify
, and in general
terminology around "unpark" has shifted to terminology around "notify"
- The
Unpark
trait has been deprecated in favor of the Notify
trait
mentioned above.
- The
UnparkEvent
structure has been deprecated. It currently should perform
the same as it used to, but it's planned that in a future 0.1.x release the
performance will regress for crates that have not transitioned away. The
primary primitive to replace this is the addition of a push
function on the
FuturesUnordered
type. If this does not help implement your use case though,
please let us know!
- The
Task::is_current
method is now deprecated, and you likely want to use
Task::will_notify_current
instead, but let us know if this doesn't suffice!
Original description
This PR is a work in progress
I'm submitting this PR early to hopefully try to illustrate my thoughts and get some early feedback.
Checklist
- [x] Land #442
- [x] Update tokio-core
- [x] Run sscache tests using new task system
- [x] Switch
Arc<Unpark>
to UnparkHandle
#432
- [x] Decide on #312 (leaning towards yes)
- [x] Allow executors to customize wait behavior #360 (deferring this until later)
- [x] Fix
Task::is_current
- [x] Remove
Notify::is_current
, I don't think this is needed anymore.
- [x] Consider
GetNotifyHandle
https://github.com/alexcrichton/futures-rs/issues/129
- [x] Should
ref_inc
-> ref_dec
be moved to UnsafeNotify
(@alexcrichton says no).
- [x] Consider getting rid of
poll_*_notify
on Stream
and Sink
. Also, maybe name it poll_notify
if it is only for Future
.
- [x] Merge https://github.com/carllerche/futures-rs/pull/4
- [x]
u64
vs. usize
Overview
The previous implementation of the task system required a number of allocations per task instance. Each task required a dedicated Arc<Unpark>
handle which means that executors require at least two allocations per task.
Things get worse when using with_unpark_event
as nested calls to with_unpark_event
result in Vec allocation and cloning during each call to task::park
.
This commit provides an overhaul to the task system to work around these problems. The Unpark
trait is changed so that only one instance is required per executor. In order to identify which task is being unparked, Unpark::unpark
takes an unpark_id: u64
argument.
with_unpark_event
is removed in favor of UnparkContext
which satisfies a similar end goal, but requires only a single allocation per lifetime of the UnparkContext
.
The new Unpark
trait
In general, tasks are driven to completion by executors and executors are able to handle large number of tasks. As such, the Unpark
trait has been tweaked to require a single allocation per executor instead of one per task. The idea is that the executor creates one Arc<Unpark>
handle and uses the unpark_id: u64
to identify which task is unparked.
In the case of tokio-core, each task is stored in a slab and the the unpark_id
is the slab index. Now, given that an Arc is no longer used, it can be that a slab slot is released and repurposed for a different task while there are still outstanding Task handles referencing the now released task.
There are two potential ways to deal with this.
a) Not care. Futures need to be able to handle spurious wake ups already. Spurious wake ups can be reduced by splitting the u64
into 28 bits for the slab offset and use the rest of the u64 as a slot usage counter.
b) Use Unpark::ref_inc
and Unpark::ref_dec
to allow the executor implementation to handle its own reference counting.
Option b) would allow an executor implementation to store a pointer as the unpark_id
and the ref_inc
and ref_dec
allow for atomic reference counting. This could be used in cases where using a slab is not an option.
UnparkContext
This is quite similar in spirit to with_unpark_event
except it requires significantly less allocations when used in a nested situation.
It does have some different behavior. Given the following:
// Currently in task A
let my_context = UnparkContext::new(my_unpark);
my_context.with(0, || {
let task = task::park();
thread::spawn(move || {
thread::sleep(a_few_secs);
task.unpark();
});
});
my_executor.spawn(move || {
// Currently in task B
my_context.with(0, || {
// some work here
});
});
Task B will be the root task that is notified.