| ▲ | amluto 7 hours ago |
| I find this example quite interesting: var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
var b_future = io.async(saveFile, .{io, data, "saveB.txt"});
const a_result = a_future.await(io);
const b_result = b_future.await(io);
In Rust or Python, if you make a coroutine (by calling an async function, for example), then that coroutine will not generally be guaranteed to make progress unless someone is waiting for it (i.e. polling it as needed). In contrast, if you stick the coroutine in a task, the task gets scheduled by the runtime and makes progress when the runtime is able to schedule it. But creating a task is an explicit operation and can, if the programmer wants, be done in a structured way (often called “structured concurrency”) where tasks are never created outside of some scope that contains them.From this example, if the example allows the thing that is “io.async”ed to progress all by self, then I guess it’s creating a task that lives until it finishes or is cancelled by getting destroyed. This is certainly a valid design, but it’s not the direction that other languages seem to be choosing. |
|
| ▲ | throwawaymaths 37 minutes ago | parent | next [-] |
| is it not the case that in zig, the execution happens in a_future.await? I presume that: io.async 1 stores in io "hey please work on this" io.async 2 stores in io "hey also please work on this" in the case where io is evented with some "provided event loop": await #1 runs through both 1 and 2 interleavedly, and if 2 finishes before 1, it puts a pin on it, and then returns a_result when 1 is completed. await #2 "no-executions" if 1 finished after 2, but if there is still work to be done for 2, then it keeps going until the results for 2 are all in. There's no "task that's running somewere mysteriously" unless you pick threaded io, in which case, yeah, io.async actually kicks shit off, and if the cpu takes a big fat nap on the calling thread between the asyncs and the awaits, progress might have been made (which wouldn't be the case if you were evented). |
|
| ▲ | jayd16 7 hours ago | parent | prev | next [-] |
| C# works like this as well, no? In fact C# can (will?) run the async function on the calling thread until a yield is hit. |
| |
| ▲ | throwup238 6 hours ago | parent [-] | | So do Python and Javascript. I think most languages with async/await also support noop-ing the yield if the future is already resolved. It’s only when you create a new task/promise that stuff is guaranteed to get scheduled instead of possibly running immediately. | | |
| ▲ | amluto 5 hours ago | parent [-] | | I can't quite parse what you're saying. Python works like this: import asyncio
async def sleepy() -> None:
print('Sleepy started')
await asyncio.sleep(0.25)
print('Sleepy resumed once')
await asyncio.sleep(0.25)
print('Sleepy resumed and is done!')
async def main():
sleepy_future = sleepy()
print('Started a sleepy')
await asyncio.sleep(2)
print('Main woke back up. Time to await the sleepy.')
await sleepy_future
if __name__ == "__main__":
asyncio.run(main())
Running it does this: $ python3 ./silly_async.py
Started a sleepy
Main woke back up. Time to await the sleepy.
Sleepy started
Sleepy resumed once
Sleepy resumed and is done!
So there mere act of creating a coroutine does not cause the runtime to run it. But if you explicitly create a task, it does get run: import asyncio
async def sleepy() -> None:
print('Sleepy started')
await asyncio.sleep(0.25)
print('Sleepy resumed once')
await asyncio.sleep(0.25)
print('Sleepy resumed and is done!')
async def main():
sleepy_future = sleepy()
print('Started a sleepy')
sleepy_task = asyncio.create_task(sleepy_future)
print('The sleepy future is now in a task')
await asyncio.sleep(2)
print('Main woke back up. Time to await the task.')
await sleepy_task
if __name__ == "__main__":
asyncio.run(main())
$ python3 ./silly_async.py
Started a sleepy
The sleepy future is now in a task
Sleepy started
Sleepy resumed once
Sleepy resumed and is done!
Main woke back up. Time to await the task.
I personally like the behavior of coroutines not running unless you tell them to run -- it makes it easier to reason about what code runs when. But I do not particularly like the way that Python obscures the difference between a future-like thing that is a coroutine and a future-like thing that is a task. | | |
| ▲ | int_19h 37 minutes ago | parent | next [-] | | > I personally like the behavior of coroutines not running unless you tell them to run -- it makes it easier to reason about what code runs when. In .NET the difference was known as "hot" vs "cold" tasks. "Hot" tasks - which is what .NET does with C# async/await - have one advantage in that they get to run any code that validates the arguments right away and fail right there at the point of the call, which is easier to debug. But one can argue that such validation should properly be separate from function body in the first place - in DbC terms it's the contract of the function. | |
| ▲ | throwup238 an hour ago | parent | prev | next [-] | | That’s exactly the behavior I’m describing. `sleepy_future = sleepy()` creates the state machine without running anything, `create_task` actually schedules it to run via a queue, `asyncio.sleep` suspends the main task so that the newly scheduled task can run, and `await sleepy_task` either yields the main task until sleepy_task can finish, or no-ops immediately if it has already finished without yielding the main task. My original point is that last bit is a very common optimization in languages with async/await since if the future has already resolved, there’s no reason to suspend the current task and pay the switching overhead if the task isn’t blocked waiting for anything. | |
| ▲ | metaltyphoon 20 minutes ago | parent | prev [-] | | In C# that Task is ALWAYS hot, aka scheduled to run. |
|
|
|
|
| ▲ | nmilo 7 hours ago | parent | prev | next [-] |
| This is how JS works |
|
| ▲ | messe 7 hours ago | parent | prev [-] |
| It's not guaranteed in Zig either. Neither task future is guaranteed to do anything until .await(io) is called on it. Whether it starts immediately (possibly on the same thread), or queued on a thread pool, or yields to an event loop, is entirely dependent on the Io runtime the user chooses. |
| |
| ▲ | amluto 6 hours ago | parent [-] | | It’s not guaranteed, but, according to the article, that’s how it works in the Evented model: > When using an Io.Threaded instance, the async() function doesn't actually do anything asynchronously — it just runs the provided function right away. So, with that version of the interface, the function first saves file A and then file B. With an Io.Evented instance, the operations are actually asynchronous, and the program can save both files at once. Andrew Kelley’s blog (https://andrewkelley.me/post/zig-new-async-io-text-version.h...) discusses io.concurrent, which forces actual concurrency, and it’s distinctly non-structured. It even seems to require the caller to make sure that they don’t mess up and keep a task alive longer than whatever objects the task might reference: var producer_task = try io.concurrent(producer, .{
io, &queue, "never gonna give you up",
});
defer producer_task.cancel(io) catch {};
Having personally contemplated this design space a little bit, I think I like Zig’s approach a bit more than I like the corresponding ideas in C and C++, as Zig at least has defer and tries to be somewhat helpful in avoiding the really obvious screwups. But I think I prefer Rust’s approach or an actual GC/ref-counting system (Python, Go, JS, etc) even more: outside of toy examples, it’s fairly common for asynchronous operations to conceptually outlast single function calls, and it’s really really easy to fail to accurately analyze the lifetime of some object, and having the language prevent code from accessing something beyond its lifetime is very, very nice. Both the Rust approach of statically verifying the lifetime and the GC approach of automatically extending the lifetime mostly solve the problem.But this stuff is brand new in Zig, and I’ve never written Zig code at all, and maybe it will actually work very well. | | |
| ▲ | messe 6 hours ago | parent [-] | | Ah, I think we might have been talking over each other. I'm referring to the interface not guaranteeing anything, not the particular implementation. The Io interface itself doesn't guarantee that anything will have started until the call to await returns. |
|
|