Skip to content

tokio::io::bsd::Aio can cause multithreaded runtime to busy-loop #4728

@asomers

Description

@asomers

Version
Tokio git master at f6c0405

Platform
FreeBSD 14.0-CURRENT amd64

Description

Background
The funny thing about AIO is that you don't register it with kevent. Instead, you register it by setting a certain field within the struct aiocb that you submit with a syscall like aio_write. When it's ready, kevent will return an event with EVFILT_AIO set. The user must then call aio_return to free the kernel resource, or else kevent will keep reporting that event as ready. As written now, the aio_return call happens outside of Tokio, in the tokio-file crate, in a custom Future's poll method.

Problem
With the multi-threaded runtime, it's possible that the future's task is on one thread, but the kqueue it's registered to is being polled by a different thread. When the kernel notifies the polling thread, that thread will signal (how I'm not sure) the thread with the future. Then it will go back to sleep waiting for more events. But if the thread with the future doesn't poll it quickly, then polling thread will immediately get woken again. This can lead to many more kevent calls than should be necessary. In my application, I see up to 200x more kevent calls than aio_return calls.

Possible Solutions

  • Move the struct aiocb into the IO Driver, and arrange for the IO driver to promptly call aio_return upon notification, storing the result.
  • Use EV_ONESHOT to make the aio operation edge-triggered. I'm not sure if this works with POSIX AIO.

Example
This test case will obviously never finish. Ideally it would simply hang. But because nothing ever calls aio_return, the IO driver will busy loop around kevent, and it will spin the cpu.

    #[tokio::test(flavor = "current_thread")]
    async fn busy() {
        use futures::FutureExt;

        let f = tempfile().unwrap();
        let fd = f.as_raw_fd();
        let aiocb = AioCb::from_fd(fd, 0);
        let source = WrappedAioCb(aiocb);
        let mut poll_aio = Aio::new_for_aio(source).unwrap();
        (*poll_aio).0.fsync(AioFsyncMode::O_SYNC).unwrap();
        let some_aio_future = FsyncFut(poll_aio);
        tokio::pin!(some_aio_future);
        
        // poll it once
        assert!(some_aio_future.as_mut().now_or_never().is_none());
        
        std::future::pending::<()>().await;
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-tokioArea: The main tokio crateC-bugCategory: This is a bug.M-ioModule: tokio/io

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions