Unsafe Cell async

When using async libraries if you spawn a new task you will often have to deal with concurrency issues surrounding this (wrapping in Arc<Mutex<>>). If I was to only have a single threaded application (e.g tokio max_threads=1) could I wrap my variables shared across threads in UnsafeCell instead? If so why do I still have to wrap in UnsafeCell?

You don't have to reach for unsafe - it's entirely possible to use Rc<RefCell<_>>, if you use single-threaded type of scheduler (e.g. tokio with feature = "rt" and not feature = "rt-multi-thread"), which doesn't require spawned futures to be Send.

*Cell is necessary anywhere you have some shared mutable state. This is not strictly question of parallel execution - the iconic example of single-threaded shared mutability problem is iterator invalidation. See here for a good explanation.

UnsafeCell on its own is not a synchronization mechanism, and doesn't guarantee correctness even for single-threaded applications. It's a very complicated low-level building block, which is used only in unsafe code implementing more useful higher-level abstractions, like Mutex itself.

I'm not sure what exactly is your issue with Arc<Mutex<T>>. Is it a worry about performance? An uncontended single-threaded mutex is basically free. Your tokio-based web server will definitely spend orders of magnitude more time on I/O. Is it the verbosity of declaration? I suggest to get used to it, that's standard Rust. Of course, you can always declare a type alias if you use a certain type a lot. Is it the locking API? That's basically a requirement even in single-threaded code for shared data, Rc<RefCell<T>> uses basically the same mechanism.

Note that tokio requires your shared data to be Send, even in single-threaded mode. In short, that's part of its design, and having different requirements for single-threaded code would basically require a separate library anyway. Even if you run all your async tasks on a single executor thread, you still need extra blocking threads to perform non-blocking IO. The basic OS IO interfaces used by tokio are blocking. This means that whenever you want to do a blocking operation, the relevant data and the request must be offloaded to a separate blocking thread. Since your shared data may be involved in those requests, it must also always be Send.

It's possible to have a truly single-threaded runtime with no Send requirements (see e.g. glommio), but that's just not what tokio is, and it comes with its own limitations.

In single-threaded mode it should be possible to use LocalSet, if I understand correctly?

LocalSet must be used within the Runtime::block_on call. You can't call it outside of a usual runtime, or inside tokio::spawn. This makes it quite inconvenient. You could use it in principle, but I don't think readymade web frameworks do it. Also, if you use tokio::spawn, then the future will need to be Send anyway, and if you use block_in_place, then you will, well, block the current thread running the LocalSet.

1 Like