I don't recall any text adequately covering threads. There were plenty of discussions about conditional variables, mutexes, locks, but few about practical methods using them beyond rather trivial examples.
About std::async vs Thread
Modern C++ takes the approach primarily dependent upon std::futures, std::promise and std::async.
The idea is that std::async has "knowledge" about system resources, and can therefore more intelligently choose whether or not to actually run the task on a thread. It isn't absolutely applicable to all threaded circumstances, especially if the intent is to run a function in a loop like that of, say, a physics engine in a game.
A logger does seem correct for std::async, but whether or not you'd prefer to use that is another matter of choice.
The reason is that for a logger, the logger's thread should wait quietly until there is material to write, without wasting resources. To do that you need an "autoreset event", an object the thread can "wait" on indefinitely until it is signaled to run. An "autoreset event" object can be created using waitable objects. Boost has one pre-made (and Windows has such an object in the OS). Wrapped in a class named, say, AutoResetEvent, you'd fashion one as a member (like you have done with Queue). When the writer thread starts, it simply does something like:
1 2 3 4 5 6 7 8 9 10 11 12
|
// assuming a member
AutoResetEvent writeEvent;
void writer()
{
while (running)
{
writeEvent.wait();
// check and write pending data
}
}
| |
Then, every function that writes to the Queue also signals the event with something like:
writeEvent.signal();
This means a write would "wake up" the writer thread from waiting.
This makes the writer thread quiet and light, but jumps into action when something is there to be written.
Also, the destructor of the owning object (Logger) must signal the event to "wake it" so it exits at conclusion.
The specific function names for "signal" and "wait" may differ depending on the class representing the event.
Contrast that with std::async. You call std::async every time you submit something to the log (providing a callable object for the function to be performed). That, in turn, operates once, on the string passed as a parameter. No loop required. Multiple calls to std::async "queue" behind each other, depending on how you fashion it.
However, there is some loss of control - it takes some study to use this while keeping the order of the text written by the logger in order (that is, maybe you still must use the Queue of strings, but the call to async merely empties the Queue, where any following calls might be "do nothing" calls, discovering an empty Queue - which is ok by design).
About locks, mutexes, and the Queue of strings
Generally it appears you have the idea about std::lock (and the related std::mutex). A thread will obtain the lock before continuing, and if the lock was already "owned" by another thread, will block and wait until the lock is released, synchronizing the operations among threads.
However, if you use a std::queue or std::deque for the string array you have this curious problem: all writing threads block while writing.
Think of it this way. The writer thread obtains a lock, then starts reading the strings in the Queue. It writes each one to the output.
Key point: it does this while holding the lock,
making all other threads pushing strings
wait for the write to complete.
It would operate as if you don't have a background thread.
You could lock/release lock to get just the next string, then write that string with the lock released. That "multiplexes" the writer, one string at a time.
Another approach would be to hold the Queue using a std::shared_ptr. When the writer comes to obtain the strings to write (there could be several pushed in quickly), the writer thread "takes" the entire Queue (while under lock). A fresh, empty Queue is left in place. The lock is released.
At that point, writer has the entire pending Queue to itself. All other threads will push strings into the fresh, empty Queue left for them, while the writer loops through and writes all the strings in the Queue it "took".
It's like hanging a container on a hook. Threads pushing to the log drops their strings into that container that is on the hook. When the writer starts writing, it takes the entire container, leaving a new empty container on the hook.
This "batches" the writing task, and when things get busy reduces collisions between the writer thread and all the pushing threads.