C++ Logo

std-proposals

Advanced search

[std-proposals] Standard library should have a simple 'gate' for multithreading

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Fri, 22 Jul 2022 00:38:16 +0100
Back when I started learning about multi-threaded programming, I
started off with the basics like using a lock (such as a "std::mutex"
or a "wxCriticalSection") when accessing global data which was
accessed by more than one thread.

>From there I learned about atomic variables, and later I moved on to
learning how to get threads to cooperate, i.e. get one thread to sleep
and do nothing until another thread gives it a nod to wake up and
continue.

For the past few years it has always bewildered me that the C++
standard library is missing a very simple class for a 'gate' used by
threads to tell each other when to sleep and when to continue.

The reusable code I have for a 'gate' class (which I use in my own
programs) starts off with the gate closed, and it has a very simple
interface:
(1) you can open it
(2) you can close it
(3) you can wait for it to open
(4) you can wait for it to close

So two threads would use it as follows:

#include <thread> // thread, this::sleep_for
#include <iostream> // cout, endl
#include <chrono> // milliseconds
using std::cout;
using std::endl;

Gate g_gate;

void Thread_Entry_Point(void); /* defined below 'main' */

int main(void)
{
    std::thread t( Thread_Entry_Point );

    for (; /* ever */ ;)
    {
        /* ====== We start off with just one thread working ========= */
        cout << "caterpillar\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );
        cout << "caterpillar\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );
        cout << "caterpillar\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );

        /* ====== Next we have two threads working in parallel ====== */
        g_gate.open();

        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "monkey\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );

        /* ====== Next we go back to just one thread working ======== */
        g_gate.wait_for_close();

        cout << "dolphin\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );
        cout << "dolphin\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );
        cout << "dolphin\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(500u) );
    }
}

void Thread_Entry_Point(void)
{
    for (; /* ever */ ;)
    {
        g_gate.wait_for_open();

        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );
        cout << "ape\n";
        std::this_thread::sleep_for( std::chrono::milliseconds(250u) );

        g_gate.close();
    }
}

Back when I was just beginning to learn about this kind of "stop and
go" cooperation between threads, I learned about condition variables,
and also spurious wake-up. It all seemed very complicated for such a
small concept as a 'gate'. So I think there should be a very simple
'gate' class in the standard library, something like as follows:

#include <mutex> // mutex, unique_lock
#include <condition_variable> // condition_variable

class Gate {
private:

    std::mutex m;
    std::condition_variable cv;
    bool is_gate_open = false;

public:

    void open(void)
    {
        m.lock();
        is_gate_open = true;
        m.unlock();
        cv.notify_all();
    }

    void close(void)
    {
        m.lock();
        is_gate_open = false;
        m.unlock();
        cv.notify_all();
    }

    void wait_for_open(void)
    {
        std::unique_lock<std::mutex> lock(m);
        while ( false == is_gate_open ) cv.wait(lock);
    }

    void wait_for_close(void)
    {
        std::unique_lock<std::mutex> lock(m);
        while ( is_gate_open ) cv.wait(lock);
    }
};

Since we now have "jthread" which is a more simple thread class, it
will also help to have a 'gate'.

Received on 2022-07-21 23:38:28