【问题标题】:Thread-safe lazy creation with TBB?使用 TBB 进行线程安全的延迟创建?
【发布时间】:2012-02-18 22:26:01
【问题描述】:

在我的 C++ 代码中,我保留了一个指向对象的指针 应该懒惰地创建,即仅应请求创建。 我有以下代码,这显然不是线程安全的。

LAZY* get_lazy()
{
    if (0 == _lazy)
        _lazy = create_lazy();
    return _lazy;
}

我想知道我应该在这里使用哪种同步? 我知道 Boost.thread 支持一次性初始化。 但我希望有一个仅使用 TBB + C++ 的简单解决方案。 我还应该注意...

  • 我无法将_lazy 创建为静态对象(我实际上想保留此类延迟创建的对象的无限数组)
  • 此类LAZY 对象不能过度分配(创建非常昂贵)

【问题讨论】:

    标签: c++ multithreading thread-safety tbb


    【解决方案1】:

    您需要一个本地互斥体 (tbb::mutex),以确保您只创建一次惰性对象。

    #include <tbb/mutex.h>
    
    tbb::atomic<LAZY*> _lazy;
    tbb::mutex myMutex;
    
    LAZY* GetLazy()
    {
      if (0 == _lazy)
      {
        myMutex.lock();
        if (0 == _lazy)
            _lazy = create_lazy();
        myMutex.unlock();
      }
      return _lazy;
    }
    

    【讨论】:

    • 在这种情况下,我想如果我将myMutex 声明为GetLazy() 中的静态变量,还是可以的。我说的对吗?
    • static 函数变量的构造函数不是线程安全的,所以你绝对不应该这样做。
    • 静态是线程安全的,但不是惰性的。如果你想要 _lazy static,myMutex 必须是静态的,但你说你不想要那个。这种以线程安全方式进行的延迟初始化问题正是所谓的单例模式。很多地方都讨论过很多次了。
    【解决方案2】:

    偶尔多次拨打create_lazy 可以吗?如果是这样,这是一个非常轻量级、仅使用 TBB 的高效解决方案:

    tbb::atomic<LAZY*> lazy;
    
    if(!lazy)
    {
        LAZY *newlazy = create_lazy();
    
        if(lazy.compare_and_swap(newlazy, 0))
        {
            // lazy was initialized elsewhere.
            delete newlazy;
        }
    }
    
    // use lazy.
    

    这将比 Maciej 的解决方案产生更少(零!)的开销,但只有在线程之间在该特定变量上存在争用的情况下偶尔多次调用 create_lazy 时才有效。

    p>

    避免互斥体和多次调用create_lazy 的一种方法是使用自旋循环。如果存在争用,这将使用比互斥锁更多的 CPU,但开销仍然很低:

    tbb::atomic<LAZY*> lazy;
    static int sentry;
    
    if(!lazy && !lazy.compare_exchange((LAZY*)&sentry, 0))
    {
        // lazy is set to a sentry value while being allocated.
        try{ lazy = create_lazy(); }
        catch(...) { lazy = 0; throw; }
    }
    else
    {
        // yield the thread while lazy is still set to the sentry.
        while(lazy == (LAZY*)&sentry)
        {
            tbb::this_tbb_thread::yield();
        }
    }
    
    // use lazy.
    

    【讨论】:

    • 对不起,我忘了说这样的LAZY对象不能被过度分配,因为创建它们非常昂贵,而且它们会使用很多其他资源。
    • 允许多次创建对象是不好的做法,并且会使代码不是线程安全的。锁定的开销可以忽略不计,因为它只会在第一次创建期间发生一次。因此,请仔细检查。
    • @Maciej 该对象没有被多次创建。可能会创建两个单独的对象,但最终只会使用其中一个。互斥体对于只能使用一次的东西来说是一个很大的存储开销,所以我试图避免这种情况。
    • 这总是不好的做法。您最终可能会有两个线程访问不同的 _lazy 对象。 Mutex 正是为此目的而设计的 - 访问关键部分,在这种情况下创建 _lazy。即使只使用了一个对象,就像你说的那样,双重创建也很昂贵,尤其是在这种特殊情况下。
    • 请注意,我说的是本地互斥锁,而不是进程间互斥锁。
    【解决方案3】:

    您还可以查看 TBB 内部如何解决此问题。代码中要搜索的名字是atomic_do_once;它是用于延迟初始化的内部(在撰写本文时)TBB 函数。这个函数和辅助东西的定义在src/tbb_misc.h,其他文件也有几个地方用到。

    基本思想与@CoryNelson 的答案相同,但在三态标志的帮助下进行了概括(请参阅enum do_once_state)。需要创建一个tbb::atomic&lt;do_once_state&gt; 类型的静态变量,并将它与应该运行一次的函数/仿函数一起传递给对atomic_do_once 的调用。例如:

    void initialize_once();
    static tbb::atomic<tbb::internal::do_once_state> init_state;
    /*...*/
    // Safe to execute concurrently
    tbb::internal::atomic_do_once( &initialize_once, init_state );
    

    对于长时间运行的初始化,最好使用@MaciejDopieralski 推荐的tbb::mutex,因为它通过使等待线程进入睡眠状态来避免过多的 CPU 使用。请注意,TBB 中的大多数其他互斥体风格也会旋转,而不是睡眠。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-27
      • 1970-01-01
      • 1970-01-01
      • 2021-07-16
      • 2023-04-07
      相关资源
      最近更新 更多