Whilst asynchronous IO (non-blocking descriptors with select/poll/epoll/kqueue etc) is not the most documented thing on the web, there are a handful of good examples.
However, all these examples, having determined the handles that are returned by the call, just have a 'do_some_io(fd)
' stub. They don't really explain how to best approach the actual asynchronous IO in such a method.
Blocking IO is very tidy and straightforward to read code. Non-blocking, async IO is, on the other hand, hairy and messy.
What approaches are there? What are robust and readable?
void do_some_io(int fd) {
switch(state) {
case STEP1:
... async calls
if(io_would_block)
return;
state = STEP2;
case STEP2:
... more async calls
if(io_would_block)
return;
state = STEP3;
case STEP3:
...
}
}
or perhaps (ab)using GCC's computed gotos:
#define concatentate(x,y) x##y
#define async_read_xx(var,bytes,line) concatentate(jmp,line): if(!do_async_read(bytes,&var)) { schedule(EPOLLIN); jmp_read = &&concatentate(jmp,line); return; }
// macros for making async code read like sync code
#define async_read(var,bytes) async_read_xx(var,bytes,__LINE__)
#define async_resume() if(jmp_read) { void* target = jmp_read; jmp_read = NULL; goto *target; }
void do_some_io() {
async_resume();
async_read(something,sizeof(something));
async_read(something_else,sizeof(something_else));
}
Or perhaps C++ exceptions and a state machine, so worker functions can trigger the abort/resume bit, or perhaps a table-driven state-machine?
Its not how to make it work, its how to make it maintainable that I'm chasing!