Consider the following snippet:
use std::net::Ipv4Addr;
use std::time::{Duration, Instant};
use tokio::net::{TcpListener, TcpStream};
use tokio::time;
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::spawn(async {
loop {
println!("[{:?}] I am alive!", Instant::now());
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
time::sleep(Duration::from_millis(1000)).await;
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0)).await.unwrap();
let address = listener.local_addr().unwrap();
drop(listener);
time::sleep(Duration::from_millis(1000)).await;
println!("Connecting");
let _connection = TcpStream::connect(address).await;
println!("Done");
time::sleep(Duration::from_millis(1000)).await;
}
In summary, on a single-threaded run of tokio
I do the following:
- I spawn a task to print something every 100 ms, just know that everything keeps running.
- I bind a
TcpListener
anywhere, get its address. - I
drop
theTcpListener
. - I then try to connect to the
TcpListener
's former address.
Now, here is the output that I get:
[Instant { t: 322033533953690 }] I am alive!
[Instant { t: 322033639639772 }] I am alive!
[Instant { t: 322033740157897 }] I am alive!
[Instant { t: 322033844392865 }] I am alive!
[Instant { t: 322033948457992 }] I am alive!
[Instant { t: 322034051559169 }] I am alive!
[Instant { t: 322034155284597 }] I am alive!
[Instant { t: 322034257879982 }] I am alive!
[Instant { t: 322034361229691 }] I am alive!
[Instant { t: 322034462803458 }] I am alive!
[Instant { t: 322034564346433 }] I am alive!
[Instant { t: 322034668035678 }] I am alive!
[Instant { t: 322034772372462 }] I am alive!
[Instant { t: 322034877014330 }] I am alive!
[Instant { t: 322034978911485 }] I am alive!
[Instant { t: 322035080462270 }] I am alive!
[Instant { t: 322035181321674 }] I am alive!
[Instant { t: 322035282532413 }] I am alive!
[Instant { t: 322035384651311 }] I am alive!
[Instant { t: 322035488278141 }] I am alive!
Connecting
Done
[Instant { t: 322036543294463 }] I am alive!
[Instant { t: 322036644255096 }] I am alive!
[Instant { t: 322036747256653 }] I am alive!
[Instant { t: 322036853860504 }] I am alive!
[Instant { t: 322036955935656 }] I am alive!
[Instant { t: 322037057575980 }] I am alive!
[Instant { t: 322037162679980 }] I am alive!
[Instant { t: 322037264386141 }] I am alive!
[Instant { t: 322037365479256 }] I am alive!
[Instant { t: 322037468083176 }] I am alive!
As expected, the connection does not succeed. Somewhat reasonably, it takes around one second for the connection attempt to return (some timeout might have been put in place, I guess?).
HOWEVER, as you can notice by the lack of I am alive
messages between Connecting
and Done
, the call to TcpStream::connect
seems to be blocking...! The task I spawned to print every 100 ms does not execute (and nor does anything else) for the whole time the connection attempt is in progress.
I am baffled: tokio
calls are supposed to be asynchronous, and not block the entire event loop.. right? Am I missing something here? Or did I stumble upon some sort of bug?
From my Cargo.lock
:
...
[[package]]
name = "tokio"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
...
Update: About my system
- Operating System: MacOs Big Sur (11.1)
- Computer: MacBook Pro, Intel 64 bits
- Rust version: 1.56.0
Update: Additional observations
In the above code, changing Ipv4Addr::UNSPECIFIED
to Ipv4Addr::LOCALHOST
significantly shortens the time to (fail to) establish the connection. It is still unclear to me whether or not it blocks while doing so.