I have two branches that I want to execute concurrently, but not in parallel, i.e. only branch runs at a time but may interrupt due to I/O, allowing the other branch to proceed while the first one is pending. In particular, I will only use tokio::join! but nottokio::spawn.
Both branches need to operate on a data structure. The access to that data structure only happens for a short time in some non-async code (i.e. not across an await). Therefor, I believe I could use a RefCell to obtain a temporary RefMut to modify the value in each branch. Using a Mutex instead of a RefCell shouldn't be necessary, because only one branch executes at the same time, and the RefMut (the mutable borrow) will be dropped before the other branch can execute.
This works fine, but it comes at a downside: The resulting (joined) future will be !Send. I don't like that because I think it would be okay to send the result of tokio::join! to a different thread in the pool (as done by the async task scheduler).
The problem is not that the two tasks would be accessing the RefCell in parallel. The problem is exactly that you are then expecting to send the resulting future to another thread. However, that future has to have a reference to the RefCell (and the closure correctly captures it by reference, too), which is !Sync, so anything containing a reference to it is correctly !Send.
I do think this does ultimately require a Mutex. How exactly would you need to access the locked value? Your toy example doesn't demonstrate at all whether it would be locked many times. The RefCell is borrowed exactly twice in that example; I don't think that the equivalent with a Mutex would impose a noticeable overhead.
Thanks, that might be the best solution for my problem, as everything will be safe and yet relatively efficient. Do you know by any chance how bad (or good) the performance of the standard library's Mutex is? I guess it directly uses the OS interface?
That is the same reasoning that the compiler is doing, and I can follow it. However, the compiler won't (can't) notice that RefCell::borrow_mut() can never be called until all RefMuts have been dropped again. That is guaranteed due to incrementors code and that it is only used in two async move blocks, which are joined (and not spawned, for example).
I don't think it does. Here is how I believe it would work with unsafe code (not wanting to imply that it's the best choice to go unsafe here): Playground. Just to allow me to understand the problem better, can anyone tell me if this unsafe example is sound in the particular "toy" case?
I'm sorry, I just tried to make the example as concise as possible.
I think that might really depend on the kind of Mutex being used. I will execute such a closure a thousand times later (concurrently, not in parallel), and I do believe that pthread_mutex_lock, for example, is a costly operation (when comparing it to async code where the OS overhead of threading is ideally reduced to a minimum).
I think your main idea that it's sound it correct; this seems more-or-less to be a special case of the reasoning that a future with a T: Send + !Sync local variable x: T that also keeps some reference r: &T to x over an await should still be soundly sendable between threads as long as that reference stays contained within the future, i.e. the reference gets sent together with its target. The compiler does not support any reasoning like this though. I do remember seeing some (light) discussion before on whether such reasoning could be possible.
Still, even if it's sound I wouldn't recommend using unsafe for this, in particular taking into consideration that alternative solutions like using parking_lot::Mutex probably won't have noticable overhead.
Thanks for that info. I think it's not suuuuch a big deal that the compiler can't do the reasoning. There are other cases in Rust, when runtime checks with little cost need to be done (e.g. when accessing a Vec by index), even if it could be proven that they are not necessary in a particular case. What bugged me in my case was the (supposedly OS-level) synchronization overhead. parking_lot should help me here. Thanks!