2

我正在尝试编写一个小测试用例来锻炼std::shared_timed_mutex::try_lock_until关于cppreference的文档。

这是我的代码

#include <thread>
#include <iostream>
#include <chrono>
#include <shared_mutex>
#include <cassert>
 
std::shared_timed_mutex test_mutex;
int global;
 
void f()
{
    auto now=std::chrono::steady_clock::now();
    test_mutex.try_lock_until(now + std::chrono::seconds(100));
    //test_mutex.lock();
    --global;
    std::cout << "In lock, global=" << global << '\n';
    test_mutex.unlock();
}

void g()
{
    auto now=std::chrono::steady_clock::now();
    test_mutex.try_lock_shared_until(now + std::chrono::seconds(10));
    //test_mutex.lock_shared();
    std::cout << "In shared lock, global=" << global << '\n';
    test_mutex.unlock_shared();
}
 
int main()
{
    global = 1;
    test_mutex.lock_shared();
    std::thread t1(f);
    std::thread t2(g);
    test_mutex.unlock_shared();
    t1.join();
    t2.join();
    assert(global == 0);
}

我期待的是

  1. main 获得一个读锁,然后启动 f 和 g
  2. f 尝试获取排他锁并阻塞
  3. g 获得读锁,读取global然后解锁读锁
  4. main 解锁读锁
  5. f 解锁、写入global、解锁和完成
  6. f 和 g 加入 7 断言为真且主要结束

(2 和 3 可以按任何顺序排列)。

这似乎可以单独工作。global在 gdb 下,如果我在 in的读取g和写入上放置断点f,然后运行,它会在读取时停止,正如我所期望的那样。

但是,如果我编译,-fsanitize=tthread那么我会遇到危险

WARNING: ThreadSanitizer: data race (pid=6780)
  Read of size 4 at 0x000000407298 by thread T2:
    #0 g() /home/paulf/scratch/valgrind/drd/tests/try_lock_shared_until14.cpp:25 (try_lock_shared_until14+0x402484)
[trimmed]
    #6 execute_native_thread_routine ../../../../../libstdc++-v3/src/c++11/thread.cc:82 (libstdc++.so.6+0xd9c83)

  Previous write of size 4 at 0x000000407298 by thread T1:
    #0 f() /home/paulf/scratch/valgrind/drd/tests/try_lock_shared_until14.cpp:15 [triimed]
    #6 execute_native_thread_routine ../../../../../libstdc++-v3/src/c++11/thread.cc:82 (libstdc++.so.6+0xd9c83)

  Location is global 'global' of size 4 at 0x000000407298 (try_lock_shared_until14+0x000000407298)

在 gdb 下,tsan 版本不会阻塞排他锁并首先到达写入。

我意识到我的例子不好,我应该检查返回值而不是依赖超时。

谁能解释 tsan 正在改变什么?如果我使用普通的 lock/lock_shared/unlock/unlock_shared 函数,则 tsan 不再抱怨。

(请注意,我不能为此使用 DRD 或 Helgrind - 我正在为他们编写测试用例,我知道他们目前不支持此功能,至少在我使用的平台 Fedora 34 / GCC 11.2.1 amd64)。

编辑:这是第 3 版,现在可以使用。main 等待 cvg()完成,然后释放共享锁,然后f()可以获得排他锁。

#include <thread>
#include <iostream>
#include <chrono>
#include <shared_mutex>
#include <mutex>
#include <cassert>
#include <condition_variable>

std::shared_timed_mutex test_mutex;
std::mutex cv_mutex;
std::condition_variable cv;
int global;
bool reads_done = false;
 
void f()
{
    auto now=std::chrono::steady_clock::now();
    std::cout << "In lock, trying to get mutex\n";
    if (test_mutex.try_lock_until(now + std::chrono::seconds(3)))
    {
       --global;
       std::cout << "In lock, global=" << global << '\n';
       test_mutex.unlock();
    }
    else
    {
        std::cerr << "Lock failed\n";
    }
}

void g()
{
    auto now=std::chrono::steady_clock::now();
    std::cout << "In shared lock, trying to get mutex\n";
    if (test_mutex.try_lock_shared_until(now + std::chrono::seconds(2)))
    {
       std::cout << "In shared lock, global=" << global << '\n';
       test_mutex.unlock_shared();
    }
    else
    {
        std::cerr << "Lock shared failed\n";
    }
    std::unique_lock<std::mutex> lock(cv_mutex);
    reads_done = true;
    cv.notify_all();
}
 
int main()
{
    global = 1;
    test_mutex.lock_shared();
    std::thread t1(f);
    std::thread t2(g);
    {
       std::unique_lock<std::mutex> lock(cv_mutex);
       while (!reads_done)
       {
          cv.wait(lock);
       }
    }
    std::cout << "Main, reader thread done\n";
    test_mutex.unlock_shared();
    std::cout << "Main, no more shared locks\n";
    t1.join();
    t2.join();
    assert(global == 0);
}


4

1 回答 1

1

这也是一个有效的调度方案:

  1. main 获得一个读锁,然后启动 f 和 g
  2. main 释放读锁然后加入
  3. f 开始执行并锁定超过 10 毫秒,例如由于抢占
  4. g 开始执行并阻塞 10 ms
  5. g 解除阻塞并读取共享变量

在 5. 中,发生了数据竞争,因此 ThreadSanitizer 指出它是正确的。纠正这个错误需要检查返回值try_lock_shared_until等等。

于 2021-11-26T09:34:44.377 回答