Reputation: 2101
I have a cpp file ("Plugin.cpp") which contains a small plugin that eventually gets linked into a much larger c++11 server project as a shared library.
It needs to start a thread and out of lazyness (it's a quick prototype) I simply declared and started it like so within Plugin.cpp:
#include <thread>
bool threadDone = false;
void myFunction() {
while(!threadDone) {
}
// do stuff
}
std::thread myThread = std::thread(&myFunction);
// Called by server to stop tell plugin to cleanup.
void stopPlugin() {
threadDone = true;
myThreadDone.join();
// ....
}
It seems like quite the anti-pattern to go starting threads as if the cpp file were a script file, but it works and doesn't seem to cause any problems when I run this code within the larger server project.
Without getting into the details of the larger project:
Can someone please detail what bad un-intended consequences may arise from doing this, as opposed to starting the thread in a more controlled manner from within an object or function?
My apologies if this is too obvious, I have not found this answer online and I have not coded in c++ recently so I am quite rusty. But it seems to me that something should eventually go very wrong.
Upvotes: 0
Views: 638
Reputation: 1861
There are several issues with your example.
threadDone
is shared data with a data racethreadDone
is shared data (write from main thread, read from plugin thread). In your example there is no synchronization mechanism (such as std::mutex
or std::atomic
). Hence there is a data race in your example; yielding undefined behavior.
When dealing with threads in C++ you should know what the C++ memory model says about threads.
You can easily verify that the code is not thread safe by inspecting the assembly of the following code:
namespace buggy {
bool threadDone = false;
unsigned turn = 0u;
void myFunction() {
while(!threadDone) {
++turn;
}
}
}// buggy
which reads
buggy::myFunction():
cmp BYTE PTR buggy::threadDone[rip], 0
jne .L1
.L3:
jmp .L3
.L1:
ret
Note that threadDone
is compared exactly once. If it is false then there is an infinite loop (without any increment).
The easiest way to fix the above is given by the use of atomics:
#include <atomic>
namespace fixed {
std::atomic<bool> threadDone{false};
std::atomic<unsigned> turn{0u};
void myFunction() {
while(!threadDone.load()) {
turn.fetch_add(1u);
}
}
}// fixed
which yields the assembly
fixed::myFunction():
movzx eax, BYTE PTR fixed::threadDone[rip]
test al, al
jne .L5
.L8:
lock add DWORD PTR fixed::turn[rip], 1
movzx eax, BYTE PTR fixed::threadDone[rip]
test al, al
je .L8
.L5:
ret
I expect that the thread from your plugin uses global data as input and output. It is difficult to ensure that the data is properly initialized before use.
startPlugin
, and stopPlugin
may failThis is just a note that the interface is asymmetric (startPlugin
not available because the plugin always starts --- when?). In addition, stopPlugin
fails if the thread is not joinable
(e.g., if stopPlugin
is called twice).
The latter may be a real issue or not.
Upvotes: 1
Reputation: 1445
I would have used an std::atomic<bool>
instead of a bool threadDone
. See When do I really need to use atomic<bool> instead of bool? for more details.
The way you are declaring your std::thread
object, you do not have any direct control on at what point the thread is going to start. Is there a pressing need to declare the thread object this way? Another important consideration is if your shared library is loaded/linked against a separate target than the intended server, would it not spawn an additional unintended thread?
If you have a stopPlugin
method which the server can call to stop the thread, you can always write a startPlugin
method as well with the actual thread object nicely wrapped in a simple interface like below.
class Plugin {
public:
void startPlugin();
void stopPlugin();
private:
std::thread myThread;
};
I very rarely use static storage the way you are using it even for much simpler structs and tend to prefer construct on first use idiom.
Upvotes: 3