Our next stop in showing off our library for building awaitable synchronization objects are the mutex and recursive mutex.
The mutex and recursive mutex are strange objects when applied to coroutines, because the traditional mutex and recursive mutex apply to threads, and threads are this implied context that every function has and shares with the functions it calls, even though it may not realize it. But coroutines don’t have that. When one coroutine awaits another coroutine, there is no formal connection between the two, so when you do this:
IAsyncAction Outer() { co_await some_mutex.lock(); co_await Inner(); } IAsyncAction Inner() { some_mutex.unlock(); }
the mutex has no way of knowing that the Outer
lock matches the Inner
unlock. It just has to trust the caller.
The lack of coroutine flow identity is particularly troublesome for the recursive mutex, because it has no way of knowing whether a second lock came from the same logical coroutine chain that performed the initial lock.
IAsyncAction Outer() { co_await some_mutex.lock(); co_await Inner(); some_mutex.unlock(); } IAsyncAction Inner() { co_await some_mutex.lock(); something(); some_mutex.unlock(); }
If some_
were an asynchronous recursive mutex, it wouldn’t be able to determine whether the call from Inner
should be allowed, since it has no access to coroutine identity, nor to the dependency relationships among coroutines.
Basically, there are no coroutine mutexes or recursive mutexes. Recursive mutexes simply cannot exist, and once you strip away the thread identity features of mutexes, all you have left is an auto-reset event.
using awaitable_mutex = awaitable_auto_reset_event;
But the shared mutex, that’s interesting. We’ll look at that next time.
You could use something else as the identity, for recursion purposes. Like a task id or the address of the coroutine handle.
Task id could work, although it creates the hassle of having to pass it around. Coroutine address is not useful because all that would detect is the same coroutine entering the mutex twice, which is not a common scenario. Usually, it’s where one function enters the mutex, and then it calls another function that also enters the mutex. And the two functions need not themselves be coroutines.