2

I understand in C++ for preventing data race in multithreading environment, we could add a mutex in a class.

But, if there is a simple class as below, which only has a get() method, do we still need consider the issue of thread safety?

class SimpleClass {
public:
  SimpleClass(int val) : v(val) {};
  int get() { return v; }
private:
  int v;
};
ROBOT AI
  • 1,217
  • 3
  • 16
  • 27
  • Not as long as once you construct a `SimpleClass` no other methods of the class can mutate `v` as a side-effect. – Cory Kramer Jan 01 '17 at 14:32
  • 2
    A data race requires a *modifying* access. If you have no such access, you have no potential for a race. – Kerrek SB Jan 01 '17 at 14:32
  • @KerrekSB Your explanation resolve my confusion. Thx! – ROBOT AI Jan 01 '17 at 14:45
  • 3
    This is unsafe because the compiler wrote an *assignment operator* for you that allows the internal value to be changed. – Galik Jan 01 '17 at 14:53
  • Is there any chance that the SimpleClass object might get created or destroyed by one thread during the period another thread has the ability to access it? If so, that would introduce a race condition. (if the lifetime of the SimpleClass object is guaranteed to be a superset of the lifetime of the thread(s) accessing it, OTOH, you're okay) – Jeremy Friesner Jan 01 '17 at 14:54

4 Answers4

4

Your code is unsafe and you have a potential race condition.

class SimpleClass {
public:
  SimpleClass(int val) : v(val) {};
  int get() { return v; }
private:
  int v;
};

void thread_1(SimpleClass& sc)
{
    std::cout << sc.get() << '\n';
}

void thread_2(SimpleClass& sc)
{
    SimpleClass other(5);
    sc = other; // potential race
}

The problem is the compiler generated an assignment operator allowing objects of your class to be assigned which overwrites their internal data.

That causes a potential race.

Galik
  • 47,303
  • 4
  • 80
  • 117
  • Thanks for pointing out this! So, will the code be safe as long as we disable the copy-assignment constructor, as follows? SimpleClass(SimpleClass const&) = delete; SimpleClass& operator=(SimpleClass const&) = delete; – ROBOT AI Jan 01 '17 at 15:15
  • 1
    @ROBOTAI Yes, although you only need to delete the *copy assignment operator* not the *copy constructor* because other threads can't access it during construction. – Galik Jan 01 '17 at 15:26
3

If this is indeed the entire class, and there's no way to change the value of v after an instance is created, the class is immutable, and you don't need any other protective measures. Whatever thread calls get, in whatever moment, will still get the same value the instance was initialized with. There is no potential for a race condition here.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
2

For code to be unsafe, four conditions must be met. The third one can only occur if the code includes a write or an update.

Also see This so answer:

  1. There must exist memory locations that are accessible from more than one thread. .
  2. Some property (often called an invariant), which is associated with these shared memory locations, exists in the code, that must be true, or valid, for the program to function correctly.
  3. Third, this invariant property does NOT hold (it is false or incorrect) during some part of the code (a write or an actual update). (It is transiently invalid or false during some portion of the processing).
  4. The fourth and final condition that must occur for a race to happen (and for the code to therefore NOT be "thread-safe") is that another thread must be able to access the shared memory while the invariant is broken, thereby causing inconsistent or incorrect behavior.

In your case, consider the following [pseudo] code:

create new SimpleClass(1) in variable a
create new SimpleClass(2) in variable b
Switch a and b
  {
     create SimpleClass(a) into variable temp <-- with value 1
     a=b                   <-- puts reference to b into variable a
     b=temp                <-- puts temp(value = 1) into variable b
  }

if this code was interrupted by a second thread in the middle, (after b was assigned to a, but before temp was assigned to b), it would be bad.

EDIT. (to clarify the point made below by @Juan). So in your case, (the case of this SimpleClass, yes, since the class is immutable it itself is "thread-safe", in that the code in it cannot cause a race withing the class itself. But that does not mean the class could not be used in external multi-threaded code in such a way as to induce a race condition.

Community
  • 1
  • 1
Charles Bretana
  • 143,358
  • 22
  • 150
  • 216
  • There is indeed a race condition in your example, but it is not inside the class definition, but in the code using it. SimpleClass can still be thread safe. Comstructors are intrinsically thread safe, as there can be no reference to the object until it is fully constructed. Obviously, it is not possible for a single instance to be concurrently constructed by more than one thread. If two threads are concurrently creating a new instance of the same class, they are actually constructing two independent instances. The only problem can be a memory-model-related issue, as explained in my answer. – Juan Jan 02 '17 at 11:36
  • @Juan, Yes, perhaps the important point to make is that is a bit misleading to use the term "thread-safe" in reference to a class or a struct. It is a code snippet, or block of code, (which can be inside of a class, or can use a class, including one which does not include unsafe code), that can potentially be unsafe or cause a race. – Charles Bretana Jan 02 '17 at 12:32
0

An immutable class is intrinsically thread-safe.

Roughly speaking, concurrency issues occur when a write can run concurrently with any other read or write of the same data.

Think of shared and exclusive locks. Reads are performed by acquiring a shared lock, and writes require acquiring an exclusive lock. Any number of threads can concurrently own a shared lock. Only one thread can own an exclusive lock at the same time, and no shared lock can be held while an exclusive lock is held. This means you can perform reads concurrently, but not writes nor reads and a write. If your data can never be modified, then there will be no concurrency issues (no need for exclusive locks and therefore, shared locks make no sense).

This is one of the advantages of functional languages: data is never modified, making functions inherently thread safe and allowing for aggressive compiler optimizations.

Now, there is another question about thread safety that is usually forgotten: the memory model, specially in modern NUMA architectures.

If you know about volatile variables, the point is the compiler is free to optimise data access as long as the progran remains correct.... in single-threaded processing.

If the compiler is not aware that another thread may read or write a variable concurrently, it may keep the value in a register and never check for changes in main memory. This can happen also with cached values in different levels of cache. It may even optimize out a conditional branch if it knows the result of the condition in compile time and doesn't know the values involved may change nondeterministically.

Declaring a variable volatile indicates its value may change and forces to flush to main memory every time and read from main menory too.

But why would this be needed, if the value never changes? Well, the value changes during construction, which cannot be assumed to be instantaneous or atomic. If the compiler doesn't know it's multithreaded, it may even never flush any data to main memory. If you make a reference to this object available to another thread, it will read it from main memory, where it has never been initialised. Or it can even see the initialization taking place (this could happen when initialising a big String in old versions of java).

I believe modern C++ standards define a memory model, but I haven't digged into it yet. If the memory model is unspecified or is not strong enough, you may always need to execute primitives, such as acquiring or releasing a lock, which establish a "happens before" relationship. In any case, you definitely need to tell the compiler that data is either volatile or immutable, so that it can provide the guarranties of the memory model in use.

In this case, I would declare the variable and the getter method with const modifiers. I'm quite sure it will work just fine, but I recommend studying the memory model of the standard you are using and switch to a more modern standard if needed.

Juan
  • 430
  • 3
  • 9