【问题标题】:ThreadSanitizer detects a data race, where is the problem?ThreadSanitizer 检测到数据竞争,问题出在哪里?
【发布时间】:2019-08-31 19:47:44
【问题描述】:

在这个示例程序中,我试图避免使用前向声明和循环依赖来利用 lambda 函数(称为 data_race)

struct B{
    int x;
    std::thread* tid;

    B(int _x){
        x = _x;
        tid = NULL;
    }

    ~B(){
        if(tid != NULL) delete tid;
    }

    void run(std::function<void(B*)> f){
        auto body = [&f, this](){
            f(this);
        };

        tid=new std::thread(body);
    }

    void wait(){
        tid->join();
    }
};

struct A{
    int x;
    std::mutex mtx;

    A(int _x){
        x = _x;
    }

    void foo(B* b){
        std::unique_lock<std::mutex> lock(mtx);
        x = b->x;
    };
};

int main(){
    A a(99);
    std::vector<B> v;

    auto data_race = [&a](B* b){ a.foo(b);};

    for(int i=0; i<10; i++){
        v.push_back(B(i));
    }

    for(int i=0; i<v.size(); i++){
        v[i].run(data_race);
    }

    for(int i=0; i<v.size(); i++){
        v[i].wait();
    }

    return 0;
}

但是 ThreadSanitizer 检测到来自 lambda 函数 data_race 的数据竞争。你能帮我理解为什么吗? A 内部的互斥体应该足以避免它。另外,你能帮我找到解决办法吗?


编辑:使用前向声明,不再检测到数据竞争,为什么? (只发结构体,不发主文,长帖见谅)

struct B;
struct A{
    int x;
    std::mutex mtx;

    A(int _x){
        x = _x;
    }

    void foo(B* b);
};

struct B{
    int x;
    std::thread* tid;

    B(int _x){
        x = _x;
        tid = NULL;
    }

    ~B(){
        if(tid != NULL) delete tid;
    }

    void run(A& a){
        auto body = [&a, this](){
            a.foo(this);
        };

        tid=new std::thread(body);
    }

    void wait(){
        tid->join();
    }
};

void A::foo(B* b){
    std::lock_guard<std::mutex> lock(mtx);
    x = b->x;
}

【问题讨论】:

  • 赋值x = b-&gt;x; 将导致x 的结果值来自随机的B 对象,即使访问已正确同步。
  • 同样不相关的注意:你的类B违反了0/3/5的规则并且会导致未定义的行为,如果任何B,在它上面调用run之后,被复制/移动, 或在没有事先调用 wait 的情况下被销毁,这可能在 run 调用之后修改 v 时发生。
  • 旁注:新的 std:: 线程没有意义。

标签: c++ multithreading c++11 concurrency data-race


【解决方案1】:

您将对函数本地 f 的引用传递给由 thread 构造函数调用的 lambda body

当线程函数到达body 内部的调用时,该对象可能不再存在。

稍微扩展一下:

run 创建的新线程将执行 body 的副本,这是一个包含对对象 f 的引用 ([&amp;f]) 的 lambda,该对象具有函数 run 的范围和当主线程离开run时将被销毁。

线程函数将在 body 中的 f(this) 行中对 f 的引用调用 operator()。如果主线程中的run 在线程函数执行此调用之前到达作用域结束,则此调用已经导致未定义的行为。这里的数据竞争是,主线程可能会写入f 的访问内存来销毁它,与生成线程中对f 的内存读取访问不同步。

中间函数对象完全可以避免:

template<typename F>
void run(const F& f){
    auto body = [this](){
        f(this);
    };

    tid=new std::thread(body);
}

这会将外部 lambda data_race 作为引用,将此引用复制到 body 中,并且只要确保 main 中的 data_race 超过所有线程,前面提到的数据竞争被避免了。

您编辑的代码做了类似的事情,即它消除了run 的本地对象。 a in body 将是对 main 中定义的 A 的引用,只要 main 保证其生命周期超出线程的生命周期,此时不会导致任何问题。

【讨论】:

  • 我不确定这是否是数据竞争的原因,但您能帮我找到解决方案吗?
  • 您的解决方案有效,但我不太确定为什么...想解释一下 std::function 有什么问题?
【解决方案2】:

数据竞争是由于有一个 A 对象(a,在 main 中声明)被所有不同的 B 对象共享,因为它通过引用传递给 data_race lambda。这个A 对象在B::run 中创建的所有线程中都被引用,因此分配x = b-&gt;x 取决于哪个线程最后执行,因此存在数据竞争。

【讨论】:

    猜你喜欢
    • 2022-08-20
    • 1970-01-01
    • 1970-01-01
    • 2016-07-07
    • 1970-01-01
    • 2022-08-03
    • 2023-01-30
    • 2023-03-23
    • 1970-01-01
    相关资源
    最近更新 更多