A soon as you type your channels you run into composability problems as you cannot easily wait for _either_ Channel[T1] or Channel[T2].
Rather than avoiding signals I would embrace them as a general purpose interrupt mechanism. And an interrupt can be scheduled on a different thread. And then I would base the entire API on top of it. This way everything is `async` in a natural fashion. The context switches become more visible in the code. No channels, sinks or sources required. For example: proc requestReadOp() = var buf: SomeBuffer req(url = "somefile.txt", op = fmRead, mode = Blocking, env = addr(buf), oncomplete = proc (env: pointer, src: pointer, size: int) {.cdecl.} = copyMem env, src, size ) Run This way the OS only need to provide the callback based version. Well ... I suppose it's close enough to a traditional event loop.