Asynchronous filesystem operations aren't usually possible on many platforms, but I'll focus on Linux.
libaio, a C library for "asynchronous filesystem operations," literally spawns a thread pool for "asynchronous" filesystem operations on Linux. Tokio and async-std IIRC do exactly the same. Linux has had issue supporting true async filesystem I/O because asynchronous operations aren't always internally supported based on filesystem implementations and other kernel details.
However, withoutboats' new library
ringbahn uses a new kernel API,
io_uring, for doing asynchronous operations which does seem to support true asynchronous filesystem operations, though it works differently than many other standard async systems. Most async systems work like this:
- I ask the kernel to read/write a socket.
- The kernel returns me an ID.
- I poll that ID until the kernel says that the operation is ready.
- If it's a read operation, I can grab that info from the kernel.
io_uring works differently in that the kernel will actually write data into a buffer you ask for directly, and this has proved somewhat difficult to work with safely in Rust, but
ringbahn does some cool Rust things in order to make this safe.
io_uring is only supported in Linux kernel version 5.5 or later, which is fairly new.
io_uring should generally provide true asynchronous IO for filesystem objects in addition to sockets and the like.
In summary, It really depends on what OS you're using and on what asynchronous system you're using, but at least on Linux,
epoll doesn't support asynchronous filesytem operations, so threads are used under the hood. Tokio/async-std use actual OS threads to do filesystem reads/writes.
TL;DR whether you use an async executor or not, you're probably going to end up using OS threads under the hood for filesystem access, unless you use something based on