pthread_once() 中的竞争条件?

Posted

技术标签:

【中文标题】pthread_once() 中的竞争条件?【英文标题】:race-condition in pthread_once()? 【发布时间】:2012-06-01 01:22:53 【问题描述】:

我在一个线程中有一个std::future,它正在等待在另一个线程中设置一个std::promise

编辑: 用一个将永远阻塞的示例应用更新了问题:

更新:如果我改用pthread_barrier,下面的代码会阻塞。

我创建了一个测试应用程序来说明这一点:

基本上,foo 类创建了一个thread,它在其运行函数中设置了一个promise,并在构造函数中等待设置该promise。设置后,它会增加 atomic 计数

然后我创建一堆foo 对象,将它们拆掉,然后检查我的count

#include <iostream>
#include <thread>
#include <atomic>
#include <future>
#include <list>
#include <unistd.h>

struct foo

    foo(std::atomic<int>& count)
        : _stop(false)
    
        std::promise<void> p;
        std::future <void> f = p.get_future();

        _thread = std::move(std::thread(std::bind(&foo::run, this, std::ref(p))));

        // block caller until my thread has started 
        f.wait();

        ++count; // my thread has started, increment the count
    
    void run(std::promise<void>& p)
    
        p.set_value(); // thread has started, wake up the future

        while (!_stop)
            sleep(1);
    
    std::thread _thread;
    bool _stop;
;

int main(int argc, char* argv[])

    if (argc != 2)
    
        std::cerr << "usage: " << argv[0] << " num_threads" << std::endl;
        return 1;
    
    int num_threads = atoi(argv[1]);
    std::list<foo*> threads;
    std::atomic<int> count(0); // count will be inc'd once per thread

    std::cout << "creating threads" << std::endl;
    for (int i = 0; i < num_threads; ++i)
        threads.push_back(new foo(count));

    std::cout << "stopping threads" << std::endl;
    for (auto f : threads)
        f->_stop = true;

    std::cout << "joining threads" << std::endl;
    for (auto f : threads)
    
        if (f->_thread.joinable())
            f->_thread.join();
    

    std::cout << "count=" << count << (num_threads == count ? " pass" : " fail!") << std::endl;
    return (num_threads == count);

如果我在一个有 1000 个线程的循环中运行它,它只需要执行几次,直到发生竞争并且其中一个 futures 永远不会被唤醒,因此应用程序会永远卡住。

# this loop never completes
$ for i in 1..1000; do ./a.out 1000; done

如果我现在 SIGABRT 应用程序,生成的堆栈跟踪显示它卡在 future::wait 堆栈跟踪如下:

// main thread
    pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
    __gthread_cond_wait (__mutex=<optimized out>, __cond=<optimized out>) at libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:846
    std::condition_variable::wait (this=<optimized out>, __lock=...) at ../../../../libstdc++-v3/src/condition_variable.cc:56
    std::condition_variable::wait<std::__future_base::_State_base::wait()::lambda()#1>(std::unique_lock<std::mutex>&, std::__future_base::_State_base::wait()::lambda()#1) (this=0x93a050, __lock=..., __p=...) at include/c++/4.7.0/condition_variable:93
    std::__future_base::_State_base::wait (this=0x93a018) at include/c++/4.7.0/future:331
    std::__basic_future<void>::wait (this=0x7fff32587870) at include/c++/4.7.0/future:576
    foo::foo (this=0x938320, count=...) at main.cpp:18
    main (argc=2, argv=0x7fff32587aa8) at main.cpp:52


// foo thread
    pthread_once () from /lib64/libpthread.so.0
    __gthread_once (__once=0x93a084, __func=0x4378a0 <__once_proxy@plt>) at gthr-default.h:718
    std::call_once<void (std::__future_base::_State_base::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>&, bool&), std::__future_base::_State_base* const, std::reference_wrapper<std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()> >, std::reference_wrapper<bool> >(std::once_flag&, void (std::__future_base::_State_base::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, ...) at include/c++/4.7.0/mutex:819
    std::promise<void>::set_value (this=0x7fff32587880) at include/c++/4.7.0/future:1206
    foo::run (this=0x938320, p=...) at main.cpp:26

我很确定我的代码没有做错什么,对吧?

这是 pthread 实现还是 std::future/std::promise 实现的问题?

我的库版本是:

libstdc++.so.6
libc.so.6 (GNU C Library stable release version 2.11.1 (20100118))
libpthread.so.0 (Native POSIX Threads Library by Ulrich Drepper et al Copyright (C) 2006)

【问题讨论】:

我们能看到更多send_promise的内部结构吗? @anthony-arnold,在操作正文中添加了更多详细信息 呃,Wait::_p什么时候初始化的?我没看到。 @ildjarn,Wait对象创建时默认构造 @ildjarn,参考文档让我相信默认构造就足够了:stdthread.co.uk/doc/headers/future/promise/… 【参考方案1】:

确实,在本地 promise 对象的析构函数(在构造函数的末尾和线程对 set_value() 的调用之间存在竞争条件。也就是说,set_value() 唤醒了主线程,即just next 销毁了promise对象,但是set_value()函数还没有完成,死锁了。

看了C++11标准,不确定你的使用是否允许:

void promise&lt;void&gt;::set_value();

效果:以原子方式将值 r 存储在共享状态中并使该状态准备就绪。

但在其他地方:

set_value、set_exception、set_value_at_thread_exit 和 set_exception_at_thread_exit 成员函数的行为就像它们在更新 Promise 对象时获取与 Promise 对象关联的单个互斥锁

set_value() 调用对于其他函数(例如析构函数)是否应该是原子的?

恕我直言,我会说不。效果类似于在其他线程仍在锁定互斥锁时销毁互斥锁​​。结果未定义。

解决方案是让p 比线程更有效。我能想到的两个解决方案:

    p 成为班级成员,正如 Michael Burr 在另一个答案中所建议的那样。

    将承诺移动到线程中。

在构造函数中:

std::promise<void> p;
std::future <void> f = p.get_future();
_thread = std::thread(&foo::run, this, std::move(p));

顺便说一句,您不需要调用bind,(线程构造函数已经重载),或者调用std::move 来移动线程(正确的值已经是一个右值)。不过,在 Promise 中调用 std::move 是强制性的。

而线程函数并没有收到引用,而是被移动的promise:

void run(std::promise<void> p)

    p.set_value();

我认为这正是 C++11 定义两个不同类的原因:promisefuture:你将 promise 移入线程,但保留 future 以恢复结果。

【讨论】:

嗯,[futures.state]/9 说使状态准备就绪(在p.set_value() 中)与来自f.wait() 的返回同步,因此承诺不应该在设置值之前销毁...但是更改代码以移动承诺确实似乎可以修复错误。我将不得不审查我对promise::set_value() 的实施...... 啊,但是p.set_value() 使状态准备就绪然后 唤醒所有等待的线程([futures.state]/6)。如果在构造函数开始等待之前准备好状态,那么f.wait() 立即返回并且p 被销毁,然后另一个线程在promise 被销毁后尝试唤醒阻塞的线程。所以我认为你已经正确地发现了问题。 但是,正如堆栈跟踪所示,当它挂起时,构造函数仍在等待f.wait(),因此承诺还没有被破坏......奇怪。 @Jonathan:我怀疑(实际上更像是一个疯狂的猜测)永远不会完成的 f.wait() 是在参与比赛的人之后,并且它永远不会完成,因为之前的比赛破坏了一些东西pthread 库。 是的,我得出了同样的结论【参考方案2】:

尝试移动std::promise&lt;void&gt; p;,这样它就不是构造函数的本地成员,而是struct foo的成员:

struct foo

    foo(std::atomic<int>& count)
        : _stop(false)
    
        // std::promise<void> p;    // <-- moved to be a member
        std::future <void> f = p.get_future();

        // ...same as before...
    
    void run(std::promise<void>& p)
    
        // ... same ...
    

    std::promise<void> p;   // <---
    std::thread _thread;
    bool _stop;
;

我相信可能发生的情况是,您会陷入一场竞赛,其中p 在构造函数中被销毁,而p.set_value() 正在作用于对promise 的引用。 set_value() 在完成/清理过程中发生了一些事情;对已销毁的std::promise 的引用正在破坏pthread 库中的某些状态。

这只是猜测 - 目前我还没有准备好访问重现问题的系统。但是让p 成为成员将确保其生命周期远远超过set_value() 调用的完成。

【讨论】:

但是代码在线程调用 p.set_value() 之前并没有离开构造函数。它正在等待 @fork0:但是一旦解除阻塞,构造函数可能会在set_value() 返回之前完成。请注意,我不确定那里是否有比赛(或者标准是否允许比赛),但如果set_value 使用来自p 的某些状态之后,它看起来可能会出现问题执行任何信号将解除对未来的等待。此外,即使事实证明是这样,我也不确定这是否会是 set_value 或它所依赖的 pthread API 的实现中的某个错误,或者是 promise 如何实现的错误在此处的代码中使用。 啊,对。 OF,实现可以是依赖的。可以说,一个破碎的实现 @fork0,我认为即使有正确的实现也会有竞争,请参阅我对rodrigo's answer的第二条评论 标准要求 promise::set_value() 在更新 promise 对象 (30.6.5/2) 时持有互斥锁(或像持有互斥锁一样行事)。如果promiseset_valuepromise 对象中持有互斥体时被破坏,那么肯定会导致UB。因此,在 之后 promise::set_value() 返回并且不再使用 promise 引用之前销毁 promise 是不安全的。我同意 Jonathan 的观点,即在 promise 被销毁而 set_value() 仍处于活动状态时,不需要实现使 promise::set_value() 安全。

以上是关于pthread_once() 中的竞争条件?的主要内容,如果未能解决你的问题,请参考以下文章

pthread_once的pthread_once

pthread线程初始化(pthread_once)

Slick Code 中的竞争条件

CUDA内核中的竞争条件

如何可靠地重现此 python 代码中的竞争条件?

JavaScript 中的竞争条件与复合赋值