0

Slim Reader/Writer (SRW) Locks is a synchronization primitive in Windows, available starting from Windows Vista.

Name and interface suggests that it should be used as non-timed shared non-recursive mutex. However, it is common to use it as non-shared mutex as well, to avoid CRTICAL_SECTION overhead (by using only Exclusive APIs).

I've noticed that it works also as a binary semaphore. This can come handy, as other semaphores available in Windows APIs - Event object and Semaphore object - are always a kernel call, so it is probably the only lightweight semaphore readily available from Windows API (and C++ has semaphores starting C++20, and boost thread also does not provide semaphores).

But is this reliable? Specifically, I have not found in the documentation explicit information that it can be used this way.

But, I have not found anything that prohibits this usage. The documentation seems to be uncertain.

What I'm expecting as an answer:

  • Maybe someone can point me to documentation wording that permits or prohibits semaphore usage
  • Maybe there's some practical experience with such usage
  • Maybe someone directly involved with SRW lock implementation could clarify (there's some chance, I think)

Example - this does not hang

#include <Windows.h>
#include <atomic>


SRWLOCK lock = SRWLOCK_INIT;

std::atomic<bool> can_release{ false };

DWORD CALLBACK Thread1(LPVOID)
{
    for (int i = 0; i < 3; i++)
    {
        while (!can_release)
        {
            // spin
        }
        can_release = false;
        ::ReleaseSRWLockExclusive(&lock);
    }

    return 0;
}


DWORD CALLBACK Thread2(LPVOID)
{
    for (int i = 0; i < 3; i++)
    {
        can_release = true;
        ::AcquireSRWLockExclusive(&lock);
    }

    return 0;
}

int main() {
    ::AcquireSRWLockExclusive(&lock);

    HANDLE h1 = ::CreateThread(nullptr, 0, Thread1, nullptr, 0, nullptr);
    HANDLE h2 = ::CreateThread(nullptr, 0, Thread2, nullptr, 0, nullptr);

    ::WaitForSingleObject(h1, INFINITE);
    ::WaitForSingleObject(h2, INFINITE);

    ::CloseHandle(h1);
    ::CloseHandle(h2);
    
    return 0;
}
Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
  • 1
    *works also as a binary semaphore.* - what you mean under this ? say code example – RbMm Jun 27 '20 at 19:41
  • @RbMm, I've added an example. Mostly I mean that `AcquireSRWLockExclusive` and `ReleaseSRWLockExclusive` can be called from arbirary threads, as long as there are no `ReleaseSRWLockExclusive` that does not match existing `AcquireSRWLockExclusive` – Alex Guteniev Jun 27 '20 at 19:55
  • at first you need `can_release = true;` after `::AcquireSRWLockExclusive(&lock);` but not before. if by code sense, what you try to do ? transfer ownership of SRW from one thread to another ? call `AcquireSRWLockExclusive(&lock)` in some thread and do match `AcquireSRWLockExclusive` from another ? yes, this possible, despite unusual – RbMm Jun 27 '20 at 20:08
  • No, if I put `can_release = true` after acquire, the program is likely to hang (and really hangs for me). initially the lock is acquired in main, then I release it in one thread, to do matching acquire in the other, this is the idea: other thread's acquire will wait for this thread's release. I'm questioning this, since you can do the same with Semahore or Event, but not with Mutex or Critical Section. And for all other mentioned objects the possibility or impossibility is documented. – Alex Guteniev Jun 27 '20 at 20:13
  • sorry, i not note initial call to `AcquireSRWLockExclusive` in *main*, but until can not understand sense of what you try do. – RbMm Jun 27 '20 at 20:17
  • Say there's a producer-consumer queue with "queue full" and "queue empty" events, in case of "queue full" producer waits on event, then consumer can set the event to wake producer. I want the same with SRW locks instead of events, since they expected to spin in user mode and avoid kernel calls. – Alex Guteniev Jun 27 '20 at 20:23
  • for this case exist [Condition Variables](https://learn.microsoft.com/en-us/windows/win32/sync/using-condition-variables) - this example exactly for producer/consumer case. you can replace here `SleepConditionVariableCS` for `SleepConditionVariableSRW`. what I don't like right away in your code - `can_release = true; /* inset Sleep or MessageBox here*/AcquireSRWLockExclusive(&lock);` this 2 lines not atomic operation - however need look for real code produces/consumer for rate it. and not understand why you so not like go to kernel - for which real code all this ? – RbMm Jun 27 '20 at 20:38
  • Condition variable will do as well, but there are caveats with lost wakeup, plus I want to avoid even too many atomic operations for the best case. Not going to kernel is good if the wait is actually very short. The code works fine with message box added there or into any other place. – Alex Guteniev Jun 27 '20 at 20:49
  • but this is not real code for producer-consumer. for rate it and give more concrete answer - need real code look – RbMm Jun 27 '20 at 21:12
  • 1
    Only the thread which acquired may release. Breaking this rule invokes undefined behavior. This is enforced by application verifier. If you just want a lightweight semaphore, you can use WaitOnAddress. – Raymond Chen Jun 27 '20 at 23:41
  • @RaymondChen, thanks. AppVerifier is enough proof that SRW lock does not work this way. `WaitOnAddress` works great, I'm already using it, but I was looking for Windows 7 fallback options. Event object or Semaphore object accompanied by an atomics seem to be a solution, but it is some effort to implement a lightweight semaphore out of them correctly and efficiently. – Alex Guteniev Jun 28 '20 at 04:00
  • @RbMm, it is some effort to recreate something close to my production code. (And I can't share production code directly due to licensing). But there's functionally similar in this regard open-source queue: https://github.com/cameron314/readerwriterqueue . It uses lightweight semaphore borrowed from here: https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h . This is also an option for me to take this or another open-source 3rd party semaphore, or to implement my own, but I wanted WinAPI solution, if it was available. – Alex Guteniev Jun 28 '20 at 04:27
  • @RaymondChen this is not true. srw lock not maintain information about thread which acquire lock. formally we can transfer "ownership" - acquire in one thread and then release in another. despite this is very unusual. nothing prevent from this, if acquire/release match. if application verifier raise here - only because he remeber thread which acquire lock. but in release environment app not run under verifier – RbMm Jun 28 '20 at 11:38
  • and `WaitOnAddress` -strong doubt that is good solution. this api anyway internal use SRW (same as pushlock) functional with predefined *WaitOnAddressHashTable* in *PEB* (concrete *SRW* block selected based on address "hash" (current `(p >> 5) & 0x7f`) wait block unconditionally inserted, even if not need.. `SleepConditionVariableSRW` faster will be better – RbMm Jun 28 '20 at 12:49
  • @RbMm Releasing from a different thread is outside the design contract. You may get away with it given the current implementation, but it is not contractual behavior and may stop working in the future. Application Verifier enforces the contract. The affinity for threads is [in the documentation](https://learn.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks), but perhaps not called out clearly enough: It says that *threads* acquire and release the lock. There are "reader threads" and "writer threads". Ownership of the lock is thread-based, not activity-based. – Raymond Chen Jun 28 '20 at 18:41
  • @RaymondChen - really only Every call to Acquire must be matched by a subsequent call to Release. are this will be from the same or different thread - no matter. and about contract - some things can be used in unusual, not designed from begin way. about "may stop working in the future" - very strong doubt in this. think you too. simply because this is by design very small (and public declared) structure and it not maintain information of thread which acquire it (even in exclusive mode). because this reqursive acquire also impossible (unlike CS) – RbMm Jun 28 '20 at 18:58
  • another case that i not view here, how srw lock can be used correct here in such way, but formal this is possible. say acquire then create new thread(or signal to existing) which call release – RbMm Jun 28 '20 at 19:02
  • main problem here that release need be called strictly after acquire. not before. unlike say SetEvent and Wait on it can be done in any order – RbMm Jun 28 '20 at 19:09
  • 1
    It is much better practice to code to the contract, rather than coding to the implementation. – Raymond Chen Jun 28 '20 at 21:16
  • contract also frequently not ideal or not the best documented. already not say about possible error in implementations of os some time. and use all only in standard, documented way, or sometime use unusual solution..this already not only programming question – RbMm Jun 29 '20 at 17:06

1 Answers1

0

@Raymond Chen is right. Application Verifier reports errors for the code in question:

The code in question produces this error:

=======================================
VERIFIER STOP 0000000000000255: pid 0x1A44: The SRW lock being released was not acquired by this thread. 

    00007FF73979C170 : SRW Lock
    00000000000025CC : Current ThreadId.
    00000000000043F4 : ThreadId of the thread that acquired the SRW lock.
    000001C1BEA8BF40 : Address of the acquire stack trace. Use dps <address> to see where the SRW lock was acquired.


=======================================
This verifier stop is continuable.
After debugging it use `go' to continue.

=======================================



=======================================
VERIFIER STOP 0000000000000253: pid 0x1A44: The SRW lock is being acquired recursively by the same thread. 

    00007FF73979C170 : SRW Lock
    000001C1BEA8BF40 : Address of the first acquire stack trace. Use dps <address> to see where the SRW lock was acquired.
    0000000000000000 : Not used
    0000000000000000 : Not used


=======================================
This verifier stop is continuable.
After debugging it use `go' to continue.

=======================================

As of now, the documentation also explicitly prohibits releasing in a different thread, see ReleaseSRWLockExclusive, ReleaseSRWLockShared:

The SRW lock must be released by the same thread that acquired it. You can use Application Verifier to help verify that your program uses SRW locks correctly (enable Locks checker from Basic group).

Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
  • really that verifier show error here not meant that another thread can not release lock. this can be done formally from any thread, until acquire/release match and in order – RbMm Jun 28 '20 at 11:40
  • AppVerifier disagrees. Even if I just once acquire a lock in one thread and release it in another. It says precisely _The SRW lock being released was not acquired by this thread._, so it does not want to release a lock acquired by **not this** thread. This is enough proof that there was not an intention for SRW Lock to support this scenario even though it is implemented accidentally or for internal use – Alex Guteniev Jun 29 '20 at 16:49
  • and so what ? this is nothing prove. *AppVerifier* remember information which lock asquire and release thread. it just design for say this is error, because this is very unusual. but i very good understand how srw worked internal. and because this i know that this is possible - release lock from another thread. and in production code - you not run under AppVerifier. another case - i anyway not view how you can use srw locks for your task – RbMm Jun 29 '20 at 16:57