C++ Logo

std-proposals

Advanced search

Re: [std-proposals] unique_lock<atomic_flag>

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Thu, 23 Feb 2023 08:39:16 +0000
On Thu, Feb 23, 2023 at 1:30 AM Jason McKesson via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> A Lockable type has a built-in assumption: if your thread locks it,
> only your thread can unlike it. This allows a scoped lock type like
> `unique_lock` to offer the guarantee that if your thread's stack holds
> that lock, then only the termination of that scope can unlock it.
>
> A binary_semaphore offers *no such guarantee*. If you can access a
> binary_semaphore object, you can *unlock it*. Even if some other
> thread locked it, you can unlock it for them. It's not a mutex. In
> fact, not being a mutex is the *point of the type*.
>
> This is also why `atomic`s are not, and should not be, Lockable. They
> don't participate in the fundamental assumption of ownership of locks.


Sometimes I want a mutex that can be unlocked by a thread other than
the one that locked it. I'll give you an example.

I needed to write a desktop PC program that would work as a 'man in
the middle' between a microscope and a piece of software that controls
the microscope. I have one thread that is reading and writing to a COM
port that looks like this:

void Thread_Entry_Point(....)
{
    for (;;)
    {
        Comms.Read( . . . );
        ProcessPacket( . . . );
        Comms.Write( . . . );
    }
}

I have several worker threads which at any time can manually insert a
packet into the COM port, and so the COM port thread actually looks
more like this:

void Thread_Entry_Point(....)
{
    string str;

    for (;;)
    {
        str = Get_Manual_Packet();

        if ( str.empty() ) Comms.Read( . . . );
        else Comms.Set_Read_Buffer(str.cbegin(), str.cend());

        ProcessPacket( . . . );
        Comms.Write( . . . );
    }
}

I devised a two-stage lock to work as follows:

(Step 1) A worker thread, in order to manually insert a packet, must
acquire both the primary lock and the secondary lock. When the worker
thread is finished putting the packet into a global buffer, it then
releases the secondary lock.
(Step 2) The COM port thread invokes a function called
"Get_Manual_Packet" to acquire the secondary lock. Once it has
acquired the secondary lock, it processes the packet and sends it out
the COM port, and about 20 milliseconds later it reads in the reply.
(Step 3) Once the COM port thread has received the reply, it releases
the secondary lock, and also releases the primary lock.
(Step 4) With both the primary and secondary lock now free, another
worker thread can acquire both of them and manually insert another
packet.

What made this a little bit complicated is the following:
Complication 1) In Step 1, the worker thread acquiring the primary
lock might be the most recent thread to have acquired the primary
lock, and so it could try to double-lock a mutex.
Complication 2) In Step 3, the COM port thread releases the primary
lock even though it was a worker thread that acquired the lock.

In order to get around both Complication 1 and Complication 2, I
changed the primary lock from an "std::mutex" to a "std::atomic_flag".

Here's the code I wrote. By the way do I realise that there's a very
minor race condition in the setting of "id_owner1" but that's only in
Debug mode. The Comms thread calls the methods "lock_weak,
unlock_weak", and the worker threads call "lock_strong,
unlock_strong". The primary lock is 'device1' and the secondary lock
is 'device2':

        https://godbolt.org/z/e9qs1dbs3

I'm thinking though that I'll replace the atomic_flag with a binary_semaphore.

Received on 2023-02-23 08:39:28