【问题标题】:Is std::call_once reentrant and thread safe?std::call_once 可重入和线程安全吗?
【发布时间】:2014-05-06 17:33:21
【问题描述】:

std::call_once 是线程安全的,但它也可以重入吗?

我使用 VS2012(调试和发布)进行的测试表明,从单个线程递归调用 std::call_once 是可以的,但如果在单独的线程上进行调用,则会导致死锁。这是std::call_once 的已知限制吗?

#include "stdafx.h"

#include <iostream>
#include <mutex>
#include <thread>

void Foo()
{
    std::cout << "Foo start" << std::endl;

    std::once_flag flag;
    std::call_once( flag, [](){
        std::cout << "Hello World!" << std::endl;
    });

    std::cout << "Foo end" << std::endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    // Single threaded Works
    {
        std::once_flag fooFlag;
        std::call_once( fooFlag, Foo);      
    }

    // Works
    // Threaded version, join outside call_once
    {
        std::once_flag fooFlag;
        std::thread t;
        std::call_once( fooFlag, [&t](){
            t = std::thread(Foo);
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        }); 
        t.join();
    }

    // Dead locks
    // Threaded version, join inside call_once
    {
        std::once_flag fooFlag;
        std::call_once( fooFlag, [](){
            auto t = std::thread(Foo);
            t.join();
        });     
    }

    return 0;
}

似乎std:call_once 正在锁定一个静态互斥锁,该互斥锁在函数退出之前不会解锁。在单线程情况下,它可以工作,因为在第二次调用时,该线程已经拥有锁。在线程版本上,它将阻塞,直到第一个调用退出。

我还注意到,如果将Foo() 函数中的std::once_flag 标志更改为static,死锁仍然会发生。

【问题讨论】:

标签: c++ c++11


【解决方案1】:

这里不是“答案”,但至少确认 Visual Studio 方面似乎有问题,而不是您的一般使用模式。

我在 coliru.stacked-crooked.com 上编译并运行了这个示例,它似乎按预期执行,没有任何死锁。

这是编译行:

g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

这是输出:

Foo start
Hello World!
Foo end
Foo start
Hello World!
Foo end
Foo start
Hello World!
Foo end

在我指责库实现之前,我个人倾向于认为我自己的代码存在问题。

我确实看到那里有一些类似的问题 w.r.t. Visual Studio 的 call_once 实现:

https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=3&ved=0CDwQFjAC&url=https%3A%2F%2Fconnect.microsoft.com%2FVisualStudio%2Ffeedback%2Fdetails%2F811192%2Fstd-call-once-hangs&ei=cVw0U-rZBor-2gXCh4HYDw&usg=AFQjCNGDikvgq8YHVzWLC-BqjeOSN-Vm3Q&sig2=7BtB2jH8DeTKEwzAOAf9lw&bvm=bv.63808443,d.b2I&cad=rja

http://www.unknownerror.org/Problem/index/1100762158/why-does-this-c-static-singleton-never-stop/

我很想知道这个问题是发生在 Debug 还是 Release 中还是两者都发生?

毕竟,ISO 标准确实声明 call_once 是线程安全的!

【讨论】:

    【解决方案2】:

    最接近指定此标准的标准是 17.6.5.8 [reentrancy]

    1 - 除非在本标准中明确指定,否则标准 C++ 库中的哪些函数可以递归重新输入是由实现定义的。

    不幸的是,call_once 的规范没有说明它是递归的(或跨线程递归),线程支持库的序言也没有说明这个话题。

    也就是说,VC++ 实现显然不是最理想的,尤其是可以使用condition_variable 编写call_once 的用户态版本:

    #include <mutex>
    #include <condition_variable>
    
    struct once_flag {
      enum { INIT, RUNNING, DONE } state = INIT;
      std::mutex mut;
      std::condition_variable cv;
    };
    template<typename Callable, typename... Args>
    void call_once(once_flag &flag, Callable &&f, Args &&...args)
    {
      {
        std::unique_lock<std::mutex> lock(flag.mut);
        while (flag.state == flag.RUNNING) {
          flag.cv.wait(lock);
        }
        if (flag.state == flag.DONE) {
          return;
        }
        flag.state = flag.RUNNING;
      }
      try {
        f(args...);
        {
          std::unique_lock<std::mutex> lock(flag.mut);
          flag.state = flag.DONE;
        }
        flag.cv.notify_all();
      }
      catch (...) {
        {
          std::unique_lock<std::mutex> lock(flag.mut);
          flag.state = flag.INIT;
        }
        flag.cv.notify_one();
        throw;
      }
    }
    

    请注意,这是一个细粒度的实现;也可以编写一个粗粒度的实现,在所有一次标志中使用一对互斥锁和条件变量,但是您需要确保在抛出异常时通知所有等待线程(例如,libc++ 这样做) .

    为了提高效率,您可以将once_flag::state 设为原子并使用双重检查锁定;为简洁起见,此处省略。

    【讨论】:

    • @dyp 对不起,我打算用原子写一个适当的双重检查锁,然后忘记了。我会删除它。
    • 另外:(现在)的条件变量是什么? this 还不够吗?
    • @dyp 调用f 时互斥锁未锁定。但你是对的,使用recursive_mutex 要简单得多;我只是倾向于忘记 C++11 有递归互斥锁。
    • @dyp (& ecatmur) - 为什么实现需要递归互斥锁(而不是普通互斥锁)?我可以想象在传递给“外部” call_once() 的函子内使用带有相同 one_flag 的 call_once(),但这似乎不是一个合理的场景......
    • @dyp BTW - 你的示例代码,当转换为使用 std::call_once 和 std::once_flag 在我尝试过的任何编译器中抛出一个 std::system_error ,所以我猜递归调用是像一个边缘案例。但是对于 pthread_once(),文档似乎表明递归调用将以死锁结束。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-24
    • 1970-01-01
    • 2017-12-23
    • 2017-05-08
    • 1970-01-01
    相关资源
    最近更新 更多