Help with borrow checker, structured concurrency and variance

I have 2 pieces of code, one is compiling and other does not. I do not understand the difference. I suspect It has something to do with variance, but I am not sure:

This one uses tokio's oneshot, and it compiles.

use std::{future::{Future, IntoFuture}, marker::PhantomData};
use tokio::task::JoinHandle;

#[derive(Debug)]
struct TaskHandle<'a>(JoinHandle<()>, PhantomData<&'a ()>);

impl Drop for TaskHandle<'_> {
    fn drop(&mut self) {
        self.0.abort();
    }
}

fn spawn<'a, IFut, Fut>(_fut: IFut) -> TaskHandle<'a>
where
    IFut: IntoFuture<Output = (), IntoFuture = Fut>,
    Fut: 'a + Send + Future<Output = ()>,
{
    todo!()
}

#[tokio::main]
async fn main() {
    let mut foo = 32;
    let (tx, rx) = tokio::sync::oneshot::channel::<TaskHandle<'_>>();
    let handle = spawn(async {
        rx.await.unwrap();
        foo += 10;
    });

    tx.send(handle).unwrap();
    assert_eq!(foo, 42);
}

This one uses chan crate, which uses safe code with Arc and Mutex. This does not compile

use std::{future::{Future, IntoFuture}, marker::PhantomData};
use tokio::task::JoinHandle;

#[derive(Debug)]
struct TaskHandle<'a>(JoinHandle<()>, PhantomData<&'a ()>);

impl Drop for TaskHandle<'_> {
    fn drop(&mut self) {
        self.0.abort();
    }
}

fn spawn<'a, IFut, Fut>(_fut: IFut) -> TaskHandle<'a>
where
    IFut: IntoFuture<Output = (), IntoFuture = Fut>,
    Fut: 'a + Send + Future<Output = ()>,
{
    todo!()
}

#[tokio::main]
async fn main() {
    let mut foo = 32;
    let (tx, rx) = chan::sync::<TaskHandle<'_>>(0);

    let handle = spawn(async {
        rx.recv();
        foo += 10;
    });

    tx.send(handle);

    assert_eq!(foo, 42);
}

Error:

 1  error[E0597]: `rx` does not live long enough
   --> src/main.rs:31:9
    |
 27 |     let (tx, rx) = chan::sync::<TaskHandle<'_>>(0);
    |              -- binding `rx` declared here
 28 |
 29 |     let handle = spawn(async {
    |                        ----- value captured here by coroutine
 30 |         // rx.await.unwrap();
 31 |         rx.recv();
    |         ^^ borrowed value does not live long enough
 ...
 39 | }
    | -
    | |
    | `rx` dropped here while still borrowed
    | borrow might be used here, when `tx` is dropped and runs the `Drop` code for type `chan::Sender`
    |
    = note: values in a scope are dropped in the opposite order they are defined

 2  error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
   --> src/main.rs:38:5
    |
 29 |     let handle = spawn(async {
    |                        ----- mutable borrow occurs here
 ...
 32 |         foo += 10;
    |         --- first borrow occurs due to use of `foo` in coroutine
 ...
 38 |     assert_eq!(foo, 42);
    |     ^^^^^^^^^^^^^^^^^^^ immutable borrow occurs here
 39 | }
    | - mutable borrow might be used here, when `tx` is dropped and runs the `Drop` code for type `chan::Sender`
    |
    = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

 Some errors have detailed explanations: E0502, E0597.
 For more information about an error, try `rustc --explain E0502`.
 error: could not compile `play` (bin "play") due to 2 previous errors

It's not because of variance.

The problem is that you need the async block you pass to tokio::spawn to capture ownership of the receiver. If it only captures a borrow, you get the error you see.

Capturing ownership happens in two scenarios:

  • You perform an operation that requires ownership of the value, or
  • You annotate the async block with move.

With the oneshot channel, awaiting it consumes it, so you fall in the first category and you are good. But with the other channel, the recv method takes &self which means that it only requires a borrow of the receiver, so you fall in neither case above, thus the error appears.

To fix it, add move to the async block.

tokio::spawn(async move {});
4 Likes