Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to implement a Yield #322

Open
taooceros opened this issue Nov 20, 2024 · 1 comment
Open

How to implement a Yield #322

taooceros opened this issue Nov 20, 2024 · 1 comment

Comments

@taooceros
Copy link

taooceros commented Nov 20, 2024

Version
0.2.4

Platform
Linux seoul-microk8s 5.15.0-125-generic

Description
I need to do a customized polling

pub async fn polling(self: Rc<Self>) {
    unsafe {
        while Rc::strong_count(&self) > 1 {
            while doca_pe_progress(self.pe.as_ptr()) > 0 {
                println!("progress");
            }
            futures_lite::future::yield_now().await;
        }
    }
}

How could I implement a yield in monoio? If I follow the standard way (e.g. like futures_lite)

pub fn yield_now() -> YieldNow {
    YieldNow(false)
}

/// Future for the [`yield_now()`] function.
#[derive(Debug)]
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub struct YieldNow(bool);

impl Future for YieldNow {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if !self.0 {
            self.0 = true;
            cx.waker().wake_by_ref();
            Poll::Pending
        } else {
            Poll::Ready(())
        }
    }
}

Because it notify the waker, it will be inserted into the front of the task queue instead of the back of the task queue, which means other tasks will be starved (and then because nobody submit a task it will just poll forever instead of doing anything valuable).

fn yield_now(&self, task: Task<Self>) {
crate::runtime::CURRENT.with(|cx| cx.tasks.push_front(task));
}

The same version works in tokio, which can either due to their scheduling policy or the cooperative yielding.

In tokio special version of yield_now, they includes a context::defer(cx.waker());, which probably prevent the needs for using the cooperative yielding.

@taooceros taooceros changed the title Yield How to implement a Yield Nov 20, 2024
@harry-van-haaren
Copy link

+1 to this request to add yield_now()

The yield_now() function is very useful, and missing from Monoio today.

I've dug into the Tokio sources a bit, and there is a seperate way of tracking &Waker instances and scheduling those, without having the corresponding Future instance. This is how tokio schedules the Waker, but doesn't spawn() or do any handling of the Future itself. I did not find similar concepts in the MonoIO runtime etc, so I'm not sure the defer(waker: &Waker) function can be easily built in MonoIO: perhaps some of the developers closer to the code have a better/novel idea to schedule a &Waker in MonoIO.

A bad but easy solution

A terrible workaround is to do a sleep; it gives very bad performance, but works at least. Note that the sleep scheduling is at 1 ms granularity, so changing that to nanos(1) isn't going to go any faster :/

monoio::time::sleep(std::time::Duration::from_millis(1)).await;

The ping-pong channel solution

A better workaround is to use a what I'll call a "ping pong" task, which uses a secondary task to "unpause" the main task again. Some pseudo code might help show how this can be implemented. Before the loop { /* long running, no .await */ } you create a local_sync (monoio depends on it too) bounded channel, which has async send/recv as its bounded.

let (yield_tx, mut yield_rx) = local_sync::mpsc::bounded::channel::<()>(1);

Spawn a task which only transmits events forever (fixup code & handle errors in this part):

monoio::spawn(async {
loop {
   yield_tx.send(()).await;
}
});

Now in the main task's "forever" loop without .await points (where you would otherwise put the yield_now() call using Tokio)

yield_rx.recv().await;

Some nice properties of this solution:

  • There is no allocation on the "hot path" (the spawn() function does a lot of work in the background, so avoid it!)
  • The channel is bounded to 1, so the tx task sleeps until the main task has run. At that point, the TX side becomes "writeable" again, which will "unpark" or resume the yield_tx side, and cause it to write one more () into the channel.
  • As we're using a local-channel, there's no atomics/heavyweight instructions here.

A quick check on performance with perf record does not show a hotspot on the "ping pong" task code, but I have not done detailed investigation.

Summary

The ping-pong task is probably OK as a "normal" workaround, however requires implementing at each yield_now() location. Perhaps this is acceptable, but I hope yield_now() functionality is included as a pub function in MonoIO's future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants