Is there yet a std lib way of polling if a future is complete without having to do a blocking Io.Future(type).wait()?
Also, I am aware of the atomic flag checking before it could be brought into the play here. (Was just wondering if I am missing something obvious here.)
AFAICT no, not directly, but passing in an Event and just doing a defer set() at the top will probably do what you need.
What problem are you trying to solve? Might be something better that can be suggested.
So this is a mock code now imagine this with multiple types of future–say a render unit that waits on a texture load before working ahead. But it can’t await() or else the app would lag the main loop. And it just seems extensive to have an atomic flag per future issued.
pub fn main(init: std.process.Init) !void {
const io = init.io;
var prog_alive = true;
var logic = LogicUnit{};
while (prog_alive) {
collectInput();
runPhys();
logic.run(io, &prog_alive);
}
}
const LogicUnit = struct {
data_future: Io.Future(!Data),
fn run(self: *@This(), io: std.Io, prog_alive: *bool) !void {
// check some condition gen by input or physics
if (true) self.data_future = try io.concurrent(loadData, .{self, io});
// no data to work with return the execution to the main while loop
if(!self.data_future.poll()) return;
const data: Data = try self.data_future.await(io);
// conditionally
if(data.suggests_exit) prog_alive.* = false;
}
fn loadData(self: *@This(), io: std.Io) !Data {
// load data from disc away from the main while loop
// dont want ti stall the application
_ = self;
_ = io;
if(false) return error.Unreachable;
return .{};
}
};
Use std.Io.Group to manage multiple tasks which you only need to await, or cancel, when your done with them as a whole.
This does require the tasks not return data directly, you’ll need some kind of out parameter, std.Io.Queue likely meets your needs here.
something like:
pub fn main(init: std.process.Init) !void {
const io = init.io;
var group: Io.Group = .init;
defer group.cancel(io);
var data_buf: [10]Data = undefined;
var queue: Io.Queue(Data) = .init(&data_buf);
defer ...; // their may still be data in the queue, clean that up if you need to
for (data_to_load) |d| {
try group.concurrent(io, loadData, .{io, &queue});
}
while (queue.getOne()) |d| {
collectInput();
runPhys();
if (d.suggests_exit) {
queue.close(); // prevent more data being submitted
break;
}
} else |_| {} // this is an error, if this is in a task you need to return canceled if it occurs
}
fn loadData(io: std.Io, queue: *Io.Queue(Data) ) void {
// you can also return error.Canceled if that is convenient
// load data from disc away from the main while loop
// dont want ti stall the application
// will fail if que is closed or task/group is canceled
// clean things up if that happens
queue.putOne(io, .{}) catch ...;
}
Just for my own edification, can you explain how this differs from Select? It seems like the same concept done manually, but I could be (and likely am) missing something.
std.Io.Select is, in fact, a wrapper around a group and a queue.
I used them directly because it is simpler for this simple example.
There is the caveat that select does not give tasks access to the queue; instead they return values normally, and a generic wrapper function adds to the queue.
This has the downside that tasks cannot handle a closed queue most efficiently, but also is simpler conceptually as only the caller needs to handle it.
Use what works best for your usecase.