As per cppreference,
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless:
a) both evaluations execute on the same thread or in the same signal handler, or
b) both conflicting evaluations are atomic operations (see std::atomic), or
c) one of the conflicting evaluations happens-before another (see std::memory_order)
I am confused a little with respect to point c. As per Anthony Williams's book(C++ concurrency in action), section 5.1.2:
If there’s no enforced ordering between two accesses to a single memory location from separate threads, one or both of those accesses is not atomic, and if one or both is a write, then this is a data race and causes undefined behavior. According to the language standard, once an application contains any undefined behavior, all bets are off; the behavior of the complete application is now undefined, and it may do anything at all
I understand that if I enforce the ordering, say by using std::mutex, so that it's not possible for more than one evaluation(of the conflicting expression) to happen at the exact same time as another, then everything is fine and my program will be well defined. I like to think of this as 'compile-time order enforcement'.
I wanted to understand if 'run-time order enforcement' is sufficient to eliminate data-race/undefined behaviour.
Say for example, I design a client-server system like below:
Server Specifications
It will be using 2 threads(Thread A and Thread B).
Both threads will be sharing a global int variable initialized with 0.
It will be listening for client's messages on 2 ports(port A and port B).
Thread A will use port A and Thread B will use port B.
Thread A/B pseudo code:
while(true) {
Receive message on the connected socket(port A in case of Thread A and port B for thread B).
Increment the shared global variable.
Send an acknowledgement to the client.
}
- Please note that, there's no inter-thread synchronisation introduced by the networking library/system calls made in the first and last step of the pseudo code.
Client Specifications
It will be single threaded
It will connect with the above mentioned 2 ports of the server.
It will alternately send messages to both the ports of the server, starting with port A.
It will send the next message only after receiving the acknowledgment of the previous message.
Pseudo code:
while(true) {
Send message to port A of server.
Receive acknowledgement of the above message.
Send message to port B of server.
Receive acknowledgement of the above message.
}
In the above example, server's Thread A and Thread B are sharing a global variable. There are 2 conflicting expressions(increment operation of shared variable) and I haven't used any language provided synchronisation mechanism. However, I enforce the ordering at run-time. I understand that the compiler cannot know about the runtime but on run time, because I ensure it, both thread A and thread B can never access the variable at the same time.
So I'm not sure if this falls in the category of data race.
So basically my queries are:
Is it necessary to enforce ordering at compile time rather than run time to avoid data race? Will the above server's code fall in the category of programs having data race?
Is there a way to enforce ordering at compile time in a multi-threaded C++ program when threads are sharing data without using C++ language's synchronisation constructs(mutex/futures/atomic) etc.
Will the above server's code fall in the category of programs having data race?
Yes.
Is there a way to enforce ordering at compile time in a multi-threaded C++ program when threads are sharing data without using C++ language's synchronisation constructs(mutex/futures/atomic) etc.
Implementations can provide additional guarantees beyond what the standard requires.