dpc / mioco.pre-0.9

Scalable, coroutine-based, asynchronous IO handling library for Rust programming language. (aka MIO COroutines).
Mozilla Public License 2.0
457 stars 30 forks source link

Mioco is blocked polling for IO while there are runnable tasks #153

Open jprafael opened 8 years ago

jprafael commented 8 years ago

Not sure if this should be considered a performance issue or a bug.

Consider the following test case

extern crate mioco;

use mioco::sync::mpsc::{sync_channel, Receiver, SyncSender};

fn producer(tx: SyncSender<usize>) {
    for i in 0.. {
        println!("sending {}", i);
        tx.send(i).expect("failed to send to consumer");
    }
}

fn consumer(rx: Receiver<usize>) {
    while let Ok(i) = rx.recv() {
        println!("received {}", i);
    }
}

fn main() {
    let size = 1;
    let (tx, rx) = sync_channel(size);

    mioco::start(|| {
        let producer = mioco::spawn(|| producer(tx));
        let consumer = mioco::spawn(|| consumer(rx));

        producer.join().unwrap();
        consumer.join().unwrap();
    }).unwrap();
}

This example shows a normal pattern where there is a producer that is decoupled from a consumer by a bounded queue (the sync_channel).

It is expected that when the queue is full, either because consumer is slow or hasn't been scheduled as often, that the producer blocks. Later, when there is enough space the producer gets resumed.

However, running the above code you can see that the "throughput" is somewhat surprisingly just 2 per second. Most likely, when the producer coroutine gets yielded (src/sync/mpsc.rs:238) the event loop thinks there is nothing left to do and starts blocking on IO (for some ms). In this case, the consumer coroutine is ready to be executed and the event loop should have either skipped the io_poll() entirelly, or done it with a 0 timeout.

If you are wondering, increasing the queue size to 2 does increase the throughput. But the result is a lot higher than just 2x. If the scheduling gods make it so that the two courotines are alternating requests to the queue then the limit is never hit, and they are able to proceed at full CPU speed.

dpc commented 8 years ago

I would have to look more into it, but I'm aware that sync channel has a naive implementation using yield_now in both producer and consumer and the only notification is on channel becoming non-empty from producer to consumer. The consumer should definitely send a notification to producer, when the channel is no longer full. My guess right now is that is the root of the problem, combined with 100ms default tick + a mio bug that makes it effectively 200ms

I'll look into that at some point, but I'm always happy to accept PRs. :)