0

When different threads only use unrelated objects and literally do not share anything they cannot have a race condition, right? Obviously.

Actually all threads share something: the address space. There is no guarantee that a memory location that was used by one thread isn't going to be allocated at some other time to another thread. This can be true of memory for dynamically allocated objects or even for automatic objects: there is no prescription that the memory space for the "stacks" (the local objects of functions) of multiple threads is pre-allocated (even lazily), disjoint and represented as the usual linear "stack"; it could be anything with stack (FILO) behavior. So the memory location used to store an automatic object can be reused later by another automatic object in another thread.

That in itself seems pretty innocuous and uninteresting as how room is made for automatic objects is only important when room is missing (very large automatic arrays or deep recursion).

What about synchronisation? Unrelated disjoint threads obviously cannot use any C++ synchronisation primitive to ensure correct synchronisation as by definition there is nothing (to) synchronize (on), so no happens before relation is going to be created between threads.

What if the implementation reuses the memory range of the stack of foo() (including the location of i) after destruction of local variables and exit of foo() in thread 1 to store variables for bar() in thread 2?

void foo() { // in thread 1
   int i;
   i = 1;
}

void bar() { // in thread 2
   int i;
   i = 2;
}

There is no happens before between i = 1 and i = 2.

Would that cause a data race and undefined behavior?

In other words, do all multithread programs have a potential for having undefined behavior based on implementation choices the user has no control over, that are unforeseeable and with races he can't do anything about?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • "do all multithread programs have a potential for having undefined behavior based on implementation choices the user has no control over, that are unforeseeable and with races he can't do anything about?" - Do you seriously believe the people who wrote the standard wrote it with such a fundamental flaw? – Jesper Juhl May 29 '19 at 04:19
  • You don't believe so? Actually I think there are many more flaws, that the concept of lifetime is uninterpretable and that even ignoring these issues [all programs have UB in C and C++](https://stackoverflow.com/q/56240786/963864) – curiousguy May 29 '19 at 04:33
  • 3
    "data race" and "undefined behaviour" are properties of the program, not the implementation. The code you posted doesn't have undefined behaviour, and if the implementation doesn't behave as the standard prescribes then the implementation is non-conforming. – M.M May 29 '19 at 05:30
  • @M.M Obviously (UB describes an execution of a program), where did I suggest that implementations exhibit UB? Why isn't there a data race in the program execution **if** the implementation choose to reuse to same location for both `i`? – curiousguy May 29 '19 at 05:32
  • 1
    That's not how any of this works... there is no data race because the standard says there isn't, and the implementation can only choose to re-use memory if it can guarantee the observable behaviour of the program will be the same as if memory wasn't reused – M.M May 29 '19 at 05:34
  • 1
    The standard specifies when there is a race, not when there isn't a race. This situation isn't specified as a race , therefore it isn't one. – M.M May 29 '19 at 05:43
  • It's not practical to write a "proof" of the standard not specifying something. The form of such a proof would be copying out the standard and saying "It's not specified in there". You're welcome to verify this proof by consulting the standard – M.M May 29 '19 at 05:52
  • Yes, in [intro.races] it does not specify that your code is a race condition. You can view the full text of [intro.races] from various sources. – M.M May 29 '19 at 05:54
  • Take another angle: let's assume the user does that, not the implementation. Obviously the user would have to provide synchronisation, right? So why is the implementation exempt? – curiousguy May 29 '19 at 06:12
  • 1
    It's not exempt. The implementation won't use the same physical address for two separate objects unless it can prove they don't have overlapping lifetimes involving conflicting reads and writes; otherwise it wouldn't be a conforming implementation. This is an issue for the implementor to worry about, not the user – M.M May 29 '19 at 06:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/194087/discussion-between-curiousguy-and-m-m). – curiousguy May 29 '19 at 06:20
  • @SolomonSlow I'm pretty sure that changing memory mapping is done after all non committed evaluations are purged. Distinct processes probably don't have memory visibility issues. Anyway it's a language semantics Q not a CPU behavior Q. What happens before relation should exist? – curiousguy May 29 '19 at 19:45
  • @JesperJuhl "_Do you seriously believe the people who wrote the standard wrote it with such a fundamental flaw?_" They managed to introduce a function to turn a ptr to memory into a ptr to an object while not noticing that ptr being trivial objects guarantees that function cannot be necessary, contrary to common belief. So, a big yes. C is similarly broken. – curiousguy Aug 03 '19 at 11:18

2 Answers2

4

The C++ memory model doesn't behave like you might intuitively expect. For example, it has memory locations, but quoting the N4713 draft section 6.6.1, paragraph 3:

A memory location is either an object of scalar type or a maximal sequence of adjacent bit-fields all having nonzero width. [ Note: Various features of the language, such as references and virtual functions, might involve additional memory locations that are not accessible to programs but are managed by the implementation. — end note ] Two or more threads of execution (6.8.2) can access separate memory locations without interfering with each other.

So by the C++ memory model, two distinct objects in different threads are never considered to have the same memory location, even if at the physical machine level, one is allocated in the same RAM after the other is deallocated.

By the C++ memory model, the situation you ask about is not a data race. The implementation must take whatever steps are necessary to ensure this is safe, regardless of the hardware's memory model.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • That quote alone would seem imply that two thread can use the same memory range at the same time: Thread 1 does `void foo() { extern int g; g = 1; }` and thread 2 does `void foo() { extern int g; new(&g) int (2); }` as two different objects are created. – curiousguy May 29 '19 at 03:04
  • @curiousguy: I believe that falls afoul of rules regarding object lifetime, but I don't have a standard quote to point to. – user2357112 May 29 '19 at 03:09
  • The construction of an object over another is legal, the issue here is the race condition caused by the lack of synchronization. It *has* to be a data race even if the **object `g`** is not used in the second function (which I meant to call `bar()`) – curiousguy May 29 '19 at 03:14
  • 1
    `new(&g) int(2);` ends the lifetime of an object that previously existed in the same region of storage; there would be undefined behaviour if `bar` were called before `foo` since `g = 1;` would modify an object after its lifetime had ended – M.M May 29 '19 at 06:13
  • @M.M By hypothesis, the functions are called by different threads w/o synchronization; are you saying this is a data race? – curiousguy May 29 '19 at 06:15
  • It's definitely UB due to "access object out of lifetime" , I don't think it is a data race (in the standard) since the data race rule is defined in terms of concurrent access to the same object. NOTE: as covered by the quote in this answer, the term "memory location" in the standard means an object or a bit-field , it does not mean a particular region of storage or a physical memory location of some implementation. Some of your comments seem to treat the term "memory location" as if it had actually said "region of storage". – M.M May 29 '19 at 06:25
  • @M.M So what is the proper term to describe that type of race condition where you destroy or reuse the storage of an object while some other thread is using it? – curiousguy May 29 '19 at 14:44
2

The physical machine's "same address" is irrelivant to the C++ memory model. The C++ memory model talks about the behaviour of the abstract machine. Addresses in the abstract machine can be incomparable in fundamenral way, even if they have the same machine address at different times.

Race conditions in the C++ abstract machine talk about operations in it, not on the physical machine. It is the job of the compiler to ensure that the physical machine implementation of the abstract machine behaviour of the C++ code is conformant.

If it does strange things like reuse stack address space between threads, then it does whatever it has to in order to maintain the lack of race conditions that accessing unrelated variables in the abstract machine. None of this happens at the C++ code level; there is no C++ code (other than possibly in namespace std) involved.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Addresses of objects can't be compared by the user, that's what you are implying? Or that the abstract machine doesn't deal with addresses? Can you describe that very abstract "abstract machine"? – curiousguy May 29 '19 at 02:52
  • @curious No, that wouldn't fit in a SO answer, let alone a comment. The C++ standard specifies the behaviour of an abstract machine that the program runs under; it does not specify what concrete machines do. Compilers are responsible to create code that behaves like the abstract machine is specified to unless the code exhibit UB. To understand everything about that machine, it you'd have to understand the entire standard, basically. Race conditions are one kind of UB defined by the standard. UB is defined in terms of abstract machine operations. Reusing stack does not happen in it. – Yakk - Adam Nevraumont May 29 '19 at 03:22
  • It's a simple question really: can addresses of unrelated objects be compared in the abstract machine? – curiousguy May 29 '19 at 03:25
  • 2
    @curious no, not with `<`; such comparisons are UB. Sometimes with `==`, but not outside of the storage lifetime; use of pointers outside of storage lifetime is full of UB. Storing them as uint ptr results in unspecified values; the only guarantee you get is round-tripping. Comparison while integral is meaningless. Like I said, it is an entire standard worth of corner cases, and "can you compare" is vague. – Yakk - Adam Nevraumont May 29 '19 at 03:33
  • What if you do a bitwise comparison? Aren't all pointers trivial objects where bitwise comparison is usable? – curiousguy May 29 '19 at 03:35
  • 3
    @curious Comments are not for asking follow up questions. Good night. Use [ask question], after doing some reseach yourself, if you have new questions. – Yakk - Adam Nevraumont May 29 '19 at 03:36
  • The "abstract machine" is just a fancy way of saying the exact evaluation steps described by the std. There is nothing here. You can't use "abstract machine" any more than you can use "as if rule" to prove anything, as neither terms describe a distinct reality. Re: trivial types. Ptr are trivial objects and you do bitwise comparisons. These are not followup questions these are facts. – curiousguy May 31 '19 at 03:04