Borrowing

When spawning tasks, the spawned async expression must own all of its data. The select! macro does not have this limitation. Each branch’s async expression may borrow data and operate concurrently. Following Rust’s borrow rules, multiple async expressions may immutably borrow a single piece of data or a single async expression may mutably borrow a piece of data.

Let’s look at some examples. Here, we simultaneously send the same data to two different TCP destinations.

  1. use tokio::io::AsyncWriteExt;
  2. use tokio::net::TcpStream;
  3. use std::io;
  4. use std::net::SocketAddr;
  5. async fn race(
  6. data: &[u8],
  7. addr1: SocketAddr,
  8. addr2: SocketAddr
  9. ) -> io::Result<()> {
  10. tokio::select! {
  11. Ok(_) = async {
  12. let mut socket = TcpStream::connect(addr1).await?;
  13. socket.write_all(data).await?;
  14. Ok::<_, io::Error>(())
  15. } => {}
  16. Ok(_) = async {
  17. let mut socket = TcpStream::connect(addr2).await?;
  18. socket.write_all(data).await?;
  19. Ok::<_, io::Error>(())
  20. } => {}
  21. else => {}
  22. };
  23. Ok(())
  24. }

The data variable is being borrowed immutably from both async expressions. When one of the operations completes successfully, the other one is dropped. Because we pattern match on Ok(_), if an expression fails, the other one continues to execute.

When it comes to each branch’s <handler>, select! guarantees that only a single <handler> runs. Because of this, each <handler> may mutably borrow the same data.

For example this modifies out in both handlers:

  1. use tokio::sync::oneshot;
  2. #[tokio::main]
  3. async fn main() {
  4. let (tx1, rx1) = oneshot::channel();
  5. let (tx2, rx2) = oneshot::channel();
  6. let mut out = String::new();
  7. tokio::spawn(async move {
  8. // Send values on `tx1` and `tx2`.
  9. });
  10. tokio::select! {
  11. _ = rx1 => {
  12. out.push_str("rx1 completed");
  13. }
  14. _ = rx2 => {
  15. out.push_str("rx2 completed");
  16. }
  17. }
  18. println!("{}", out);
  19. }