【问题标题】:Thread-local singletons线程局部单例
【发布时间】:2010-11-16 15:12:13
【问题描述】:

我想创建一个单例类,在使用它的每个线程中实例化一次。我想将实例指针存储在 TLS 插槽中。我想出了以下解决方案,但我不确定当涉及线程本地存储时,多线程访问单数工厂是否有任何特殊考虑。也许还有更好的解决方案来实现线程本地单例。

class ThreadLocalSingleton 
{
    static DWORD tlsIndex;
public:
    static ThreadLocalSingleton *getInstance()
    {
        ThreadLocalSingleton *instance = 
            static_cast<ThreadLocalSingleton*>(TlsGetValue(tlsIndex));
        if (!instance) {
            instance = new ThreadLocalSingleton();
            TlsSetValue(tlsIndex, instance);
        }
        return instance;
    }
};
DWORD ThreadLocalSingleton::tlsIndex = TlsAlloc();

Tls* 函数当然是特定于 win32 的,但可移植性不是这里的主要问题。您对其他平台的想法仍然很有价值。

主要编辑:我最初询问过在这种情况下使用双重检查锁定。然而正如DavidK 指出的那样,无论如何,单例都是基于每个线程创建的。

剩下的两个问题是:

  1. 是否适合回复 TlsGetValue/TlsSetValue 以确保每个线程获得一个实例并且每个线程只创建一次实例?

  2. 是否可以注册一个回调,允许我在某个线程完成时清理与该线程关联的实例?

【问题讨论】:

    标签: c++ multithreading winapi singleton thread-local-storage


    【解决方案1】:

    查看this paper 以了解为什么双重检查锁定在一般情况下不起作用(即使它可能在特殊情况下起作用)。

    【讨论】:

    • 自 VS2005 起, volatile 关键字应该在微软编译器中解决这个问题。见en.wikipedia.org/wiki/…
    • 但是,关于 volatile 的使用,VC 一个特例。一般来说,这是行不通的。
    • 正确...在讨论 DCLP 时,本文的链接始终是有序的 :)
    【解决方案2】:

    既然你的对象是线程本地的,为什么你需要锁定来保护它们呢?每个调用 getInstance() 的线程都将独立于任何其他线程,那么为什么不检查单例是否存在并在需要时创建它呢?仅当多个线程尝试访问同一个单例时才需要锁定,这在您的设计中是不可能的,因为它是上面的。

    编辑: 继续讨论另外两个问题...我看不出使用 TlsAlloc/TlsGetValue 等无法按预期工作的任何原因。由于保存指向单例的指针的内存只能由相关线程访问,因此对其进行延迟初始化不会有任何问题。但是没有明确的回调接口来清理它们。

    对此的明显解决方案是拥有一个由所有线程主函数调用的方法,以清除创建的单例(如果有)。

    如果线程很可能会创建一个单例,一个更简单的模式可能是在线程主函数的开头创建单例并在最后删除它。然后,您可以通过在堆栈上创建单例或将其保存在 std::auto_ptr 中来使用 RAII,以便在线程结束时将其删除。 (除非线程异常终止,但如果发生这种情况,所有的赌注都关闭了,并且泄漏的对象是您的问题中最少的。)然后您可以传递单例,或将其存储在 TLS 中,或将其存储在 a 的成员中类,如果大多数线程功能都在一个类中。

    【讨论】:

      【解决方案3】:

      我们使用一个将线程 ID 映射到数据的类来实现我们的线程本地存储。这似乎工作得很好,然后可以将此类的实例放置在您需要线程本地存储的任何地方。通常客户端使用 的实例作为静态私有字段。

      这里是代码的大致轮廓

      template <class T>
      struct ThreadLocal {
          T & value()
          {
              LockGuard<CriticalSection> lock(m_cs);
      
              std::map<int, T>::iterator itr = m_threadMap.find(Thread::getThreadID());
      
              if(itr != m_threadMap.end())
                  return itr->second;
      
              return m_threadMap.insert(
                  std::map<int, T>::value_type(BWThread::getThreadID(), T()))
                      .first->second;
          }
      
          CriticalSection     m_cs;
          std::map<int, T>    m_threadMap;
      };
      

      然后将其用作

      class A {
          // ...
      
          void doStuff();
      private:
         static ThreadLocal<Foo> threadLocalFoo;
      };
      
      ThreadLocal<Foo> A::threadLocalFoo;
      
      void A::doStuff() {
          // ...
          threadLocalFoo.value().bar();
          // ...
      }
      

      这很简单,适用于任何可以获取线程 ID 的平台。请注意,关键部分仅用于返回/创建引用,一旦您拥有引用,所有调用都在关键部分之外。

      【讨论】:

      • 我也用过这种方法,效果很好。但缺点是,由于锁定和地图访问,它相对较慢。所以我现在使用前面的由线程 ID(转换为 16 位)索引的静态数组,这要快得多,因为不需要锁定它。仅当两个转换的线程 ID 之间发生冲突时,才使用映射。当然,这个解决方案也有一个缺点 - 它非常占用内存(每个单例额外 1/4MB)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-28
      • 1970-01-01
      • 2014-07-16
      相关资源
      最近更新 更多