Though I have read all of the OP, the answer and the comments of Why do I get a deadlock when using Tokio with a std::sync::Mutex?, I don’t yet understand why the code in the OP blocks forever.
Here’s the slightly changed version of the original code:
use std::sync::Arc;
use std::sync::Mutex;
// use tokio::sync::Mutex;
use tokio::time::Duration;
async fn f(mtx: Arc<Mutex<i32>>, index: usize) {
println!("{}: trying to lock...", index);
{
let mut v = mtx.lock().unwrap();
// let mut v = mtx.lock().await;
println!("{}: locked", index);
tokio::time::sleep(Duration::from_millis(1)).await;
*v += 1;
}
println!("{}: unlocked", index);
}
#[tokio::main]
async fn main() {
let mtx = Arc::new(Mutex::new(0));
tokio::join!(f(mtx.clone(), 1), f(mtx.clone(), 2));
}
The output is
1: trying to lock...
1: locked
2: trying to lock...
(and blocks forever...)
According to the answer and the comments (and if I read them correctly), the reason is the entire code is executed in a single-threaded environment. If the italicized part is true, I can understand the blocking behavior. However, I don’t understand if the italicized part is actually true in the first place.
As far as I understand,
-
the default runtime of Tokio is multi-threaded unless you explicitly specify
#[tokio::main(flavor = "current_thread")](source) -
and
awaited tasks can automatically be moved to another worker thread (source).
So I think the code does NOT block if the tasks (i.e. f(mtx.clone(), 1).await, f(mtx.clone(), 2).await and sleep(...).await) are chosen (by Tokio runtime) to be executed in different threads, but the code looks blocking as the runtime happens to choose the tasks are all executed in the same single thread.
Is my understanding correct?
>Solution :
the entire code is executed in a single-threaded environment
Indeed.
the default runtime of Tokio is multi-threaded
Yes, but that only applies to tasks. Tasks are like lightweight threads and they can execute in parallel on different OS threads. But tokio::join!() does not create new tasks. It is an async primitive that takes two (or more) futures and combine them into one via a state machine, on the same task. This has the advantage that it is more lightweight, but it also means that if you’re blocking in one of the futures all others will also be blocked. Therefore, it is good for really IO-bound code, if the code is CPU-bound even a little or you have many futures it is better to spawn a task.
Also note that tokio tasks may run on different threads, but this is not guaranteed. In particular, tokio has optimization heuristics that can cause only one thread to be used. In this case this code will also deadlock. Also, even if it will not deadlock, it is still blocking, and blocking should never be done in async environment.