←back to thread

517 points bkolobara | 1 comments | | HN request time: 0s | source
Show context
Spivak ◴[] No.45041771[source]
How do you encode the locking issue in the type system, it seems magical? Can you just never hold any locks when calling await, is it smart enough to know that this scheduler might move work between threads?
replies(5): >>45041806 #>>45041833 #>>45041852 #>>45041891 #>>45041898 #
vlovich123 ◴[] No.45041833[source]
Presumably the author is using tokio which requires the future constructed (e.g the async function) to be Send (either because of the rules of Rust or annotated as Send) since tokio is a work-stealing runtime and any thread might end up executing a given future (or even start executing and then during a pause move it for completion on another thread). std::sync::MutexGuard intentionally isn't annotated with Send because there are platforms that require the acquiring thread be the one to unlock the mutex.

One caveat though - using a normal std Mutex within an async environment is an antipattern and should not be done - you can cause all sorts of issues & I believe even deadlock your entire code. You should be using tokio sync primitives (e.g. tokio Mutex) which can yield to the reactor when it needs to block. Otherwise the thread that's running the future blocks forever waiting for that mutex and that reactor never does anything else which isn't how tokio is designed).

So the compiler is warning about 1 problem, but you also have to know to be careful to know not to call blocking functions in an async function.

replies(6): >>45041892 #>>45041938 #>>45041964 #>>45042014 #>>45042145 #>>45042479 #
Spivak ◴[] No.45041964[source]
Thank you! It seems so simple in hindsight to have a type that means "can be moved to another thread safely." But the fact that using a Mutex in your function changes the "type" is really novel. It becoming Send once the lock is released before await is just fantastic.
replies(2): >>45042232 #>>45042345 #
1. NobodyNada ◴[] No.45042345{3}[source]
The way that this fully works together is:

- The return type of Mutex::lock() is a MutexGuard, which is a smart pointer type that 1) implements Deref so it can be dereferenced to access the underlying data, 2) implements Drop to unlock the mutex when the guard goes out of scope, and 3) implements !Send so the compiler knows it is unsafe to send between threads: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html

- Rust's implementation of async/await works by transforming an async function into a state machine object implementing the Future trait. The compiler generates an enum that stores the current state of the state machine and all the local variables that need to live across yield points, with a poll function that (synchronously) advances the coroutine to the next yield point: https://doc.rust-lang.org/std/future/trait.Future.html

- In Rust, a composite type like a struct or enum automatically implements Send if all of its members implement Send.

- An async runtime that can move tasks between threads requires task futures to implement Send.

So, in the example here: because the author held a lock across an await point, the compiler must store the MutexGuard smart pointer as a field of the Future state machine object. Since MutexGuard is !Send, the future also is !Send, which means it cannot be used with an async runtime that moves tasks between threads.

If the author releases the lock (i.e. drops the lock guard) before awaiting, then the guard does not live across yield points and thus does not need to be persisted as part of the state machine object -- it will be created and destroyed entirely within the span of one call to Future::poll(). Thus, the future object can be Send, meaning the task can be migrated between threads.