There is nothing to optimize away here (no commands to be excluded, see the details below), but there are the following:
- It is possible that
is_log_file
is set to true before log_stream
opens the file; and then another thread is possible to bypass the outer if
block code and start using the stream before the std::ofstream::open
has completed.
It could be solved by using std::atomic_thread_fence(std::memory_order_release);
memory barrier before setting the flag to true
.
Also, a compiler is forbidden to reorder accesses to volatile
objects on the same thread (https://en.cppreference.com/w/cpp/language/as_if), but, as for the code specifically, the available set of operator <<
functions and write
function of std::ofstream
just is not for volatile
objects - it would not be possible to write in the stream if make it volatile
(and making volatile
only the flag would not permit the reordering).
Note, a protection for is_log_file
flag from a data race
with C++ standard library means releasing std::memory_order_release
or stronger memory order - the most reasonable would be std::atomic
/std::atomic_bool
(see LWimsey's answer for the sample of the code) - would make reordering impossible because the memory order
- Formally, an execution with a data race is considered to be causing
undefined behaviour
- which in the double-checked lock is actual for is_log_file
flag. In a conforming to the standard of the language code, the flag must be protected from a data race (the most reasonable way to do it would be using std::atomic
/std::atomic_bool
).
Though, in practice, if the compiler is not insane so that intentionally spoils your code (some people wrongly consider undefined behaviour
to be what occurs in run-time and does not relate to compilation, but standard operates undefined behaviour
to regulate compilation) under the pretext it is allowed everything if undefined behavior
is caused (by the way, must be documented; see details of compiling C++ code with a data race in: https://stackoverflow.com/a/69062080/1790694
) and at the same time if it implements bool
reasonably, so that consider any non-zero physical value as true
(it would be reasonable since it must convert arithmetics, pointers and some others to bool
so), there will never be a problem with partial setting the flag to true
(it would not cause a problem when reading); so the only memory barrier std::atomic_thread_fence(std::memory_order_release);
before setting the flag to true
, so that reordering is prevented, would make your code work without problems.
At https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables you can read that implementations of initialization of static local variables since C++11 (which you also should consider to use for one-time actions in general, see the note about what to consider for one-time actions in general below) usually use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison.
This is an examples of exactly that environment-dependent safety of a non-atomic flag which I stated above. But it should be understood that these solutions are environment-dependent, and, since they are parts of implementations of the compilers themselves, but not a program using the compilers, there is no concern of conforming to the standard there.
To make your program corresponding to the standard of the language and be protected (as far as the standard is implemented) against a compiler implementation details liberty, you must protect the flag from data races, and the most reasonable then, would be using std::atomic
or std::atomic_bool
.
Note, even without protection of the flag from data races
:
because of the mutex, it is not possible that any thread would not get updates after changing values (both the bool
flag and the std::ofstream
object) by some thread.
The mutex implements the memory barrier, and if we don’t have the update when checking the flag in the first condition clause, we then get it then come to the mutex, and so guaranteedly have the updated value when checking the flag in the second condition clause.
because the flag can unobservably be potentially accessed in unpredictable ways from other translation units, the compiler would not be able to avoid writes and reads to the flag under the as-if
rule even if the other code of translation unit would be so senseless (such as setting the flag to true and then starting the threads so that no resets to false accessible) that it would be permitted in case the flag is not accessible from other translation units.
For one-time actions in general besides raw protection with flags and mutexes consider using:
All the mentioned multi-threading functionality is available since C++11 (but, since you are already using std::mutex
which is available starting since it too, this is the case).
Also, you should correctly handle the cases of opening the file failure.
Also, everyone must protect your std::ofstream
object from concurrent operations of writing to the stream.
Answering the additional question from the update of the question, there are no problems with properly implemented double-check lock, and the proper implementation is possible in C++.