直接读取 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