Of course even if that exact queue is not itself selectable, you can still implement a Go channel with select capabilities in Zig. I'm sure one exists somewhere already. Go doesn't get access to any magic CPU opcodes that nobody else does. And languages (or libraries in languages where that is possible) can implement more capable "select" variants than Go ships with that can select on more types of things (although not necessarily for "free", depending on exactly what is involved). But it is more than a queue, which is also why Go channel operations are a bit to the expensive side, they're implementing more functionality than a simple queue.
select {
case result := <-resultChan:
// whatever
case <-cxt.Done():
// our context either timed out or was cancelled
}
or any more elaborate structure.Or, to put it a different way, when someone says "I implement Go channels in X Language" I don't look for whether they have a threaded queue but whether they have a select equivalent. Odds are that there's already a dozen "threaded queues" in X Language anyhow, but select is less common.
Again note the difference between the word "distinctive" and "unique". No individual feature of Go is unique, of course, because again, Go does not have special unique access to Go CPU opcodes that no one else can use. It's the more defining characteristic compared to the more mundane and normal threaded queue.
Of course you can implement this a number of ways. It is not equivalent to a naive condition wait, but probably with enough work you could implement them more or less with a condition, possibly with some additional compiler assistance to make it easier to use, since you'd need to be combining several together in some manner.
Thanks for giving me a reason to peek into how Zig does things now.
Zig has a generic select function[1] that works with futures. As is common, Blub's language feature is Zig's comptime function. Then the io implementation has a select function[2] that "Blocks until one of the futures from the list has a result ready, such that awaiting it will not block. Returns that index." and the generic select switches on that and returns the result. Details unclear tho.
[1] https://ziglang.org/documentation/master/std/#std.Io.select
[2] https://ziglang.org/documentation/master/std/#std.Io.VTable
Even better, how would I write my own `Future` in a way that supports this `select` and is compatible with any reasonable `Io` implementation?
Channel semantics don't match futures semantics. As the name implies, channels are streams, futures are a single future value that may or may not have resolved yet.
Again, I'm sure nothing stops Zig from implementing Go channels in half-a-dozen different ways, but it's definitely not as easy as "oh just wrap a future around the .get of a threaded queue".
By a similar argument it should be observed that channels don't naively implement futures either. It's fairly easy to make a future out of a channel and a couple of simple methods; I think I see about 1 library a month going by that "implements futures" in Go. But it's something that has to be done because channels aren't futures and futures aren't channels.
(Note that I'm not making any arguments about whether one or the other is better. I think such arguments are actually quite difficult because while both are quite different in practice, they also both fairly fully cover the solution space and it isn't clear to me there's globally an advantage to one or the other. But they are certainly different.)
In my mind a queue.getOne ~= a <- on a Go channel. Idk how you wrap the getOne call in a Future to hand it to Zig's select but that seems like it would be a straightforward pattern once this is all done.
I really do appreciate you being strict about the semantics. Tbh the biggest thing I feel fuzzy on in all this is how go/zig actually go about finding the first completed future in a select, but other than that am I missing something?
https://ziglang.org/documentation/master/std/#std.Io.Queue.g...
I think the big one is that a futures based system no matter how you swing it lacks the characteristic that on an unbuffered Go channel (which is the common case), successfully sending is also a guarantee that someone else has picked it up, and as such a send or receive event is also a guaranteed sync point. This requires some work in the compiler and runtime to guarantee with barriers and such as well. I don't think a futures implementation of any kind can do this because without those barriers being inserted by either the compiler or runtime this is just not a guarantee you can ever have.
To which, naturally, the response in the futures-based world is "don't do that". Many "futures-based worlds" aren't even truly concurrently running on multiple CPUs where that could be an issue anyhow, although you can still end up with the single-threaded equivalent of a race condition if you work at it, though it is certainly more challenging to get there than with multi-threaded code.
This goes back to, channels are actually fairly heavyweight as concurrency operations go, call it two or three times the cost of a mutex. They provide a lot, and when you need it it's nice to have something like that, but there's also a lot of mutex use in Go code because when you don't need it it can add up in price.
Everything is "just another thing" if you ignore the advantage of abstraction.