You can, but you must be careful. Synchronization and happens-before are required, or you wander into undefined behavior territory.
Undefined behavior can include "it appears to work", but it breaks when you change unrelated code, linking order, or update your compiler.
First, we can clean up mySingleton
:
class mySingleton {
private:
mySingleton() {
std::cout<<"Singleton initialization"<<std::endl;
std::this_thread::sleep_for(chrono::milliseconds(10000));
std::cout<<"Singleton initialization terminated"<<std::endl;
}
public:
static mySingleton* getInstance() {
static mySingleton* instance = new mySingleton;
return instance;
}
};
"magic statics" guarantee that the initialization of a static local variable is done exactly once, no matter how many threads attempt it, and nobody will miss it.
This doesn't however provide a guarantee that your worker threads operations will be visible from the main thread.
We should replace this pthread task:
void *create(void* x) {
this_thread::sleep_for(chrono::milliseconds(3000));
*(mySingleton**)x = mySingleton::getInstance();
pthread_exit(NULL);
}
with something a bit less C and more C++.
int main(){
mySingleton* x = NULL;
std::thread t1 = ([&x]{
this_thread::sleep_for(chrono::milliseconds(3000));
x = mySingleton::getInstance();
});
std::cout << "thread 1 created"<<std::endl;
std::this_thread::sleep_for(chrono::milliseconds(1000));
std::cout << "thread 2 is about to start"<<std::endl;
while(x == NULL){
std::cout <<"t2: instance not created yet"<<std::endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
std::cout << "main thread\n";
return 0;
}
I've gotten rid of the pthread noise and replaced it with a simpler std::thread
implementation.
The above code contains a race condition, as did the original code I based it off of. You write to x
from one thread and read from another without synchronization; that is a race condition in C++, and the result is undefined behavior.
We can fix this a few ways.
int main(){
std::future<mySingleton*> x = std::async( std::launch::async, []{
this_thread::sleep_for(chrono::milliseconds(3000));
return mySingleton::getInstance();
});
std::cout << "thread 1 created"<<std::endl;
std::this_thread::sleep_for(chrono::milliseconds(1000));
std::cout << "thread 2 is about to start"<<std::endl;
using namespace std::chrono::literals;
while(x.wait_for(0s) == std::future_status::timeout){
std::cout <<"t2: instance not created yet"<<std::endl;
this_thread::sleep_for(1000ms);
}
std::cout << "main thread\n";
return 0;
}
here we make the worker task populate a std::future
of what we want. This lets us wait for it to be ready, and when it is we can x.get()
to get a synchronized mySingleton*
that has been initialized.
We can create an explicit thread instead:
std::promise<mySingleton*()> pr;
std::future<mySingleton*> x = pr.get_future();
std::thread t1 = std::thread( [&pr]{
this_thread::sleep_for(chrono::milliseconds(3000));
pr.set_value( mySingleton::getInstance() );
});
and leave the rest of the code alone.
If we don't want x
to be a future (or shared future), we can make it a mutex wrapped value:
template<class T>
struct shared_mutex_wrapped {
template<class F>
auto read( F&& f ) const {
auto l = lock();
return f(t);
}
template<class F>
auto write( F&& f ) {
auto l = lock();
return f(t);
}
private:
mutable std::shared_mutex m;
T t = {};
auto lock() const { return std::shared_lock< std::shared_mutex >( m ); }
auto lock() { return std::unique_lock< std::shared_mutex >( m ); }
};
we can then get code similar to your original flow:
mutex_wrapped<mySingleton*> x;
std::thread t1 = ([&x]{
this_thread::sleep_for(chrono::milliseconds(3000));
x.write([](auto&x){ x= mySingleton::getInstance();} )
});
std::cout << "thread 1 created"<<std::endl;
std::this_thread::sleep_for(chrono::milliseconds(1000));
std::cout << "thread 2 is about to start"<<std::endl;
while(x.read([](auto& x){ return x==NULL; }) ){
std::cout <<"t2: instance not created yet"<<std::endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
std::cout << "main thread\n";
return 0;
where access to x
is guarded by reader/writer synchronization.
Any attempt to write directly to x
in one thread, and read directly without synchronization from x
in another thread, makes your program execute undefined behavior under the C++ standard.
If you do this, it might "appear to work", because that is one possible symptom of undefined behavior. But to actually work -- to be a well defined program that does what you want it to do -- you must arrange that the shared value x
have its access be synchronized somehow. It must be possible for the code initializing it to formally "happen-before" the code reading it, and no reads can occur the writing is occurring that doesn't happen-before or happen-after.
Another approach is std::atomic<mySingleton*>
:
std::atomic<mySingleton*> x;
std::thread t1 = ([&x]{
this_thread::sleep_for(chrono::milliseconds(3000));
x = mySingleton::getInstance();
});
std::cout << "thread 1 created"<<std::endl;
std::this_thread::sleep_for(chrono::milliseconds(1000));
std::cout << "thread 2 is about to start"<<std::endl;
while(x.get() == nullptr){
std::cout <<"t2: instance not created yet"<<std::endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
std::cout << "main thread\n";
return 0;
which requires even less changes to your main
function.