-2

I have a following code:

#include <thread>

void foo(int& value)
{
  // do nothing
}

int main()
{
  int value = 42;

  std::thread t1([&value]{ foo(value); });
  std::thread t2([&value]{ value = 100500; });

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

  return 0;
}

Common sense tells there is no data race here, as passing value by reference "must"(???) be thread-safe, but I could not verify it based on cppreference or standard draft N4950.

  1. How can I verify there is no data race based on cppreference or standard?
  2. Is passing value by reference thread-safe operation?
mouse_00
  • 593
  • 3
  • 13
  • 1
    [This answer](https://stackoverflow.com/a/71897193/7151494) basically explains everything with citations. I am nonetheless wondering, why would you think that it'd be common sense to assume that references provide any data-race protections. It's actually on the contrary - the value is shared and (at least) one thread changes it. With copies (no reference), there would be no data shared thus there would be no data races. – Fureeish Jul 26 '23 at 23:43
  • 1
    Verifying the lack of data races is hard. This code _looks_ like a data race, except that `foo` actually does nothing. So it's not. If `foo` uses `value` then it _is_ a data race. It's fine to pass by reference, provided the referenced data does not go out of scope until all threads are finished using that reference. In terms of _visibility_, you still have a problem here because there's no synchronization on access to a variable that can be modified by another thread. Technically, the changes to `value` in thread `t2` are not currently guaranteed visible in `main`. – paddy Jul 26 '23 at 23:44
  • @Fureeish, I assume it to be common sense, because I don't access value of reference – mouse_00 Jul 26 '23 at 23:44
  • For this to be *common sense*, the `value` would have to be **immutable**. The closest you can get to immutable in C++ is `const`, which is often sufficient. Otherwise, if `value` needs to be mutable, you are responsible for ensuring that it is protected (e.g., with a `mutex`). – Eljay Jul 26 '23 at 23:46
  • @paddy, I understand that reading value in `foo`, for instance, will cause data race. I am specifically interested in passing-by-reference operation and its thread-safeness. – mouse_00 Jul 26 '23 at 23:50
  • A reference is an alias. No data is copied/read. There is no data race in your code. – jabaa Jul 27 '23 at 00:00
  • 1
    *"Common sense"* is never as common as the sensible person hopes, nor as sensible as the common person thinks. – JaMiT Jul 27 '23 at 00:07
  • From [Reference initialization](https://en.cppreference.com/w/cpp/language/reference_initialization): _"A reference to T can be initialized with an object of type T, a function of type T, or an object implicitly convertible to T. Once initialized, a reference cannot be reseated (changed) to refer to another object."_ – paddy Jul 27 '23 at 00:09
  • Just on a side note: `std::thread t1([&value]{ foo(value); });` can eliminate the lambda by using `std::ref` instead, eg: `std::thread t1(foo, std::ref(value));` – Remy Lebeau Jul 27 '23 at 00:39

1 Answers1

1

Modifying or reading the same value without a sequenced-before/after relationship is a race condition. (the actual rules are more complex, but this is sufficient to avoid a race condition, and avoiding a race condition while breaking this rule is very challenging).

Passing references around does not modify or read something.

So, narrowly, your code has no race conditions, because only 1 of the two threads ever reads or writes your variable. And modifying the variable before you create a std::thread that modifies it has a happens-before relationship.

However, if foo does anything non-trivial with its argument, suddenly your code exhibits undefined behavior.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • You said that "passing references around does not read something", but I could not find this information in cppreference. Could you please give some reference? – mouse_00 Jul 26 '23 at 23:47
  • Have you read [Reference declaration](https://en.cppreference.com/w/cpp/language/reference)? Maybe the bit you're looking for is: _"references, once initialized, always refer to valid objects or functions"_. – paddy Jul 26 '23 at 23:55
  • @paddy, it does not really say anything about "reference initialization" being "reading operation" or "thread-safe" – mouse_00 Jul 27 '23 at 00:00
  • What exactly is the "thread-safety" issue you're worried about? The identifier `value` is an integer owned by the thread executing `main`. All references to that data are also created by this thread. Any possible "copies" of the reference _on any thread_ will also refer to that same object. The object exists _once_ in _one thread_, and all _references_ to it are immutable. – paddy Jul 27 '23 at 00:04
  • @mouse_00: the point is that reference initialization is *not* a reading operation on the object the reference is being initialized to refer to. So in the case of `t1` in your code, it never actually accesses the value of `value` (it only creates references to it) so there can be no race. – Chris Dodd Jul 27 '23 at 01:36
  • You're saying reading(w/o modify) a variable from N threads is a race condition. How can it be? What sort of architecture is it? Simultaneously writing is a race condition, but reading the same immutable peace of memory won't lead to any issues. – Dmitry Jul 27 '23 at 07:37
  • @Dmitry, "race condition" is when two or more threads concurrently access a variable and at least one access is a write. If thread A writes a variable just one time, and _then_ it creates threads B, C, and D that only read it, the read operations are not concurrent with the write, and so there is no race condition. But, if B, C, or D existed before the assignment, then that would be a race condition if there were no other synchronization. – Solomon Slow Jul 27 '23 at 11:41
  • @ChrisDodd, I was guessing so, but could not verify it using standard or cppreference. If you know where to find this info, could you please mention it? – mouse_00 Aug 09 '23 at 16:13
  • The standard never precisely spells out what constitues an 'access' to a memory location or object. but there's no reason for a reference initializer to access the memory location or object it is being initialized to refer to. It does, in section 1.8 call out "refer-to" and "access" as two distinct operations, implying that a "refer-to" action does not constitute an "access" – Chris Dodd Aug 09 '23 at 21:28