Can I use char variable without lock in the multi-threading case

Viewed 114

As c/c++ standard said, the size of char must be 1. As my understanding, that means CPU guarantees that any read or write on a char must be done in one instruction.

Let's say we have many threads, which share a char variable:

char target = 1;

// thread a
target = 0;

// thread b
target = 1;

// thread 1
while (target == 1) {
    // do something
}

// thread 2
while (target == 1) {
    // do something
}

In a word, there are two kinds of threads: some of them are to set target into 0 or 1, and the others are to do some tasks if target == 1. The goal is that we can control the task-threds through modifying the value of target.

As my understanding, it doesn't seem that we need to use mutex/lock at all. But my coding experience gave me a strong feeling that we must use mutex/lock in this case.

I'm confused now. Should I use mutex/lock or not in this case?

You see, I can understand why we need mutex/lock in other cases, such as i++. Because i++ can't be done in only one instruction. So can target = 0 be done in one instruction, right? If so, does it mean that we don't need mutex/lock in this case?

Well, I know that we could use std::atomic, so my question is: is it OK to not use neither mutex/lcok nor std::atomic.

3 Answers

std::atomic guarantees that accessing a variable is atomic. From cppreference:

Each instantiation and full specialization of the std::atomic template defines an atomic type. If one thread writes to an atomic object while another thread reads from it, the behavior is well-defined (see memory model for details on data races).

When a char actually is atomic (being size 1, is not sufficient), then std::atomic<char> needs no extra synchronization. However, on a platform where char is not atomic, std::atomic<char> guarantees that it can be read and written atomically by using a mutex or similar.

In practice, I'd expect char to be atomic, but the standard does not guarantee that.

Also consider that operations like eg += read and write the value, hence atomic reads and writes alone are not sufficient to safely call +=, while std::atomic<T> has a proper operator+=.

TL;DR

I'm confused now. Should I use mutex/lock or not in this case?

Let someone else take that decision for you. When you want something atomic, use a std::atomic<something> unless you want fine grained control over the synchronisation.

TL;DR

is it OK to not use neither mutex/lcok nor std::atomic

No

In general, it's not ok to assume things. If you need guarantees for something, then make sure you have them.

This is closely related to a common logical fallacy. Just because you cannot imagine why something could be true, that does not mean that it's true.

Longer version

As c/c++ standard said

There's no such thing as "C/C++" and definitely not "the C/C++ standard". They are two completely different languages with different standards. However, they do agree on this point. sizeof (char) is 1 in both languages.

(Sidenote: sizeof 'a' will yield different results.)

As my understanding, that means CPU guarantees that any read or write on a char must be done in one instruction.

That's not correct. The CPU has it's own specification, completely separate from the language standards. And there's nothing that says that this has to be true, even if it probably are in most or all cases.

Because i++ can't be done in only one instruction.

That is CPU dependent. The x86 architecture has an instruction for this. https://c9x.me/x86/html/file_module_x86_id_140.html

As my understanding, it doesn't seem that we need to use mutex/lock at all. But my coding experience gave me a strong feeling that we must use mutex/lock in this case.

Even if the target CPU does read and write in one instruction, which it probably does, there's nothing that says that the C or C++ code needs to be compiled to just that instruction.

The standards for both C and C++ describes the behavior of the code. Not how it is converted to assembly.

So no, you cannot make the assumptions you're doing.

In general, it cannot be assumed that reading or writing a char is an atomic operation. However, the target architecture may provide that guarantee. For embedded C programs it is common practice to rely on such underlying guarantees to avoid the overhead of synchronization mechanisms in certain situations.

In the example in the question it must be noted that even if reading/writing target is an atomic operation, the value could be changed at any time, so there is no guarantee that it will be 1 inside the while loops.

Related