1

How can I check bool variable in class considering thread safe?

For example in my code:

// test.h

class Test {
    void threadFunc_run();
    void change(bool _set) { m_flag = _set; }

    ...
    bool m_flag;
};


// test.cpp

void Test::threadFunc_run()
{
    // called "Playing"
    while(m_flag == true) {
        for(int i = 0; i < 99999999 && m_flag; i++) {
            // do something .. 1
        }

        for(int i = 0; i < 111111111 && m_flag; i++) {
            // do something .. 2
        }
    }
}

I wan to stop "Playing" as soon as change(..) function is executed in the external code.

It also wants to be valid in process of operating the for statement.

According to the search, there are variables for recognizing immediate changes, such as atomic or volatile.

If not immediately, is there a better way to use a normal bool?

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
mystes
  • 59
  • 7
  • 4
    Either make `m_flag` atomic (e.g. `std::atomic_bool` or `std::atomic_flag`), or protect it with a mutex or similar. The code you show exhibits undefined behavior by way of a data race. – Igor Tandetnik Jul 10 '22 at 14:19
  • 4
    `volatile` is not for threading, it's for disabling optimizations. `std::atomic` is a better route. Or `std::atomic_flag`. – Eljay Jul 10 '22 at 14:20
  • 4
    It is not meaningfully possible to learn C++ by doing one "search" after another. C++ is the most complicated and the hardest to learn general purpose programming language in use today. The only way to do that is with a C++ textbook which will explain to you what mutexes and condition variables are, and how to use them. Google is not a replacement for a C++ textbook, and in this case the search results will be filled with meaningless ramblings about chasing the elusive lock-free unicorn fairy. – Sam Varshavchik Jul 10 '22 at 14:20
  • Unrelated: `while(m_flag == true)` is better written as `while(m_flag)`. – Evg Jul 10 '22 at 14:23
  • First of all, thank you for your deep advice. I considered the atomic_flag, but I thought it was not suitable for my code because it was impossible to simply check the status. – mystes Jul 10 '22 at 14:25
  • 2
    @mystes [`std::atomic`](https://en.cppreference.com/w/cpp/atomic/atomic) is a bit easier to handle than `std::atomic_flag` and very likely sufficient for your purpose. – user17732522 Jul 10 '22 at 14:27
  • 2
    Atomic variables do not guarantee proper thread synchronization. Relying only on atomic variables, for that, always ends in tears. A lot more work needs to be done to do this correctly, and for simple use cases traditional mutexes and condition variables are simpler to use, and are perfectly sufficient. But, most people cannot be convinced that neither Google, nor Stackoverflow, are proper replacements for a C++ textbook. That's something they'll always end up discovering by themselves. – Sam Varshavchik Jul 10 '22 at 14:29
  • 3
    @sam atomic variables guarantee proper thread synchronization for the atomic variable, especially using the default memory order. Which is sufficient in OP's example. – JohnFilleau Jul 10 '22 at 14:42
  • 1
    It is a near certainty, @JohnFilleau, that all those ellipsis are covering up stuff that needs to be synchronized across threads. That's the next question I expect to see, from a typical [Google Programmer](https://it.slashdot.org/story/22/06/25/1745219/are-google-programmers-the-new-next-next-finish-programmers). And then the answer will be to use mutexes and condition variables. Why take such a roundabout way to reach the same conclusion? – Sam Varshavchik Jul 10 '22 at 14:54
  • 1
    Maybe, but I'm commenting on what I can see. Not on what I can't. And the statement *"Atomic variables do not guarantee proper thread synchronization."* is wrong. – JohnFilleau Jul 10 '22 at 14:57
  • @SamVarshavchik: An `atomic` seems appropriate for a `keep_running` or `exit_now` flag. You clear or set it, then (when you need to know the thread has fully exited), you `.join()` it. The `join()` creates happens-before synchronization, but the atomic store makes the thread notice that you want it to stop. You don't need *that* to be ordered wrt. other operations. Being `atomic` guarantees that it will be seen promptly, real implementations having inter-thread latency usually well below 100 ns. (So if the other thread checks the flag any time after that, it'll see the new value.) – Peter Cordes Jul 10 '22 at 21:46
  • If you *just* need a `stop` flag without other synchronization, `std::atomic` is the way to go. See [Why set the stop flag using \`memory\_order\_seq\_cst\`, if you check it with \`memory\_order\_relaxed\`?](https://stackoverflow.com/q/70581645) for details on the required memory-ordering. (`relaxed` is sufficient). – Peter Cordes Jul 10 '22 at 22:02
  • 1
    @SunderamDubey - such minor edits are discouraged, I think. Fixing a typo is ok, but your changes aren't enough on their own to be worth an edit that bumps the question as recently active. Both changes were improvements, but IMO not significant enough to be worth it unless there's something else to improve at the same time. A space before a ? and a `;` instead of `:` are very minor things. For me, I'd only consider editing for that if the question was already new or just edited, or if I had some other change to make, like tags. (e.g. adding [stdatomic] but this Q seems to avoid that.) – Peter Cordes Aug 09 '22 at 08:37

1 Answers1

1

Actually synchronizing threads safely requires more then a bool. You will need a state, a mutex and a condition variable like this. The approach also allows for quick reaction to stop from within the loop.

#include <chrono>
#include <condition_variable>
#include <iostream>
#include <future>
#include <mutex>

class Test
{
private:
    // having just a bool to check the state of your thread is NOT enough. 
    // your thread will have some intermediate states as well
    enum play_state_t
    {
        idle,           // initial state, not started yet (not scheduled by OS threadscheduler yet)
        playing,        // running and doing work
        stopping,       // request for stop is issued
        stopped         // thread has stopped (could also be checked by std::future synchronization).
    };

public:
    void play()
    {
        // start the play loop, the lambda is not guaranteed to have started
        // after the call returns (depends on threadscheduling of the underlying OS)
        // I use std::async since that has far superior synchronization with the calling thead
        // the returned future can be used to pass both values & exceptions back to it.
        m_play_future = std::async(std::launch::async, [this]
        {
            // give a signal the asynchronous function has really started
            set_state(play_state_t::playing);
            std::cout << "play started\n";

            // as long as state is playing keep doing the work
            while (get_state() == play_state_t::playing)
            {
                // loop to show we can break fast out of it when stop is called
                for (std::size_t i = 0; (i < 100l) && (get_state() == play_state_t::playing); ++i)
                {
                    std::cout << ".";
                    std::this_thread::sleep_for(std::chrono::milliseconds(200));
                }
            }

            set_state(play_state_t::stopped);
            std::cout << "play stopped.\n";
        });

        // avoid race conditions really wait for
        // trhead handling async to have started playing
        wait_for_state(play_state_t::playing);
    }

    void stop()
    {
        std::unique_lock<std::mutex> lock{ m_mtx }; // only wait on condition variable in lock
        if (m_state == play_state_t::playing)
        {
            std::cout << "\nrequest stop.\n";
            m_state = play_state_t::stopping;
            m_cv.wait(lock, [&] { return m_state == play_state_t::stopped; });
        }
    };

    ~Test()
    {
        stop();
    }

private:

    void set_state(const play_state_t state)
    {
        std::unique_lock<std::mutex> lock{ m_mtx }; // only wait on condition variable in lock
        m_state = state;
        m_cv.notify_all();              // let other threads that are wating on condition variable wakeup to check new state
    }

    play_state_t get_state() const
    {
        std::unique_lock<std::mutex> lock{ m_mtx }; // only wait on condition variable in lock
        return m_state;
    }

    void wait_for_state(const play_state_t state)
    {
        std::unique_lock<std::mutex> lock{ m_mtx };
        m_cv.wait(lock, [&] { return m_state == state; });
    }

    // for more info on condition variables 
    // see : https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables

    mutable std::mutex m_mtx;
    std::condition_variable m_cv;       // a condition variable is not really a variable more a signal to threads to wakeup
    play_state_t m_state{ play_state_t::idle };
    std::future<void> m_play_future;
};


int main()
{
    Test test;
    test.play();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    test.stop();

    return 0;
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
  • 1
    In C++20, you probably *could* use `std::atomic` if you wanted, since C++20 adds `.wait()` and `.notify()` methods to expose the same kind of functionality as a condition variable. (OS-assisted sleep/wake while waiting for a value to change. Of course it waits for any different value, rather than for one specified value, but check in a wait loop is fine.) You might need `exchange` or `compare_exchange_weak` instead of `store` if multiple threads can be changing a variable. – Peter Cordes Jul 10 '22 at 22:07
  • Thank you very much for your reply. Unfortunately, I am currently working on the C++11 base. In fact, I understand that frequent use of mutex can cause overhead in operating hours. So I was reluctant to do that. but I will try to apply the "state_t" synchronization you suggested and test it! – mystes Jul 11 '22 at 00:57
  • I think C++11 should be able to run this (https://onlinegdb.com/AE7P1j_KW), maybe replace brace initialization of the classes with ( ) . std::mutex can cost you a bit of performance (e.g. inner loop in this example), however for performance you need to measure (not rely on gut feeling). The condition_variable/mutex/state pair will ensure that threads will not be scheduled if the condition variable is not triggered freeing up the CPU for other things (so no busy waits). – Pepijn Kramer Jul 11 '22 at 03:28