I have a question about the way in which I am using boost::mutex objects in my Linux C++ application. I have a collection of convenience macros for performing various mutex operations, for example I have macros that return whether or not a mutex is locked by any thread, whether or not one is already locked specifically by the calling thread, and several others. The macros that need to know what thread (if any) currently has the mutex locked are the following:
// Determine whether or not a boost mutex is already locked
#define IsBoostMutexLocked(m) ((m.native_handle()->__data.__owner) != 0)
// Determine whether or not a boost mutex is already locked by the calling thread
#define IsBoostMutexLockedByCallingThread(m) ((m.native_handle()->__data.__owner) == (syscall(SYS_gettid)))
But I began to wonder whether or not it is safe to directly read the __owner int field. Could one thread not attempt to read the __owner field while another thread is busy locking or unlocking the mutex, thereby writing to the __owner field?
So, I devised a test that would attempt to expose any data race vulnerabilities and abort if such a situation was detected. So far, I have had 100 threads all concurrently locking, unlocking, and reading the __owner of one global mutex for tens of millions of loop iterations per thread and not once have I ever read an invalid __owner value. Below I've included the entirety of the test code. Frankly I am surprised that I've never read a bad __owner value.
Can anyone explain to me why it is (apparently) safe to directly read the __owner of a mutex even when other threads are trying to lock/unlock? Thanks in advance!
// The test mutex
boost::mutex g_TestMutex;
// The number of threads to launch for the test
#define NUM_THREADS_TO_LAUNCH 100
// The thread IDs of all test threads
long int g_AllSpecialThreadsTIDs[NUM_THREADS_TO_LAUNCH];
// Whether or not each test thread is ready to begin the test
std::atomic<bool> g_bEachTestThreadIsReadyToBegin[NUM_THREADS_TO_LAUNCH];
// Whether or not the test is ready to begin
std::atomic<bool> g_bTestReadyToBegin(false);
// A structure that encapsulates data to be passed to each test thread
typedef struct {
long *pStoreTIDLoc; // A pointer to the variable at which to store the thread ID
std::atomic<bool> *pTIDStoredLoc; // A pointer to the variable at which to store the status of whether or not the thread ID has been set
} TestThreadDataStructure;
// Ensure that a test thread ID is valid
void AssertIsValidTID(int iTID)
{
// Whether or not this thread ID is valid
bool bValid = false;
// If the thread ID indicates that no-one has locked the mutex
if (iTID == 0)
{
// A thread ID indicating that no-one has locked the mutex is always valid
bValid = true;
}
// Or, if this is a non-zero thread ID
else
{
// For each test thread
for (int i = 0; i < NUM_THREADS_TO_LAUNCH; i++)
{
// If this is a thread ID match
if (iTID == static_cast<int>(g_AllSpecialThreadsTIDs[i]))
{
// Set that the incoming thread ID is valid
bValid = true;
// Stop looking
break;
}
}
}
// If the incoming thread ID is invalid
if (!bValid)
{
// The test has failed
abort();
}
}
// Each test thread
void TestMutexTesterThread(void *pArg)
{
// Each mutex owner thread ID
int iOwner = 0;
// Unpack the incoming data structure
TestThreadDataStructure *pStruct = ((TestThreadDataStructure *)pArg);
long int *pStoreHere = pStruct->pStoreTIDLoc;
std::atomic<bool> *pTIDStoredLoc = pStruct->pTIDStoredLoc;
// Clean up
delete pStruct;
pStruct = NULL;
pArg = NULL;
// Get this thread ID
const long int lThisTID = syscall(SYS_gettid);
// Store this thread ID
(*pStoreHere) = lThisTID;
// Set that we have finished storing the thread ID
pTIDStoredLoc->store(true);
// While we are waiting for everything to be ready so that we can begin the test
while (true)
{
// If we are now ready to begin the test
if (g_bTestReadyToBegin.load())
{
// Stop waiting
break;
}
}
// The loop iteration count
uint64_t uCount = 0;
// For the life of the test, i.e. forever
while (true)
{
// Increment the count
uCount++;
// If we are about to go over the edge
if (uCount >= (UINT64_MAX - 1))
{
// Reset the count
uCount = 0;
}
// Every so often
if ((uCount % 500000) == 0)
{
// Print our progress
printf("Thread %05ld: uCount = %lu\n", lThisTID, uCount);
}
// Get the mutex owner's thread ID
iOwner = g_TestMutex.native_handle()->__data.__owner;
// Ensure that this is a valid thread ID
AssertIsValidTID(iOwner);
// Lock the mutex as part of the test
g_TestMutex.lock();
// Get the mutex owner's thread ID
iOwner = g_TestMutex.native_handle()->__data.__owner;
// Ensure that this is a valid thread ID
AssertIsValidTID(iOwner);
// Unlock the mutex as part of the test
g_TestMutex.unlock();
// Get the mutex owner's thread ID
iOwner = g_TestMutex.native_handle()->__data.__owner;
// Ensure that this is a valid thread ID
AssertIsValidTID(iOwner);
}
}
// Start the test
void StartTest()
{
// For each thread to launch
for (int i = 0; i < NUM_THREADS_TO_LAUNCH; i++)
{
// Initialize that we do not have a thread ID yet
g_AllSpecialThreadsTIDs[i] = 0;
g_bEachTestThreadIsReadyToBegin[i].store(false);
}
// For each thread to launch
for (int i = 0; i < NUM_THREADS_TO_LAUNCH; i++)
{
// Allocate a data structure with which to pass data to each thread
TestThreadDataStructure *pDataStruct = new TestThreadDataStructure;
// Store the location at which the thread should place its thread ID
pDataStruct->pStoreTIDLoc = ((long int *)((&(g_AllSpecialThreadsTIDs[i]))));
// Store the location of the atomic variable that each thread should set to true when it has finished storing its thread ID
pDataStruct->pTIDStoredLoc = ((std::atomic<bool> *)((&(g_bEachTestThreadIsReadyToBegin[i]))));
// The thread to return
boost::thread *pNewThread = NULL;
// Launch the new thread
try { pNewThread = new boost::thread(TestMutexTesterThread, pDataStruct); }
// Catch errors
catch (boost::thread_resource_error &ResourceError)
{
// Print this error
printf("boost::thread construction error: '%s'", ResourceError.what());
// This is a fatal error
abort();
}
// Clean up
delete pNewThread;
pNewThread = NULL;
}
// Whether or not all threads are ready to begin
bool bAllThreadsReadyToBegin = false;
// While we are waiting for all threads to be ready to begin
while (true)
{
// Reset to assuming all threads are ready to begin
bAllThreadsReadyToBegin = true;
// For each thread we launched
for (int i = 0; i < NUM_THREADS_TO_LAUNCH; i++)
{
// If this thread has not yet stored its thread ID
if (g_bEachTestThreadIsReadyToBegin[i].load() == false)
{
// We are not yet ready to begin
bAllThreadsReadyToBegin = false;
// Start over
break;
}
}
// If all threads are ready to begin
if (bAllThreadsReadyToBegin)
{
// We are done waiting
break;
}
}
// Atomically store that all threads are ready to begin and that the test should proceed
g_bTestReadyToBegin.store(true);
}