Home > other >  C how to make a series of statements atomic?
C how to make a series of statements atomic?

Time:07-03

I have a program which have multiple threads running. Inside the main thread, a class variable maybe changed by operations from different threads. So I will like to make sure that within a series of steps involving the variable, no other threads may change the variable midway through that series of steps. E.g.

this->a = b / c;
if( this->a < d)
{
    this->e = 7;
}

In the above code, how do I make sure no interruptions from other thread occur E.g. thread 2

int j = 7 / 2
this->a *= j

Both threads tried to change the value of "a". How can I make sure that after this->a = b / c, the if(this->a < d) block will execute immediately before the this->a *= j statement has a chance to execute in thread 2? Thanks!

CodePudding user response:

From the comments, it seems your view of a mutex is that it serves to protect a single block of code, so that no two threads can execute it simultaneously. But that is too narrow a view.

What a mutex really does is ensure that no two threads can have it locked simultaneously. So you can have multiple blocks of code "protected" by the same mutex; then, whenever one thread is executing one of those blocks, no other thread can execute that block nor any of the other protected blocks.

This leads to the idiom of having a mutex that protects a variable (or set of variables); meaning, you set a rule for yourself that every block of code accessing those variables must be executed with the mutex locked.

A simple example (untested) could be:

class foo {
public:
    void f1();
    void f2();
private:
    int a, e;
    std::mutex m;
};

void foo::f1() {
    m.lock();
    this->a = b / c;
    if( this->a < d) {
        this->e = 7;
    }
    m.unlock();
}

void foo::f2() {
    m.lock();
    int j = 7 / 2;
    this->a *= j;
    m.unlock();
}

Now other threads can safely execute any combination of f1 and f2 in parallel without causing a data race or risking an inconsistent view of the data.

In practice, take advantage of RAII by using something like std::scoped_lock instead:

void foo::f1() {
    std::scoped_lock sl(m);
    this->a = b / c;
    if( this->a < d) {
        this->e = 7;
    }
}

Among other benefits (that would become relevant in more complex code), it automatically unlocks the mutex for you at the end of the block, so you don't have to remember to do it manually.

CodePudding user response:

Here's an example using a std::lock_guard on a std::mutex. The point of the lock guard is to benefit from RAII so that the piece of code you lock is automatically released when the guard goes out of scope and is destroyed.

[Demo]

#include <chrono>  // ms
#include <fmt/core.h>
#include <future>  // async
#include <mutex>
#include <thread>  // lock_guard, sleep_for

std::mutex m{};

struct S {
    int a{};
    int e{};
    void work(int b, int c, int d) {
        using namespace std::chrono_literals;
        std::lock_guard<std::mutex> lg{m};
        fmt::print("Calling work with b={}, c={}, and d={}\n", b, c, d);
        a = b / c;
        std::this_thread::sleep_for(500ms);
        if (a < d) {
            e = 7;
        }
        fmt::print("S: a={}, e={}\n", a, e);
    }
};

int main() {
    S s{7, 15};
    auto fut1 = std::async(std::launch::async, [&s]() { s.work(20, 30, 0); });
    auto fut2 = std::async(std::launch::async, [&s]() { s.work(50, 6, 10); });
    fut1.get();
    fut2.get();
}

// Possible output:
//
//   Calling work with b=20, c=30, and d=0
//   S: a=0, e=15
//   Calling work with b=50, c=6, and d=10
//   S: a=8, e=7

And here's what can happen if you don't use any synchronization at all.

[Demo]

...
        //std::lock_guard<std::mutex> lg{m};
...

// Possible output:
//
//   Calling work with b=20, c=30, and d=0
//   Calling work with b=50, c=6, and d=10
//   S: a=8, e=15
//   S: a=8, e=7
  • Related