A customer found that if they ran their program under Application Verifier, the tool identified that their program was acquiring an SRWLock in exclusive mode on one thread, but was releasing it on another thread. They found a remark in the documentation:
The SRW lock must be released by the same thread that acquired it.
They asked what goes wrong if you break this rule, because the documentation doesn’t say.
The documentation doesn’t say what goes wrong because the operation is not supported, so the behavior is undefined. You don’t have to define undefined behavior.
A member of the kernel team nevertheless explained what goes wrong.
The system keeps track of which thread have acquired an SRWLock so that it can deal with priority inversion scenarios: If a low-priority thread acquires an SRWLock, and a higher-priority thread blocks waiting to acquire the SRWLock, then the priority of the waiting thread is transferred to the owner thread.¹
This information to assist in avoiding priority inversion is kept in preallocated per-thread data.
If you release an SRWLock from a thread different from the one that acquired it, then the internal bookkeeping gets all messed up. The most likely result is that the priority transfer to the acquiring thread is never removed (since it was cleaned up by the wrong thread). From an outsider’s point of view, it looks like the acquiring thread’s priority got permanently boosted for no reason.
The kernel folks didn’t say, but I suspect that another consequence is that any priority transfer to the releasing thread is removed prematurely, so it looks like the releasing thread’s priority got demoted for no reason.
All of this is implementation detail and not contractual. The contractual requirement is that you release the SRWLock from the same thread that acquired it. If you fail to follow this rule, then the behavior is undefined, and your program will behave erratically in unspecified ways.
So don’t do that. Follow the rules and nobody gets hurt.
¹ Priority inversion was famously the source of hangs on the Mars Pathfinder mission in 1997.
I find it inconvenient to be unable to unlock something from a different thread that locked it.
Most of the time, the restriction is no issue, but for some algorithms (mainly, constructing a more powerful lock from a primitive), it causes major issues.
I think SRWLock is high enough level that it shouldn't come up. But the bottom-end lock primitive is the one that matters.
Incidentally the documentation for LeaveCriticalSection is ambiguous. Does it really care that the...
If it works you are just getting lucky.
There are two scenarios;
Before Vista, only the checked build cares.
On newer versions the actual implementation does not follow what winnt.h tells you and it could be using a SRW etc.
“To release its ownership, the thread must call LeaveCriticalSection once for each time that it entered the critical section.” and “If a thread calls LeaveCriticalSection when it does not have ownership of the specified critical section object, an error occurs …” suggest that LeaveCriticalSection must be called by the owning thread.
In this case, what is a supported lock type usable in a coroutine? `std::mutex` is explicitly prohibited from being released from a different thread, SRWLock also cannot be used.
Is the only option left `WaitOnAddress` or the same priority inversion applies to it as well?
`std::counting_semaphore` / `std::binary_semaphore` explicitely allow being released from a different thread.