1

Can multiple threads safely read the same class member variable without creating a race condition?

class foo {
    int x;
};

void Thread1(foo* bar) {
    float j = bar->x * 5;
}

void Thread2(foo* bar) {
    float k = bar->x / 5;
}

So if for example, we have two threads running Thread1 and Thread2. If each thread is passed the same foo object, can they run independantly, without race conditions, because we are only reading the variable and not writing? Or is the act of accessing the object make this whole thing unsafe?

If the above is safe, can a third thread safely write to that same foo object as long as it doesn't touch foo::x?

#include <thread>

class foo {
public:
    int x = 1;
    int y = 1;
};

void Thread1(foo* bar) {
    int j;
    for (int i = 0; i < 1000; i++) {
        j = bar->x * 5;
    }
    printf("T1 - %i\n", j);
}

void Thread2(foo* bar) {
    int k;
    for (int i = 0; i < 1000; i++) {
        k = bar->x / 5;
    }
    printf("T2 - %i\n", k);
}

void Thread3(foo* bar) {
    for (int i = 0; i < 1000; i++) {
        bar->y += 3;
    }
    printf("T3 - %i\n", bar->y);
}

int main() {
    foo bar;

    std::thread t1(Thread1, &bar);
    std::thread t2(Thread2, &bar);
    std::thread t3(Thread3, &bar);

    t1.join();
    t2.join();
    t3.join();

    printf("x %i, y %i\n", bar.x, bar.y);

    return 0;
}
KKlouzal
  • 642
  • 5
  • 25
  • 2
    Yes, I strongly believe so. That, of course, includes that `foo::x` may not be written "indirectly" e.g. by overriding the `foo` completely somehow. – Scheff's Cat Dec 05 '19 at 08:56
  • 2
    You can read the same variable from as many threads as you like. Only writing requires synchronisation. – churill Dec 05 '19 at 08:59
  • 1
    The start of thread is a sufficient barrier. Read accesses to a certain address/range after start of thread (and before join) are safe and need not be guarded as long as nothing else writes to that specific address/range. – Scheff's Cat Dec 05 '19 at 08:59
  • Reading an aligned int is of atomic operation by default in most processors today, so it won't do harm, but still in C++ expressing it in a more explicit way using `std::atomic` with a corresponding memory fence parameter would be appreciated. – Dean Seo Dec 05 '19 at 09:19
  • 1
    @DeanSeo what has it to do with an aligned int and atomic operation, if the only thing I do is reading? Even if it would be a large array, there would be no race condition, if there is no writng at all? – RoQuOTriX Dec 05 '19 at 09:23
  • @RoQuOTriX You can read as much as you want in parallel, that will never be a race condition (as long as the data you read is not written to without synchronization). – Max Langhof Dec 05 '19 at 09:50
  • @DeanSeo No, it's not "appreciated", it is required to use some form of synchronization if there would be a race otherwise. Compiler optimizations may break your code otherwise, which is why data races are UB, period. – Max Langhof Dec 05 '19 at 09:51
  • @MaxLanghof _"Compiler optimizations may break your code otherwise,"_ How does compiler optimizations break **read-only** aligned int to the world of race conditions? In that sense I used the term _"appreciated"_ – Dean Seo Dec 05 '19 at 09:56
  • @DeanSeo Ok, then I misunderstood what you were trying to say. It wasn't clear to me whether you actually talked about "reading in read-only context" or "reading with concurrent writes" (see current answer). Synchronization is useless in the first case and required in the second one. Thus, I don't see how "aligned accesses are atomic on hardware level" matters - you shouldn't go make all your primitives an `atomic` because it "would be appreciated". You should synchronize exactly where needed, not sprinkle it on to taste... – Max Langhof Dec 05 '19 at 10:02
  • @MaxLanghof Oh I see now. Yeah, I was sticking to the OP's statement where _"there's no writes introduced"_, (and still using `std::atomic` is more explicit than just `int` with a proper memory fence). _"Synchronization is useless in the first case and required in the second one."_ Yes, you're totally right on that. I agree. – Dean Seo Dec 05 '19 at 10:08

1 Answers1

6

Can multiple threads safely read the same class member variable without creating a race condition?

Yes and no.

Yes - The code you provided will not cause race condition, because race condition might occur when you have at least 2 threads working on the same shared resource, and at least one of those threads is writing to that resource.

No - your code isn't considered to be thread-safe, as it exposes x and y members for both read and write, and makes it possible (for you or other programmers that use your code) to cause a race condition. You rely on your knowledge (which you might forget over time) that x should only be read, and not written to, and that y should only be written by a single thread. You should enforce this by creating mutual exclusion in the critical code sections.

If you want the threads to only read from x and y, you should make this class immutable.

SubMachine
  • 440
  • 3
  • 9
  • For the last sentence: Or use `std::atomic` – Mike van Dyke Dec 05 '19 at 09:29
  • @MikevanDyke std::atomic is just another C++ tool to create mutual exclusion – SubMachine Dec 05 '19 at 09:32
  • 1
    @SubMachine Yes, atomics can be _used_ to create mutual exclusion and critical code sections, but using an atomic variable does not in itself _imply_ either of those. Mike is right - using an atomic is a valid alternative and probably preferred, as mutexes/critical sections tend to be orders of magnitude more expensive. – Max Langhof Dec 05 '19 at 09:37
  • I like your second part. This could be achieved e.g. by exposing the member `foo::x` as const reference to threads (or just as copied value). What I find more difficult to model in C++: data which is immutable just for the life-time of threads. – Scheff's Cat Dec 05 '19 at 09:38
  • A critical section is something which may cause additional performance impact. Hence, I understand that OP carefully asks how much safety is really necessary. (This is under the general assumption that C++ has been chosen because you can fine-grained manage what to use where to gain maximum performance. It's definitely a wrong choice to write bullet-proof multi-threading code as fast as possible.) ;-) – Scheff's Cat Dec 05 '19 at 09:46