A little help with `Io` please. I am trying to understand how to use the new `Io` interface for Zig

You are mistaken, io.async indeed can block execution, that’s the main purpose. There is no guarantee the function called from io.async is going to run concurrently. You need to use io.concurrent there.

However, for long sleeps, especially for something lie sleeping for half a day until certain time, you definitely don’t want an idle thread just for that.

1 Like

Welcome to the forum @colorpalette!

Let’s make that feedback a little bit more useful, shall we?

Your comment that says it will wait three seconds: that depends on the Io implementation which is used. It’s allowed to block, in which case, it would be four seconds.

Concurrent would cause a runtime error if the two sleeps cannot be run in parallel. Don’t use it unless the code requires that property.

@lalinsky Thanks for pointing that out, I neglected to make the distinction between functions which yield execution and functions which block. I’ve updated the comment now, can probably be worded more accurately and have better and more detailed examples.

@mnemnion Thanks, I’ve been lurking for quite some time and finally signed up. You’re right about being specific about the Io implementation and the different behaviors you can get from the same code depending on which one is used. I was exploring with a standard Threaded io with multiple cores and jumped to conclusions too quickly. I think the current example is useful for a javascript developer to get an initial idea of the parallels between js and zig’s asynchrony, but I definitely need to update this demo to make it more correct.

1 Like

The key part is that you should use io.concurrent in the example. Then it would be correct. As is, the behavior depends on a chance.

Just thinking out loud: I’m trying to think of what situations that would be useful (outside of accepting connections). E.g., at least in business logic style work, if you have tasks A, B, and C, it is optimal is A, B, and C run concurrently, but it’s not strictly a requirement. It just needs to be done. So you’d just use io.async, I presume. Similarly, even in more complex use cases where you have some degree of enforcement on ordering (e.g. a DAG), once again you would use io.async as, you only need to block execution until all ancestors complete, which can be accomplished in order as opposed to in parallel. E.g. in the Python context

import graphlib
import asyncio

sorter = graphlib.TopologicalSorter(dag)
in_flight = {}
while sorter.is_active():
    for node in sorter.get_ready():
        task = asyncio.create_task(some_f, node)
        in_flight[task] = node

    done_or_cancelled, not_done = asyncio.wait(in_flight.keys(), timeout=10, return_when=asyncio.FIRST_COMPLETED)
    for task in done_or_cancelled:
        if task.cancelled():
            pass # do whatever reconciliation is needed
        else:
            node = in_flight.pop(task)
            sorter.done(node)

Summary being, it seems io.concurrent is very specific.

Anything that deals with timing can’t realistically use io.async, because you don’t now the final sleep time.

Another class of problems is using Io.Queue or similar synchronization mechanisms. If you want to call a function that puts items to the queue and you process them after that, you need to call the function using io.concurrent, using io.async might deadlock the program. This similar to your A, B, C example. As soon as you have any kind of dependency between them, running them using io.async is dangerous.

For me, io.async is basically the tool to use when I have linear blocking code, and part of the could benefit from being concurrent, but it was linear blocking code to begin with.

2 Likes

Isn’t that where timeouts come in for tasks? In the A, B, C example they’re all independent, hence, say your underlying IO is single-threaded, so long as you have timeouts, once A exceeds its timeout, you move onto B, etc. etc.

Same in the DAG example, if you reach a point where there are no more processable nodes and those next in line have parents in error, you know to quit at that point and propagate the collection of errors to the user along with their DAG state.

My only real point was, there’s: “it would be nice if this runs concurrently” (i.e. business logic stuff where you make HTTP requests) vs. “this needs to be concurrent” (i.e. your server has to be running and accepting connections). io.concurrent seems specific for the latter.

Timeout is a good example, std.Io doesn’t have timeouts for most operations, so the way you would do that is using io.concurrent for a sleep, io.concurrent for the task and then io.select on them. If you used io.async instead, you would end up in a situation where the sleep could run sequentially before or after the task.

2 Likes

This is how I’ve always thought about async/await, language-independent. Notably: there’s a certain independence between the tasks that “could be run linearly… but could benefit from coincidental asynchrony”. No interdependence - that gets dangerous, and is not a good fit. io.concurrent is the choice if you need concurrency.