3

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.

Yves
  • 11,597
  • 17
  • 83
  • 180
  • Never make any assumpitons. It can be interrupted. If you don't want to use mutexes, maybe volatile sig_atomic_t will be helpful. – tango-1 Apr 09 '21 at 10:58
  • In this case target is not marked volatile so the compiler will likely only do one read. – stark Apr 09 '21 at 10:58
  • I was expecting a comment mentioning volatile, so I already prepared the link [When to use volatile with multi threading?](https://stackoverflow.com/a/58535118) – MatG Apr 09 '21 at 11:09
  • @MatG synchronization stuff comes from sig_atomic_t, not from volatile. If you have, prepare another link. – tango-1 Apr 09 '21 at 11:15
  • @tango-1 In fact, `sig_atomic_t` is nothing but just a `typedef int`... And `volatile sig_atomic_t` is not thread safe in a multi-cores machine. – Yves Apr 09 '21 at 11:19

3 Answers3

5

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.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 1
    Atomicity isn’t enough on NUMA. The change also needs to be propagated. `std::atomic` does that, plain `char` doesn’t. – Konrad Rudolph Apr 09 '21 at 10:56
  • Well said, making assumptions on a particular architecture doesn't play well with code portability; moreover, using std::atomic makes clear that the variable is supposed to be accessed concurrently. – MatG Apr 09 '21 at 11:00
  • I appreciate that you tell me about `std::atomic`, well, so I changed my question... lol – Yves Apr 09 '21 at 11:21
  • @Yves well I answered your original question, you modified it such that my answer does not apply anymore. Anyhow, my answer is still the same: "Let someone else make that decision". There is no reason to not use `std::atomic` when you want an atomic variable – 463035818_is_not_an_ai Apr 09 '21 at 11:28
  • My bad. I want to use `mutex/lock` to represent all kinds of lock mechanisms... – Yves Apr 09 '21 at 11:29
1

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.

Yves
  • 11,597
  • 17
  • 83
  • 180
klutt
  • 30,332
  • 17
  • 55
  • 95
0

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.

nielsen
  • 5,641
  • 10
  • 27