Tokio channel deadlock

I faced some interesting behavior using tokio's channel - tokio::sync::mpsc .

Long story short: in some cases, receiver doesn't get the value, sent by the sender if there's something like loop (or any other heavy computations, i assume?) on the current tokio-thread.

Channel from std::thread::sync works perfectly though. I assume it has something to do with either tokio scheduler or cpu context switch ?

Even in the example below it works correctly from time to time. But sometimes it prints "second thread" only when the first thread is dead due to integer overflow.

How can i overcome this problem ? For now i use default channel() from std::thread::sync . But it seems like a crutch to me.

Example:

use tokio::sync::mpsc;

async fn thread1(mut sender: mpsc::Sender<bool>) {
    let mut i = 0;
    loop {
        i += 1;
        if i == 100 {
            sender.send(true).await.unwrap();
        }
    }
}

async fn thread2(mut receiver: mpsc::Receiver<bool>) {
    receiver.recv().await.unwrap();
    println!("second  thread");
}

#[tokio::main]
async fn main() {
    let (sender, receiver) = mpsc::channel(1);

    let p1 = tokio::spawn(async move {
        thread1(sender).await;
    });

    let p2 = tokio::spawn(async move {
        thread2(receiver).await;
    });

    tokio::join!(p1, p2);
}

Thanks in advance!

For long running computations, you should use tokio::task::spawn_blocking. In fact, I believe you are facing the exact problem described in the documentation here.

My best guess for what's going on: in your example, the you initialized the channel with a buffer size of 1. This means that a call to send will be Poll::Ready since the channel indeed has an available buffer for the result to be sent immediately. Thread1 then goes on looping forever and does not yield since there are no more await points, preventing thread2 from making progress.

1 Like

Don't use the std channel, and don't run expensive computations in async code.

1 Like

First of all, thank you a lot! Blocking operation helped me indeed.
About your explanation and the documentation. If i understood correctly: because of the single await inside of the loop, tokio isn't able to swap tasks ?

Thread1 then goes on looping forever and does not yield since there are no more await points, preventing thread2 from making progress.

Can you clarify about this part a little bit more ?
Yet again, thank you a lot!

Can you explain why i shouldn't use std channel ?
Do you mean, that i shouldn't use std channel inside of async ?

Yes, if your task spends a long time without reaching an await, then it cannot be swapped with the other task, and the other task can't make progress. You should not use the std channel in async because the methods put the thread to sleep without an await.

The try_* methods on the std channel is ok, of course. They don't put the thread to sleep.

The Tokio channel sleeps using an await, so that's fine to use.

1 Like

Thank you a lot :heart:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.