Hello all, fairly new to zig but have been playing about with sockets to get to know the new 0.16 Io API. The little project I’m working on is based on ZMQ, where I want to see if I can abstract a lot of the high performance networking / threading tricks behind the Io API. The dream here would be that I can just describe the protocol and how the sockets should work and tasks should run, and the choice of Io API lets the user make decisions about performance tradeoffs.
There are a few specific use cases I’m finding hard to express in the current API, and I wondered if people might have any thoughts on how to express these.
Expressing task dependencies
A common case I’ve been running into is I want to express something like “do A, then do B after A, then do C after B, then at some point later, wait for all of this…”. Say for example if I’m queueing sends on a socket, I want to ensure that send B happens after send A, rather than calling both with io.async as they may run out-of-order.
Solutions for this I can think of are
- Create an
Io.Queueand a worker process that runs inio.concurrent. This is fine if the tasks you want to do are all pretty homogenous (so if I’m just sending different bodies this works), but does add overhead of then having to manage a long-lived concurrent job outside of this. If A, B and C are quite different, the worker can end up quite complex. Also, this never works in single threaded mode. - If you always want A, then B, then C, then just write that as large function
doAthenBthenCand.asyncthis. This is fine if you know you will always do all these tasks, but it could be that the choice of tasks is conditional on runtime inputs. - Write wrapper functions that are like
doBafterXand take a reference to a future to await before running. This creates a bit of crud where I need lots of small wrapper functions depending on if I think a function might be depended on, and starts to feel like function colouring.
What I feel like I end up wanting is something like
const fut = io.async(doA, .{});
const next_fut = io.asyncAfter(fut, doB);
But it may be that I’m missing some more obvious primitives for describing this.
Describing cases where I want exactly one operation to happen
I know Io.Select exists for creating a number of futures and getting results one at a time, and you can then discard the rest. This works great for something like DNS resolution where you don’t expect a side effect from making additional resolution queries.
But consider a case where you expect your work to have a side effect. For example, I have cases where I might have multiple Io.Queue (or say, socket) objects and I want to write my next piece of data to whichever socket is first available to writing. I can
queue.putwith a min size of 0. This does at least tell me to try the next one, but the checking loop has to resort to sleeping and trying again, or not blocking the thread and consistently trying.asynca number ofqueue.putOne, and cancel once the first succeeds - this has the side effect that whilst cancelling other tasks may alsoputOne, so not a correct program I think. But may be some tricks here with other threading primitives and cancellation regions.- I can probably ensure something happens exactly once with another mutex, but I have the (maybe wrong) impression that as a high level API with futures I don’t want to be reaching for mutex’s / thinking in threading primitives too often? But this may be a hangover from thinking of mutex’s as being part of a threading paradigm, rather than an Io primitive that may be provided by threading.
I think this one can also be an understanding gap for me. I feel like the method I want is something like putOneIfFirst which puts it on the queue if it’s the first put operation on the queue, and skips otherwise.
When viewed more from a syscall perspective, I understand that you if you make 3 async syscall writes you shouldn’t be surprised if more than 1 write occurs. But if all I want is a syscall to say “block until you can tell me which of these is writable” I think that kind of thing exists?
Use case: writing a performant network send buffer
So these both come up when thinking about this problem: I have an application that does something and wants to send data. My application in general is realtime and wants to send data as quickly as it can to the system. However, whilst waiting for a syscall to send, I would like to queue remaining data in the buffer, so that if the system is slow and I’m working quickly I can buffer writes in the next syscall.
So each call I want to make to a Io.Writer can be described as .writeMyMessage(), .maybeFlush() , where
writeMyMessage()may also result in Io if it’s larger than the buffer, and is not thread safe in the sense that it can’t run alongside a call towriteMyMessagewith a different message (because they share a write buffer).maybeFlush()then looks like: queue at most one syscall to flush this buffer
This is achievable now by creating a concurrent worker thread which does this and has to run in the background, and causing the application send to just put on a queue. But as noted above, this means the code doesn’t work in single-threaded mode - whereas it has a correct single-threaded program, which is to flush every message immediately.
I should prefix all of this with: I spend my day job writing high level code, and may perhaps just be too reluctant or unfamiliar to recognise that when programming at systems level you need to handle the gory details of managing more complex dependencies. But this has made me wonder if there could be scope for increasing the types of concurrent programs we can describe by slightly extending the available language in Io. Also, this may all already exist and I’m just missing something! Would love thoughts from those more experienced
(and also, if anyone out there knows Io really well and wants to implement TCP networking with URing on zig master I really want to try it out but am too dumb to implement it myself)