Multi-threaded Singleton Access in embedded C++
Earlier I described how I use a Meyer’s Singleton to manage C++ objects that are tied to the hardware. In this post, I want to share how I access singleton’s in a multi-threaded environment.
If you have ever tried to debug multi-threaded programs that read/write the same data you know it is difficult to debug problems. The problems show up intermittently and sometimes only very rarely. In C++, you can avoid these problems by scoping access to shared data and aggressively asserting if you forgot to lock a mutex.
Scoped Singleton Access
We want to make the compiler do as much of the work as possible, so the first step is to create a class that locks the singleton on construction and unlocks it on destruction. Below IO
is the class we want to access from multiple threads. The nested class IO::Scope
allows us to use a mutex
whenever IO
is accessed.
Keep in mind, you don’t have to use a nested class to use this technique.
struct IO {
class Scope {
public:
Scope(){
auto & io = IO::instance();
//This will block if another thread is already accessing IO
io.scope_mutex.lock();
//use a recursive mutex allows the user to next Scope instances
if( io.scope_lock_count == 0 ){
io.scope_pthread = Thread::self();
}
++io.scope_lock_count;
}
~Scope(){
auto & io = IO::instance();
--io.scope_lock_count;
if( io.scope_lock_count == 0 ){
io.scope_pthread = {};
}
io.scope_mutex.unlock();
}
};
Uart uart;
private:
IO() : uart("/dev/uart0"){}
//This will allow Scope and IoAccess
//to use instance() but nothing else
friend Scope;
friend class IoAccess;
//mutex should be recursive so the same thread can
//lock it recursively without worrying about errors
Mutex scope_mutex = Mutex(Mutex::Attributes().set_type(Mutex::Type::recursive));
pthread_t scope_pthread = {};
int scope_lock_count = 0;
static IO & instance(){
//constructed on first access
static IO m_instance;
return m_instance;
}
};
Accessor Class
Now I create a class that allows me to easily access the members of my singleton. This class ensures that whenever I access a member of IO
, I do so with the IO::Scope
locked to the current thread.
struct IOAccess {
static IO & io(){
auto & io = IO::instance();
//this guarantees the caller has the scope locked
API_ASSERT(io.scope_pthread == Thread::self());
return IO::instance();
}
static Uart& uart(){ return io().uart; }
};
Example using Accessor
Now let’s take a look at what it looks like to use IOAccess
in an inherited class called Logic
.
class Logic: public IOAccess {
public:
void do_some_work(){
{
IO::Scope io_scope;
//anywhere in this scope I can access io()
uart().write("Hello World\n");
}
//this access to io() will cause the program to
//halt using an ASSERT
uart().write("Failed\n");
//This line won't compile at all because IO::instance() is private
//and Logic is not a friend class
IO::instance().uart.write("Won't compile\n");
}
void do_some_other_work(){
IO::Scope io_scope;
io().uart.write("doing some other work\n");
//this can be called because Scope uses a recursive mutex
do_some_work();
}
}
Last Thing
Multi-threaded data access bugs can be very difficult to find and fix. Using C++, you can largely automate the process of locking and unlocking data using RAII. You lock the data in the constructor (IO::Scope
) and unlock it in the destructor. If you create a friend class
dedicated to accessing the shared data, you can have that friend class
check for a mutex lock and only allow the program to proceed if all is well. This puts most of the work of ensuring access to shared data is mutually exclusive on the compiler. Using assert()
, you will know immediately if you forgot to scope the access rather than waiting for the bug to manifest itself in strange ways.