-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy path__init__.py
86 lines (71 loc) · 4.13 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"""Concurrency based on `shift`/`reset` and object-oriented effect handlers
We implement a portable `shift`/`reset`[0] for async Python, which works when running
under any supported async runner. We also build several concurrent communication
mechanisms with `shift`/`reset`.
Delimited continuations, such as `shift`/`reset`, can be used to transform a
callback-based concurrency system into a more ergonomic direct-style system.[1]
For example, we can go from:
```
def cb(data):
if data:
more_work(data)
file.read_cb(cb)
```
to:
```
data = await shift(file.read_cb)
if data:
more_work(data)
```
We can then construct an ergonomic direct-style concurrency system by layering
delimited continuations on top of a traditional callback-based concurrency
system. This has a number of advantages.
Instead of a coroutine performing a blocking operation by yielding up to a
global event loop, a coroutine performs a blocking operation by calling `shift`
with any object that accepts a callback. Context switching between running
coroutines happens automatically, in a distributed fashion, as objects receive
events and call callbacks. Instead of relying implicitly on a global event loop
and global scheduler, a coroutine explicitly selects what object is responsible
for scheduling it, simply by making a call into that object. That object may
perform its duties by calling into other objects or by performing blocking
system calls itself, and ultimately resumes the coroutine with the result by
calling the callback.
With callback-based concurrency, an object can guarantee that when callbacks are
registered in a certain order, the underlying operations are performed in that
order; and that when events happen in some order, the registered callbacks are
called in that same order. This preserves information about event ordering and
allows for much simpler bookkeeping of state. This guarantee is preserved when
using `shift`/`reset`.
For example, suppose some underlying bit of state is either "True" or "False",
and we read that state on each related operation, and maintain a Python boolean
which is supposed to match the underlying state. If callbacks are called in
order, we know that the underlying state is always equal to the state at the
time of the last callback, so we can just set the tracking bool to the bit we
read. Thus, we can safely perform multiple operations at a time, in parallel,
from unrelated coroutines, with no explicit sequencing in user code. Without
this guarantee, we would have to explicitly track the order in which operations
are performed, or, more easily, lock the underlying state so that we only
perform one operation at a time, reducing parallelism.
Our object-oriented style also gives us effect handlers which are not based on a
stack discipline. We don't need to be concerned about computations being
performed in or out of the scope of handlers. Effect handlers are simply
regular garbage collected objects: An effect handler stays alive as long as
there is something with a reference to it.
We also get an object-capability-style effect system for free. Since we don't
rely on an implicit global event loop, there are no implicit global asynchronous
effects. To allow a function to perform an asynchronous effect, we pass it (as
a normal argument) the object that implements that effect. The type of an
effectful function is simply a regular function type, accepting one or more
objects with which it can perform effects.[2]
[0]: If you aren't familiar with delimited continuations or shift/reset,
read this tutorial:
http://pllab.is.ocha.ac.jp/~asai/cw2011tutorial/main-e.pdf
[1]: Using delimited continuations to pass callbacks is a well known technique;
here's one post about it:
http://www.gregrosenblatt.com/writing/reinvert-control-delim-cont.html
[2]: Since we can close over effect handlers (they're regular objects), a single
object or function might abstract over multiple effects. Is this a bad thing?
Some say yes, but it seems like a pretty normal form of abstraction to me.
"""
from dneio.core import shift, reset, Continuation, is_running_directly_under_trio
from dneio.concur import RequestQueue, Event, Future, make_n_in_parallel, run_all