3 ms·
I wonder how much that is obsoleted by io_uring by now? The article mentions how non-blocking IO is the way to gain back control otherwise conceded to the kern
by diarrhea 3y ago
I wonder how much that is obsoleted by io_uring by now?
The article mentions how non-blocking IO is the way to gain back control otherwise conceded to the kernel in the case of blocking IO. However, non-blocking IO has to be laboriously implemented in user code. Is that true still? What if we used the threading model but relied on io_uring?
- ComputerGuru 3y agoYou still need async (or an event loop model) because otherwise your threads will block waiting for the io_uring result when they could be put to better use (namely submitting new io_uring jobs or conveying the results of completed ones). Note that the io_uring approach isn't too different (just more universalized) than how Windows has always implemented async operations (with overlapped and io completion ports). Code written for that environment originally just used the event loop model but it's easier/better ergonomics to wrap it in async and use that instead (e.g. how C#'s async/await works) because you can avoid callback hell, state passing, etc. and just write code "linearly" with cooperative yield points invisibly stashing and retrieving the "stack" for you.
- memefrog 3y agoWhat? The total opposite is true! 'async/await' is callback hell. It's the barest syntactic sugar over it, but it's definitely callback hell. You write code sequentially if you use threads (whether they're userspace M:N or N:1 threads, or 1:1 kernel threads).
- ComputerGuru 3y agoSorry, not callback hell (that's what JS had before async/await) but certainly not code that linearly handles a connection from start to end, either. (Threaded code is certainly "linear" but I meant from the perspective of walking through the lifetime of a connection, one connection at a time linearly.)
- memefrog 3y agoYou can use a "threading model" with readiness-based or completion-based non-blocking IO. There are (userspace-)threaded runtimes based on epoll/etc. and also those based on io_uring/IOCP/etc.
- FridgeSeal 3y agoAs a commenter in the other sub-thread pointed out, you still need async capabilities to make the most of io_uring API’s (I’m a huge fan on”f io_uring). Whilst you can do this, I still think the best approach is “actual-async”. The idea of “spin up this worker and shunt the data across to it just so it can block” strikes me as somewhat wasteful. If you know you’re going to have to wait, you’re better off not expending resources doing so, and instead use the time to do useful work instead.
- memefrog 3y ago"Spin up this worker" is trivial: one memory allocation, probably from a free list, to allocate a thread stack, setting a couple of pointers, and a function call. Doing any kind of I/O is much heavier-weight. Thread stacks don't have to even be very large. It doesn't expend any additional resources to have a userspace thread blocked on a socket. You can just have a pointer to the thread as the userdata of the SQE. It doesn't make any sense to say 'use the time to do useful work instead'. That's what you do when you block: in a threaded model, when you block you do 'io_uring_get_sqe', 'io_uring_prep_read' (or whatever), 'io_uring_set_userdata' 'io_uring_submit' and then you switch contexts (with <ucontext.h> or some equivalent) to another thread that is ready. When you have no ready threads you just call 'io_uring_wait_cqe', and for each completion you do 'io_uring_get_userdata', get a pointer to the thread, record the result of the operation and append the thread to a runqueue to be resumed. From the perspective of the thread, you write int e = read(fd, buf, sizeof buf); if (e < 0) err(1, "read: %s", strerror(-e)); and it all feels completely sequential. Underneath it is stackful coroutines.