Date: Wed, 7 May 2025 10:36:10 +0300
In our internal read-write lock implementation this difference in behavior
is explicitly recognized, and users select the policy for each lock they
create - either reader-preferring, or writer-preferring. This was the
reason why we didn't switch to a standard lock or to a thin wrapper over
the platform-specific lock provided by the OS - we didn't want to lose this
control. Of course there is also the contrary view that code should be
written as if the locks are writer-preferring, in which case it will work
with reader-preferring locks too without blocking, but writers could be
severely delayed, which could break certain assumptions.
My personal opinion is that if you are in a situation where you are
justified in using a shared_mutex instead of a regular mutex, you have a
good understanding of the behavior of readers and writers in your program
and you are necessarily also interested in the relative priority of readers
and writers. In other words, if you don't care whether your shared_mutex
prefers readers or writers, you probably should just use a regular mutex
instead. Therefore I believe that shared_mutex should let the user select
the policy. But I also understand that the implied requirements on the
implementation could be a strong argument against it.
On Tue, Apr 29, 2025 at 7:24 PM Jennifier Burnett via Std-Discussion <
std-discussion_at_[hidden]> wrote:
> Looking up the behaviour for Window's SRW locks, and beyond finding that
> they similarly don't make any guarantees about the order of readers and
> writers I also found the following:
>
> https://devblogs.microsoft.com/oldnewthing/20170705-00/?p=96535
>
> The blog claims: "That said, SRW locks aren’t aggressively unfair either.
> If shared readers arrive when there is a waiting writer, they will queue up
> behind the writer, which generally avoids the problem of readers starving
> out a writer."
>
> I don't have immediate access to a windows machine so I can't test the
> current behaviour but if that blog is to be believed then Windows did at
> one point block readers for waiting writers.
>
> Either way it does look like the standard's wording is intentionally vague
> to cover all implementations of the OS primitives it's targeting
>
>
> On 29 April 2025 16:49:38 BST, Nate Eldredge via Std-Discussion <
> std-discussion_at_[hidden]> wrote:
>
>> At the very least, I don't see a clear unambiguous statement that the
>> program *cannot* block in this situation.
>>
>> It's worth noting that POSIX pthread rwlocks explicitly allow blocking in
>> this case (https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/):
>> "If the Thread Execution Scheduling option is not supported, it is
>> implementation-defined whether the calling thread acquires the lock when a
>> writer does not hold the lock and there are writers blocked on the lock."
>> (When Thread Execution Scheduling is supported, they give conditions under
>> which an attempt to lock for reading *must* block when a writer is already
>> blocked.) I think it's unlikely that the ISO C++ committee would have
>> deliberately imposed requirements that contradicted those of one of the
>> most prominent potential implementations, at least not without saying so
>> very clearly.
>>
>> The advantage of making the reader block is that it avoids a starvation
>> scenario, where so many readers are using the data that there is always at
>> least one reader holding the lock, even though each one needs it for only a
>> short time. Then a writer may be starved out indefinitely, unless the
>> implementation deliberately blocks new readers from taking the lock until
>> the writer gets a chance to hold it.
>>
>>
>> On Apr 29, 2025, at 08:30, Lénárd Szolnoki via Std-Discussion <
>> std-discussion_at_[hidden]> wrote:
>>
>> Hi,
>>
>> Consider the following program:
>>
>> #include <shared_mutex>
>> #include <mutex>
>> #include <thread>
>> #include <barrier>
>> #include <iostream>
>>
>> int main() {
>> std::shared_mutex smtx;
>> std::barrier b(2);
>> auto reader_1 = std::jthread([&]{
>> auto lock = std::shared_lock{smtx};
>> std::cout << "shared lock acquired\n";
>> b.arrive_and_wait();
>> });
>>
>> std::this_thread::sleep_for(std::chrono::seconds(1));
>>
>> auto writer = std::jthread([&]{
>> auto lock = std::unique_lock{smtx};
>> std::cout << "unique lock acquired\n";
>> });
>>
>> std::this_thread::sleep_for(std::chrono::seconds(1));
>>
>> auto reader_2 = std::jthread([&]{
>> auto lock = std::shared_lock{smtx};
>> std::cout << "shared lock acquired\n";
>> b.arrive_and_wait();
>> });
>> }
>>
>> Is this program guaranteed to terminate, assuming the implementation
>> provides concurrent progress guarantees? The behaviour is different for
>> libstdc++ and libc++:
>>
>> https://godbolt.org/z/vzdYnvGPr
>>
>> Seemingly libc++ gets deadlocked in the following configuration:
>> * reader_1 is blocked on b.arrive_and_wait()
>> * writer is blocked on smtx.lock()
>> * reader_2 is blocked on smtx.lock_shared()
>>
>> I think it's questionable whether the standard allows smtx.lock_shared()
>> to block while no thread holds exclusive ownership over smtx.
>>
>> Relevant wording:
>>
>>
>> https://eel.is/c++draft/thread.mutex#thread.sharedmutex.requirements.general-5
>>
>> "Blocks the calling thread until shared ownership of the mutex can be
>> obtained for the calling thread."
>>
>> My reading of "shared ownership of the mutex can be obtained": if shared
>> ownership would be acquired for the current execution agent, then the
>> condition described in the paragraph
>> [thread.sharedmutex.requirements.general]/2 would hold.
>>
>>
>> https://eel.is/c++draft/thread.mutex#thread.sharedmutex.requirements.general-2
>>
>> "Multiple execution agents can simultaneously hold a shared lock
>> ownership of a shared mutex type. But no execution agent holds a shared
>> lock while another execution agent holds an exclusive lock on the same
>> shared mutex type, and vice-versa."
>>
>> This suggests that if there is no exclusive lock on smtx then
>> smtx.lock_shared() in reader_2 does not block.
>>
>> Is my reading correct?
>>
>> Is this a bug in libc++ or a defect in the standard? Notably the the
>> reference implementation in N2406 also has the same blocking behaviour.
>>
>> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html
>>
>> Cheers,
>> Lénárd
>> --
>> Std-Discussion mailing list
>> Std-Discussion_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>>
>>
>> --
> Std-Discussion mailing list
> Std-Discussion_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>
is explicitly recognized, and users select the policy for each lock they
create - either reader-preferring, or writer-preferring. This was the
reason why we didn't switch to a standard lock or to a thin wrapper over
the platform-specific lock provided by the OS - we didn't want to lose this
control. Of course there is also the contrary view that code should be
written as if the locks are writer-preferring, in which case it will work
with reader-preferring locks too without blocking, but writers could be
severely delayed, which could break certain assumptions.
My personal opinion is that if you are in a situation where you are
justified in using a shared_mutex instead of a regular mutex, you have a
good understanding of the behavior of readers and writers in your program
and you are necessarily also interested in the relative priority of readers
and writers. In other words, if you don't care whether your shared_mutex
prefers readers or writers, you probably should just use a regular mutex
instead. Therefore I believe that shared_mutex should let the user select
the policy. But I also understand that the implied requirements on the
implementation could be a strong argument against it.
On Tue, Apr 29, 2025 at 7:24 PM Jennifier Burnett via Std-Discussion <
std-discussion_at_[hidden]> wrote:
> Looking up the behaviour for Window's SRW locks, and beyond finding that
> they similarly don't make any guarantees about the order of readers and
> writers I also found the following:
>
> https://devblogs.microsoft.com/oldnewthing/20170705-00/?p=96535
>
> The blog claims: "That said, SRW locks aren’t aggressively unfair either.
> If shared readers arrive when there is a waiting writer, they will queue up
> behind the writer, which generally avoids the problem of readers starving
> out a writer."
>
> I don't have immediate access to a windows machine so I can't test the
> current behaviour but if that blog is to be believed then Windows did at
> one point block readers for waiting writers.
>
> Either way it does look like the standard's wording is intentionally vague
> to cover all implementations of the OS primitives it's targeting
>
>
> On 29 April 2025 16:49:38 BST, Nate Eldredge via Std-Discussion <
> std-discussion_at_[hidden]> wrote:
>
>> At the very least, I don't see a clear unambiguous statement that the
>> program *cannot* block in this situation.
>>
>> It's worth noting that POSIX pthread rwlocks explicitly allow blocking in
>> this case (https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/):
>> "If the Thread Execution Scheduling option is not supported, it is
>> implementation-defined whether the calling thread acquires the lock when a
>> writer does not hold the lock and there are writers blocked on the lock."
>> (When Thread Execution Scheduling is supported, they give conditions under
>> which an attempt to lock for reading *must* block when a writer is already
>> blocked.) I think it's unlikely that the ISO C++ committee would have
>> deliberately imposed requirements that contradicted those of one of the
>> most prominent potential implementations, at least not without saying so
>> very clearly.
>>
>> The advantage of making the reader block is that it avoids a starvation
>> scenario, where so many readers are using the data that there is always at
>> least one reader holding the lock, even though each one needs it for only a
>> short time. Then a writer may be starved out indefinitely, unless the
>> implementation deliberately blocks new readers from taking the lock until
>> the writer gets a chance to hold it.
>>
>>
>> On Apr 29, 2025, at 08:30, Lénárd Szolnoki via Std-Discussion <
>> std-discussion_at_[hidden]> wrote:
>>
>> Hi,
>>
>> Consider the following program:
>>
>> #include <shared_mutex>
>> #include <mutex>
>> #include <thread>
>> #include <barrier>
>> #include <iostream>
>>
>> int main() {
>> std::shared_mutex smtx;
>> std::barrier b(2);
>> auto reader_1 = std::jthread([&]{
>> auto lock = std::shared_lock{smtx};
>> std::cout << "shared lock acquired\n";
>> b.arrive_and_wait();
>> });
>>
>> std::this_thread::sleep_for(std::chrono::seconds(1));
>>
>> auto writer = std::jthread([&]{
>> auto lock = std::unique_lock{smtx};
>> std::cout << "unique lock acquired\n";
>> });
>>
>> std::this_thread::sleep_for(std::chrono::seconds(1));
>>
>> auto reader_2 = std::jthread([&]{
>> auto lock = std::shared_lock{smtx};
>> std::cout << "shared lock acquired\n";
>> b.arrive_and_wait();
>> });
>> }
>>
>> Is this program guaranteed to terminate, assuming the implementation
>> provides concurrent progress guarantees? The behaviour is different for
>> libstdc++ and libc++:
>>
>> https://godbolt.org/z/vzdYnvGPr
>>
>> Seemingly libc++ gets deadlocked in the following configuration:
>> * reader_1 is blocked on b.arrive_and_wait()
>> * writer is blocked on smtx.lock()
>> * reader_2 is blocked on smtx.lock_shared()
>>
>> I think it's questionable whether the standard allows smtx.lock_shared()
>> to block while no thread holds exclusive ownership over smtx.
>>
>> Relevant wording:
>>
>>
>> https://eel.is/c++draft/thread.mutex#thread.sharedmutex.requirements.general-5
>>
>> "Blocks the calling thread until shared ownership of the mutex can be
>> obtained for the calling thread."
>>
>> My reading of "shared ownership of the mutex can be obtained": if shared
>> ownership would be acquired for the current execution agent, then the
>> condition described in the paragraph
>> [thread.sharedmutex.requirements.general]/2 would hold.
>>
>>
>> https://eel.is/c++draft/thread.mutex#thread.sharedmutex.requirements.general-2
>>
>> "Multiple execution agents can simultaneously hold a shared lock
>> ownership of a shared mutex type. But no execution agent holds a shared
>> lock while another execution agent holds an exclusive lock on the same
>> shared mutex type, and vice-versa."
>>
>> This suggests that if there is no exclusive lock on smtx then
>> smtx.lock_shared() in reader_2 does not block.
>>
>> Is my reading correct?
>>
>> Is this a bug in libc++ or a defect in the standard? Notably the the
>> reference implementation in N2406 also has the same blocking behaviour.
>>
>> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html
>>
>> Cheers,
>> Lénárd
>> --
>> Std-Discussion mailing list
>> Std-Discussion_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>>
>>
>> --
> Std-Discussion mailing list
> Std-Discussion_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>
-- This electronic communication and the information and any files transmitted with it, or attached to it, are confidential and are intended solely for the use of the individual or entity to whom it is addressed and may contain information that is confidential, legally privileged, protected by privacy laws, or otherwise restricted from disclosure to anyone else. If you are not the intended recipient or the person responsible for delivering the e-mail to the intended recipient, you are hereby notified that any use, copying, distributing, dissemination, forwarding, printing, or copying of this e-mail is strictly prohibited. If you received this e-mail in error, please return the e-mail to the sender, delete it from your computer, and destroy any printed copy of it.
Received on 2025-05-07 07:36:02