-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async/await/suspend/resume #6025
Comments
I'm willing to try implementing async/await/suspend/resume for stage2 as i require them for a project i'm working on. The issue is that i dont really know where to start. The AIR only have async_call, async_call_alloc, suspend_begin, and suspend_end instructions. By looking at stage1 it seems like the await/suspend/resume instructions are missing. Should I try to just add the instructions and replace calls to Futhermore, is async implemented in a similar way in stage2 as stage1? (basically stage1 being a good representation of how stage2 implements and uses frames, async calls, suspends, resumes, etc) Edit: I've been using the stage2-async branch, assuming that's where the async development is being done. |
This commit removes async/await/suspend/resume from the language reference, as that feature does not yet work in the self-hosted compiler. We will be regressing this feature temporarily. Users of these language features should stick with 0.10.x with the `-fstage1` flag until they are restored. See tracking issue #6025.
I've been following the WASI development and it seems to be going great! That being said, I am currently working on a new project and I am using some specific stage2 features. I am not using async yet, but I'd love to introduce it soon. Can you provide a very rough estimate of when this is planned to be merged in master? It is just for general planning (no pressure). Cheers! |
*Async JS* For now only callback style is handled (Promises is planned later). We use persistent handle on v8 JS callback call after retrieving the event from the kernel, has the parent JS function is finished and therefore local handles are already garbage collected by v8. * Event Loop* We do not use the event loop provided in Zig stdlib but instead Tigerbeetle IO (https://github.com/tigerbeetledb/tigerbeetle/tree/main/src/io). The main reason is to have a strictly single-threaded event loop, see ziglang/zig#1908. In addition the desing of Tigerbeetle IO based on io_uring (for Linux, with wrapper around kqueue for MacOS), seems to be the right direction for IO. Our loop provides callback style native APIs. Async/await style native API are not planned until zig self-hosted compiler (stage2) support concurrent features (see ziglang/zig#6025). Signed-off-by: Francis Bouvier <[email protected]>
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * 'cases/compile_errors/async/*' and all remaining 'safety/*' depend on async; see ziglang#6025. Resolves: ziglang#14849
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * `cases/compile_errors/async/*` and all remaining `safety/*` depend on async; see ziglang#6025. Resolves: ziglang#14849
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * `cases/compile_errors/async/*` and all remaining `safety/*` depend on async; see ziglang#6025. Resolves: ziglang#14849
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * `cases/compile_errors/async/*` and all remaining `safety/*` depend on async; see ziglang#6025. Resolves: ziglang#14849
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * `cases/compile_errors/async/*` and all remaining `safety/*` depend on async; see #6025. Resolves: #14849
There are now very few stage1 cases remaining: * `cases/compile_errors/stage1/obj/*` currently don't work correctly on stage2. There are 6 of these, and most of them are probably fairly simple to fix. * `cases/compile_errors/async/*` and all remaining `safety/*` depend on async; see ziglang#6025. Resolves: ziglang#14849
I really like the var some_value = (await (await async_func()).some_other_thing()).finally() var some_value = async_func().await.some_other_thing().await.finally() Would it be possible to adopt this kind of syntax? |
The original async syntax zig had would not be affected by this, as explained in this blog post from 2020: https://kristoff.it/blog/zig-colorblind-async-await/ Don't know if the syntax idea has changed, but I really liked that Zig just flipped the async/await function call usage, so that for the common case of calling a non-async function and an async function would have the same syntax. const some_value = async_func().some_other_thing().finally()
// Equivalent to:
const frame = async async_func()
const other_frame = async (await frame).some_other_thing()
const some_value = (async other_frame).finally()
// Equivalent to:
const some_value = (await async (await async async_func()).some_other_thing()).finally() Though, in this reversed case where you want to grab the async frame, maybe then it could be a field named const frame = async_func.async()
const other_frame = frame.await().some_other_thing.async()
const some_value = other_frame.await().finally()
// Equivalent to:
const some_value = async_func.async().await().some_other_thing.async().await().finally() |
That syntax has not changed, and (if async is re-implemented) will not change, because it's required for colorless async. So, yes, we don't need |
I think you could make const asyncfn = std.async(syncfn);
const result = asyncfn() |
Good day, any ETA for this? |
This comment was marked as off-topic.
This comment was marked as off-topic.
Please don't add noise like this to the issue tracker.
If you can provide a particular reason you think Zig should retain async functionalities (or not) -- especially a concrete use case -- then feel free to give it. Otherwise, rest assured that the core team will get to this issue with time. |
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as off-topic.
This comment was marked as off-topic.
Hello everyone, I have a problem, I don't know if it is suitable for this topic. |
@JiaJiaJiang I am not sure if this will fix your issue. But I hacked something that can turn async JS calls into sync zig calls. On zig side, have something like this: extern fn send_recv(
buf: [*]const u8,
buf_len: usize,
result: [*]u8,
result_len: *usize,
) u8; then, on the JS side (in your WASM thread that you spawn in a web worker), bind a function like this one: return function (buf_ptr, buf_len, result_ptr, result_len_ptr) {
// instance is created with something like this WebAssembly.instantiate(...).instance
const mem = get_memory_buffer() // return instance.exports.memory.buffer
const view = get_memory_view() // return new DataView(instance.exports.memory.buffer)
const ctx = get_shared_context() //see below
const data = new Uint8Array(mem, buf_ptr, buf_len)
ctx.lock()
ctx.write(data)
ctx.client_notify()
ctx.unlock()
ctx.wait_for_server()
ctx.lock()
const result = ctx.read()
ctx.unlock()
const result_len = view.getUint32(result_len_ptr, true)
if (result.length === 0) {
return 1// error codes for zig
}
if (result.length > result_len) {
return 2 // error codes for zig
}
view.setUint32(result_len_ptr, result.length, true)
const dest = new Uint8Array(mem, result_ptr, result.length)
dest.set(result)
return 0// error codes for zig
} In another web worker, do something like this: const step = async function () {
const ctx = get_shared_context() // send the same context to both workers
if (ctx.wait_for_client(10) !== true) {
step()
return
}
ctx.lock()
const request = ctx.read()
// process request.buffer, you can pass JSON commands, function names... encode it the way you like
const response = await whatever_process_request(request) // this is where the magic happens as it turns an async call to a sync call
ctx.write(new Uint8Array(response))
ctx.server_notify()
ctx.unlock()
step()
}
step() // this starts an infinite loop A shared context is something I threw together to allow to sync two thread export default function SharedContext(buffer) {
if (!buffer) {
throw new Error("Buffer must be a shared buffer")
}
const META = new Int32Array(buffer, 0, 4)
const LOCK = 0
const CLIENT_NOTIFY = 1
const SERVER_NOTIFY = 2
const BUF_LEN = 3
// LOCK values
const UNLOCKED = 0
const LOCKED = 1
// NOTIFY values
const OFF = 0
const ON = 1
const DATA = new Uint8Array(buffer, 16) // start at offset 16
function write(buf) {
if (buf.length > DATA.length) {
return 1
}
DATA.set(buf, 0)
Atomics.store(META, BUF_LEN, buf.length)
return 0
}
function writeU32(n) {
const buf = new Uint8Array(4)
new DataView(buf).setUint32(n, true)
return write(buf)
}
function lock() {
while (true) {
Atomics.wait(META, LOCK, LOCKED)
if (
Atomics.compareExchange(META, LOCK, UNLOCKED, LOCKED) ===
UNLOCKED
) {
Atomics.notify(META, LOCK)
break
}
}
}
function unlock() {
Atomics.store(META, LOCK, UNLOCKED)
Atomics.notify(META, LOCK)
}
function read() {
const len = Atomics.load(META, BUF_LEN)
return DATA.slice(0, len)
}
function readU32(n) {
const buf = read()
return new DataView(buf).getUint32(true)
}
function client_notify() {
Atomics.store(META, CLIENT_NOTIFY, ON)
Atomics.notify(META, CLIENT_NOTIFY)
}
function server_notify() {
Atomics.store(META, SERVER_NOTIFY, ON)
Atomics.notify(META, SERVER_NOTIFY)
}
function wait_for_client(timeout) {
if (Atomics.wait(META, CLIENT_NOTIFY, OFF, timeout) === "timed-out") {
return false
}
Atomics.store(META, CLIENT_NOTIFY, OFF)
return true
}
function wait_for_server(timeout) {
if (Atomics.wait(META, SERVER_NOTIFY, OFF, timeout) === "timed-out") {
return false
}
Atomics.store(META, SERVER_NOTIFY, OFF)
return true
}
return {
buffer,
lock,
unlock,
write,
read,
client_notify,
server_notify,
wait_for_client,
wait_for_server,
}
} Create it like this: This is something I threw together to unblock my project, I didn't analyze the performances but it works well enough. |
@kuon Thank you for your reply. |
Please have this discussion in a Zig community instead. This issue exists to track the implementation of Zig's async/await feature in the self-hosted compiler. The issue tracker isn't for questions. |
@mlugg I disagree that this discussion is not relevant to this thread. I think it provides good insights on real world usage and can help prioritize this issue and decide how it should be implemented. I use zig in a fairly large and popular app through WASM, and I was able to workaround the missing Deciding if With that said, I agree that the issue tracker should not be used for a ping/pong kind of discussion as the essence of the issue can be highly diluted and I am sorry if my participation made it go that way. |
I'm sorry, I'm not an expert in asynchronous programming, but tell me why it's impossible to add runtime like golang with green threads when say |
i intended to propose this a while back but was discouraged because it seemed fundamentally incompatible with what everyone else wants. i think any sensible proposal would need to have a good answer how this works with javascript. the current answer is that LLVM cororutines work, so that's just the path of least resistance. |
two colors function:), will this problem force all library consumers to use async + await? |
Hello everyone, I don't know whether this is helpful (I should get more into community in general), but here are my ideas I had for a kernel project (I don't know whether this is standard or whether it does work at all in practice, but I can't see the problem right now; however, JS async is a mystery to me, but if it introduces function colours, is it realistic it does not in zig when they should be compatible? That's what I grasp from some of the early discussion here.): The idea is essentially that async functions work like this: There is no difference between async and non-async functions, but there are some functions dedicated for dealing with call stacks: allocate, prepare, resume, await (note: no suspend). In the design, the most important part is
This means, one will always need a stack to jump into. When wanting to call an “async” function, after allocating a stack, one would push all registers (prepare the stack) so that A modification to the above resume function to support functions actually terminating: When the function being resumed into has already terminated, the stack will simply not get swapped at all (resume will do nothing). It would be enough to save in the returned stack pointer, as that would be reentry point. (Note: It is undefined behaviour to use an old pointer to a stack - this could also be changed by reserving the uppermost (my stack grows downward - perhaps too much x86) pointer on the stack for the real stack address and then dereferencing once more - but this undefined behaviour is comparable to use after free, in my imagination).
So, in short, the following are the problems I can see:
But it would also open up new possibilities, the following being the main possibilities I can think of:
|
I would like to suggest that the async functions support can live outside the language. There is no magic, and I don't think Zig Can do better than other languages with the limited async function contract. |
But I have figured out one more problem: async return needs a stack to continue on. Since asynchronous functions will always have a valid stack to return to, this could be saved somewhere on that stack, but I also have another, admittedly even stranger idea (other than separating asynchronous from synchronous functions): one could only allow noreturn functions as asynchronous functions and then let it be up to the code to implement async return and await. The disadvantage is that calling such a function gets more difficult, the advantage is that one could also implement Python like yield. But one point which will probably always separate synchronous from asynchronous functions is the fact that an asynchronous function will receive a stack it can resume into as implicit or explicit parameter. And I think it has to be built in into zig directly because writing that resume code into an asm block would technically be undefined behaviour (intentional failure to list all clobbers, if you will. An alternative would be to change the wording of the specification). So, do what you want with that idea, but these are my thoughts on this functionality. |
I'm pretty sure the entire point of Zig's idea of implementing async/await is that the colorness problem won't exist. |
Well, I have not figured out how to call async function in js, but I assume the colouring comes exactly from this that I simply can not figure this out. With the solution I wrote, it wold be clear how to call an asynchronous function from a synchronous one (create a stack and resume it as often as it is required for letting it terminate aka write the return value to a result location; yes, all of this can be done from a synchronous function). So you can clearly call an asynchronous function from a synchronous one, and if that is too much labour: write a library for it. But for the noreturn variant, there should be the possibility to clean up stuff somehow, but in principle, when you put the stuff needing to be cleaned up into its own scope, you can use defer, and if that’s not possible, then you have to give the cleanup information to the function also cleaning up the stack somehow (eg via shared state). For better support of zig, optimal would be to have the stack save a pointer to the cleanup function which, as part of stack deinit, would get called there. (Assuming this will be supported, although this would be a good reason against it) |
Hi @ThatDreche. I'm not familiar with Zig async and am mostly following this issue out of curiosity, but I am deeply familiar with effects handlers systems which offer answers to some of your questions. I think you would find the paper Lexical Effect Handlers, Directly particularly interesting.
If I understand what you mean by this, I think this would be a problem with the implementation of the scheduler, right?
This is generally a problem in the literature. Most implementations (incl. the one above) deal with this dynamically (and WasmFX traps) but it is also possible to solve this by a types system treating stacks as affine values (resumed no more than once). You could also keep it as undefined behavior for sure.
Yeah, debuggers for sure. If you provide little "bare" pieces of assembly as primitives for your stack switching operations like the paper linked above does, I think LLVM should be alright though.
You might be interested in looking into effects handlers more broadly, this is kind of their beauty - they allow for implementing all non-local control flow in the same system, in user code, providing compositional semantics for-free. Not sure if they're the right fit for Zig specifically. They're conceptually still a bit of a nightmare (because of how powerful they are), though languages like Effekt have been making strides on that front. |
I mean, with respect to LLVM coroutines, hasn't this already been discussed on this issue as to why they simply are insufficient? |
No, as the scheduler only does cooperative scheduling. This means the called function will resume the scheduler somewhen. It only means the scheduler itself could decide the function is not worth running any more and letting it be as it is.
Assuming I understand what you mean, this is not really a problem at all. Really, in my comments above, one could almost completely replace “problem” by “design decision which has to be done before implementing this”. In this case, it is the design decision on how to handle the fact that the used part of the stack may grow or shrink, so the bottom may be at different locations at different interrupts (or however this would be called in cooperative multitasking) and whether the current pointer should be managed by code or by the language. |
Use repassi/zigcoro library for the latest versions of Zig if anybody want to emulate the functionality till async/await is here in Zig. Its more than enough for now. |
This is a sub-task of #89.
The text was updated successfully, but these errors were encountered: