直接读取 pthread 互斥锁的所有者字段是否安全?
Is it safe to directly read owner field of a pthread mutex?
我对在 Linux C++ 应用程序中使用 boost::mutex 对象的方式有疑问。我有一组方便的宏来执行各种互斥操作,例如我有宏 return 互斥锁是否被任何线程锁定,一个是否已经被调用线程专门锁定,以及几个其他。需要知道哪个线程(如果有的话)当前锁定了互斥量的宏如下:
// 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)))
但是我开始怀疑直接读取__owner int字段是否安全。当另一个线程忙于锁定或解锁互斥体时,一个线程是否可以不尝试读取 __owner 字段,从而写入 __owner 字段?
因此,我设计了一个测试,试图暴露任何数据竞争漏洞,并在检测到这种情况时中止。到目前为止,我已经有 100 个线程同时锁定、解锁和读取一个全局互斥锁的 __owner,每个线程进行数千万次循环迭代,而且我从未读取过无效的 __owner 值.下面我包含了完整的测试代码。坦率地说,令我感到惊讶的是,我从未读过错误的 __owner 值。
任何人都可以向我解释为什么直接读取互斥锁的 __owner 是(显然)安全的,即使其他线程正在尝试 lock/unlock 也是如此吗?提前致谢!
// 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);
}
它是 'safe' 因为读取和写入最多 8 个字节,与相应的字节数对齐,是原子的。假设 x86_64
我不知道提升,但你的宏在 3 个方面看起来很糟糕:
- 你选择内部数据布局,这使得东西容易 api 和 abi 损坏
- 检查 "do i have the lock" 几乎总是设计错误(程序逻辑应该决定你是否这样做,因此不需要检查)。除了这种访问会减慢竞争中的代码 - 另一个 cpu 可能已经用锁定字弄脏了缓存行,您现在无缘无故地获取它
- 检查 "does someone have a lock" 本质上是活泼的,而且通常是错误的。如果你想在锁可用时抢到锁,否则失败,你可以trylock
我对在 Linux C++ 应用程序中使用 boost::mutex 对象的方式有疑问。我有一组方便的宏来执行各种互斥操作,例如我有宏 return 互斥锁是否被任何线程锁定,一个是否已经被调用线程专门锁定,以及几个其他。需要知道哪个线程(如果有的话)当前锁定了互斥量的宏如下:
// 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)))
但是我开始怀疑直接读取__owner int字段是否安全。当另一个线程忙于锁定或解锁互斥体时,一个线程是否可以不尝试读取 __owner 字段,从而写入 __owner 字段?
因此,我设计了一个测试,试图暴露任何数据竞争漏洞,并在检测到这种情况时中止。到目前为止,我已经有 100 个线程同时锁定、解锁和读取一个全局互斥锁的 __owner,每个线程进行数千万次循环迭代,而且我从未读取过无效的 __owner 值.下面我包含了完整的测试代码。坦率地说,令我感到惊讶的是,我从未读过错误的 __owner 值。
任何人都可以向我解释为什么直接读取互斥锁的 __owner 是(显然)安全的,即使其他线程正在尝试 lock/unlock 也是如此吗?提前致谢!
// 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);
}
它是 'safe' 因为读取和写入最多 8 个字节,与相应的字节数对齐,是原子的。假设 x86_64
我不知道提升,但你的宏在 3 个方面看起来很糟糕:
- 你选择内部数据布局,这使得东西容易 api 和 abi 损坏
- 检查 "do i have the lock" 几乎总是设计错误(程序逻辑应该决定你是否这样做,因此不需要检查)。除了这种访问会减慢竞争中的代码 - 另一个 cpu 可能已经用锁定字弄脏了缓存行,您现在无缘无故地获取它
- 检查 "does someone have a lock" 本质上是活泼的,而且通常是错误的。如果你想在锁可用时抢到锁,否则失败,你可以trylock