Skip to content

Commit 960fc7a

Browse files
authored
Update asyncio module for task-like execution (#6)
Add top-level run for coroutine kick-off. Add asyncio.xawait for parallel execution. Store everything but the return value in the coro stack. Ensure coroutines have a stable parent that's yielded to on return. Remove unnecessary constCast in coro storage. Clean up aio tests. Update readme
1 parent abbe7b3 commit 960fc7a

8 files changed

Lines changed: 734 additions & 579 deletions

File tree

README.md

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,7 @@
22

33
Async Zig as a library using stackful asymmetric coroutines.
44

5-
* Stackful: each coroutine has an explicitly allocated stack and
6-
suspends/yields preserve the entire call stack of the coroutine. An
7-
ergonomic "stackless" implementation would require language support and
8-
that's what we expect to see with Zig's async functionality.
9-
* Asymmetric: coroutines are nested such that there is a "caller"/"callee"
10-
relationship, starting with a root coroutine per thread. The caller coroutine
11-
is the parent such that upon completion of the callee (the child coroutine),
12-
control will transfer to the caller. Intermediate yields/suspends transfer
13-
control to the last resuming coroutine.
14-
15-
Async IO is provided by [`libxev`][libxev].
5+
Supports async IO via [`libxev`][libxev].
166

177
---
188

@@ -23,18 +13,15 @@ supports {Linux, Mac} `aarch64`.*
2313

2414
## Current status
2515

26-
*Updated 2023/09/06*
16+
*Updated 2023/09/08*
2717

2818
Alpha, WIP.
2919

30-
Further exploring (structured) concurrency and cooperative multitasking atop
31-
`libxev` using coroutines.
20+
Currently fleshing out async io atop `libxev`. See [TODOs](#TODO) for current work.
3221

3322
## Coroutine API
3423

3524
```
36-
stackAlloc(allocator, size)->[]u8
37-
remainingStackSize()->usize
3825
xcurrent()->*Coro
3926
xcurrentStorage(T)->*T
4027
xresume(*coro)
@@ -43,28 +30,38 @@ Coro
4330
init(*func, *stack, ?*storage)
4431
getStorage(T)
4532
CoroFunc(Fn)
46-
init(.{args})
47-
initPtr(&fn, .{args})
48-
coro(*stack)->Coro
49-
xresumeStart()->YieldT
50-
xresume(inject)->YieldT
51-
xresumeEnd(inject)->ReturnT
33+
init()
34+
coro(args, stack)->Coro
35+
coroPtr(func, args, stack)->Coro
36+
xnextStart(coro)->YieldT
37+
xnext(coro, inject)->YieldT
38+
xnextEnd(coro, inject)->ReturnT
5239
xyield(yield)->InjectT
53-
StackCoro
54-
init(*func, .{args}, *stack)
55-
frame(*func, coro)->CoroFunc(Fn)
40+
xreturned(coro)->ReturnT
41+
42+
# Stack utilities
43+
stackAlloc(allocator, size)->[]u8
44+
remainingStackSize()->usize
5645
```
5746

5847
## Async IO API
5948

60-
`libcoro.xev.aio` provides coroutine-friendly wrappers to all the [high-level
61-
async APIs][libxev-watchers] in [`libxev`][libxev].
49+
[`libcoro.asyncio`][aio] provides coroutine-based async IO functionality
50+
building upon the evented IO system of [`libxev`][libxev]. It provides
51+
coroutine-friendly wrappers to all the [high-level async
52+
APIs][libxev-watchers] in [`libxev`][libxev].
6253

63-
See
64-
[`aio_test.zig`](https://github.com/rsepassi/zigcoro/blob/main/aio_test.zig)
65-
for usage examples.
54+
See [`test_aio.zig`][test-aio] for usage examples.
6655

6756
```
57+
# Run top-level coroutines in the event loop
58+
run
59+
runCoro
60+
61+
# Concurrently run N coroutines and wait for all to complete
62+
xawait
63+
64+
# IO
6865
sleep
6966
TCP
7067
accept
@@ -89,17 +86,11 @@ Async
8986
wait
9087
```
9188

92-
Stackful asymmetric coroutines provide a clean way of wrapping up async IO
93-
functionality, providing a programming model akin to threads (where the lightweight
94-
versions are variously called Coroutines, Green Threads, or Fibers). Calls to
95-
IO functionality are blocking from the perspective of the coroutine, but many
96-
coroutines can be running on the same thread.
89+
The IO functions are run from within a coroutine and appear as blocking, but
90+
internally they suspend so that other coroutines can progress.
9791

98-
Under the hood, what's required is async IO functionality, such that a coroutine
99-
can submit work to be done, suspend, and then be resumed when the work is complete.
100-
Libraries like [libuv][libuv] and [libxev][libxev] provide cross-platform async IO,
101-
and this is a new Zig project, so why not depend on another new Zig project like
102-
libxev?
92+
To run several coroutines concurrently, create the coroutines and pass them
93+
to `asyncio.xawait`.
10394

10495
## Depend
10596

@@ -116,15 +107,6 @@ const libcoro = b.dependency("zigcoro", .{}).module("libcoro");
116107
my_lib.addModule("libcoro", libcoro);
117108
```
118109

119-
## Coroutine Examples
120-
121-
*TODO: Fill back in*
122-
123-
* resume, suspend
124-
* storage
125-
* args, return
126-
* yield, inject
127-
128110
## Performance
129111

130112
I've done some simple benchmarking on the cost of context switching and on
@@ -216,17 +198,38 @@ ns/ctxswitch: 233
216198
...
217199
```
218200

201+
## Stackful asymmetric coroutines
202+
203+
* Stackful: each coroutine has an explicitly allocated stack and
204+
suspends/yields preserve the entire call stack of the coroutine. An
205+
ergonomic "stackless" implementation would require language support and
206+
that's what we expect to see with Zig's async functionality.
207+
* Asymmetric: coroutines are nested such that there is a "caller"/"callee"
208+
relationship, starting with a root coroutine per thread. The caller coroutine
209+
is the parent such that upon completion of the callee (the child coroutine),
210+
control will transfer to the caller. Intermediate yields/suspends transfer
211+
control to the last resuming coroutine.
212+
213+
The wonderful 2009 paper ["Revisiting Coroutines"][coropaper] describes the
214+
power of stackful asymmetric coroutines in particular and their various
215+
applications, including nonblocking IO.
216+
219217
## Future work
220218

221219
Contributions welcome.
222220

221+
* Multi-threading support
222+
* Simple coro stack allocator, reusing stacks
223223
* Libraries
224-
* (WIP) Task library: schedulers, futures, cancellation
224+
* TLS, HTTP, WebSocket
225+
* Actors
225226
* Recursive data structure iterators
226227
* Parsers
228+
* Alternative async IO loops (e.g. libuv)
227229
* Debugging
228230
* Coro names
229231
* Tracing tools
232+
* Dependency graphs
230233
* Detect incomplete coroutines
231234
* ASAN, TSAN, Valgrind support
232235
* Make it so that it's as easy as possible to switch to Zig's async when it's
@@ -240,7 +243,11 @@ Contributions welcome.
240243

241244
### TODO
242245

243-
* Revisit CoroFunc state machine:
246+
* Concurrent execution with async/await-like semantics and helpers
247+
(waitAll, waitFirst, asReady, ...).
248+
* Cancellation and timeouts
249+
* Async iterators
250+
* Better coroutine error propagation
244251
* If a coroutine errors, it will be in the Done state and retval will be set
245252
to the error. The caller in that situation will have to test whether the
246253
coro is Done to call xreturned instead of xnext. The YieldT should probably
@@ -249,7 +256,8 @@ Contributions welcome.
249256
## Inspirations
250257

251258
* ["Revisiting Coroutines"][coropaper] by de Moura & Ierusalimschy
252-
* [Lua coroutines](https://www.lua.org/pil/9.1.html)
259+
* [Lua coroutines][lua-coro]
260+
* ["Structured Concurrency"][struccon] by Eric Niebler
253261
* https://github.com/edubart/minicoro
254262
* https://github.com/kurocha/coroutine
255263
* https://github.com/kprotty/zefi
@@ -260,3 +268,7 @@ Contributions welcome.
260268
[libxev]: https://github.com/mitchellh/libxev
261269
[libxev-watchers]: https://github.com/mitchellh/libxev/tree/main/src/watcher
262270
[libuv]: https://libuv.org
271+
[struccon]: https://ericniebler.com/2020/11/08/structured-concurrency
272+
[aio]: https://github.com/rsepassi/zigcoro/blob/main/src/asyncio.zig
273+
[test-aio]: https://github.com/rsepassi/zigcoro/blob/main/src/test_aio.zig
274+
[lua-coro]: https://www.lua.org/pil/9.1.html

benchmark.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ fn contextSwitchBm() !void {
2828
{
2929
var test_coro = try libcoro.Coro.init(testFn, stack, null);
3030
for (0..num_bounces) |_| {
31-
libcoro.xresume(&test_coro);
31+
libcoro.xresume(test_coro);
3232
}
33-
libcoro.xresume(&test_coro);
33+
libcoro.xresume(test_coro);
3434
}
3535

3636
num_bounces = 20_000_000;
@@ -39,14 +39,14 @@ fn contextSwitchBm() !void {
3939

4040
const start = std.time.nanoTimestamp();
4141
for (0..num_bounces) |_| {
42-
libcoro.xresume(&test_coro);
42+
libcoro.xresume(test_coro);
4343
}
4444
const end = std.time.nanoTimestamp();
4545
const duration = end - start;
4646
const ns_per_bounce = @divFloor(duration, num_bounces * 2);
4747
std.debug.print("ns/ctxswitch: {d}\n", .{ns_per_bounce});
4848

49-
libcoro.xresume(&test_coro);
49+
libcoro.xresume(test_coro);
5050
}
5151
}
5252
}
@@ -113,7 +113,7 @@ fn ncorosBm(num_coros: usize) !void {
113113
std.debug.print("Running {d} coroutines for {d} rounds\n", .{ num_coros, rounds });
114114

115115
// number of coroutines benchmark
116-
var coros = try allocator.alloc(libcoro.Coro, num_coros);
116+
var coros = try allocator.alloc(*libcoro.Coro, num_coros);
117117
defer allocator.free(coros);
118118

119119
var buf = try allocator.alloc(u8, num_coros * 1024 * 4);
@@ -130,7 +130,7 @@ fn ncorosBm(num_coros: usize) !void {
130130

131131
var start = std.time.nanoTimestamp();
132132
for (0..rounds) |i| {
133-
for (coros) |*coro| {
133+
for (coros) |coro| {
134134
libcoro.xresume(coro);
135135
}
136136
if ((i + 1) % batching == 0) {

build.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub fn build(b: *std.Build) !void {
88

99
// Module
1010
const coro = b.addModule("libcoro", .{
11-
.source_file = .{ .path = "src/coro.zig" },
11+
.source_file = .{ .path = "src/main.zig" },
1212
.dependencies = &[_]std.Build.ModuleDependency{
1313
.{ .name = "xev", .module = xev },
1414
},

0 commit comments

Comments
 (0)