C++11中的动态TLS

5
我正在编写一个C++11类Foo,并且我想为每个实例提供自己的类型为Bar的线程本地存储。也就是说,我希望每个线程和每个Foo实例都分配一个Bar。
如果我使用pthread,Foo将具有非静态成员pthread_key_t类型,Foo的构造函数将使用pthread_key_create()初始化它,Foo的析构函数将使用pthread_key_delete()释放它。或者如果我只编写Microsoft Windows应用程序,我可以使用TlsAlloc()和TlsFree()完成类似的操作。或者如果我使用Boost.Thread,则Foo将具有boost::thread_specific_ptr类型的非静态成员。
然而,实际上,我正在尝试编写可移植的C++11代码。 C++11的thread_local关键字不适用于非静态数据成员。所以,如果您想要每个线程一个Bar,那么没问题,但如果您想要每个线程每个Foo一个Bar,那么就不行了。
因此,据我所知,我需要定义一个从Foos到Bars的线程本地映射,然后处理如何在销毁Foo时适当清理的问题。但在我开始之前,我希望在这里发布,希望有人能阻止我并说“有更简单的方法”。
(顺便说一句,我不使用pthread_key_create()或boost::thread_specific_ptr的原因是,如果我理解正确,它们假定所有线程都使用pthread或Boost.Thread生成。我不想对我的代码的用户如何生成线程做出任何假设。)

我开始觉得最好的方法还是使用boost::thread_specific_ptr,但是我有一些顾虑。 - slyqualin
编写一个返回指向 thread_local std::list<Bar> 或类似对象的指针的小型分配器是否足够?然后将此分配器传递给 Foo 的数据成员 std::shared_ptr。从技术上讲,存储最终当然来自非 TLS 区域。C++ 没有一种动态分配 TLS 的方法,但您也可以在分配器内部具有指向内存的 thread_local 原始指针。 - jared_schmitz
@jared_schmitz写道:“然后将此分配器传递给Foo的一个数据成员std::shared_ptr。” 那么Foo只有一个这样的数据成员,对吗?如果我理解正确,这意味着对于每个Foo,只有一个Bar(对应于在创建Foo的线程中本地列表中的条目)。但是我希望每个Foo有N个Bars,其中N是引用Foo的线程数。当然,我可能误解了,所以请纠正我。 :) - slyqualin
啊,我误解了你的问题。让我试着编写一些代码并放在一个适当的答案中。 - jared_schmitz
抱歉,“引用线程数”这个说法有些粗糙。实际上,一旦创建了Foo f,任何线程中的用户代码都可以调用f.get_bar(),该方法获取特定于f和特定于发出该调用的线程的Bar对象——同一个f上可能会有多个线程进行此类调用。 - slyqualin
显示剩余2条评论
1个回答

2
您希望Foo包含一个类型为Barthread_local变量。由于如前所述,thread_local无法应用于数据成员,因此我们必须采取更间接的方式。基本行为是对于每个Foo实例存在N个Bar实例,其中N是存在的线程数。
以下是一种效率较低的方法。通过编写更多代码,可以使其更快。基本上,每个Foo将包含一个TLS映射。
#include <unordered_map>

class Bar { ... };

class Foo {
private:
  static thread_local std::unordered_map<Foo*, Bar> tls;
public:    
  // All internal member functions must use this too.
  Bar *get_bar() {
    auto I = tls.find(this);
    if (I != tls.end())
      return &I->second;
    auto II = tls.emplace(this, Bar()); // Could use std::piecewise_construct here...
    return &II->second.second;
  }
};

引用我的原始问题:“据我所知,我需要定义一个从Foos到Bars的线程本地映射,然后处理每当Foo被销毁时如何适当清理的问题。但在我着手之前,我在这里发布帖子,希望有人能阻止我并说“有更简单的方法。””所以我同意您的建议可能会起作用,但我希望找到更简单的东西。另请参见此处 - slyqualin
编译器实现可能会使用更好的锁定方案,因此使用thread_local应该更快。顺便说一句,我相信通过将其模板化并将其放在类似指针的接口后面,就可以实现boost::thread_specific_ptr的行为,但使用C++线程。此外,关于不对用户如何生成线程做出假设,除非使用thread_local关键字,否则我认为手动编写的TLS实现不能保证正常工作。 - jared_schmitz
...而且,公平地说,对于我的建议以另一种方式构建地图(即从线程ID到Bars的每个Foo映射,而不是从Foos到Bars的每个线程映射),存在相应的问题;也就是说,我必须检测线程何时退出并删除其地图条目。我怀疑这就是为什么boost::thread_specific_ptr仅限于boost线程或在它们死亡之前调用boost::on_thread_exit()的线程的根本原因。(请参见此处。) - slyqualin
嗯,那么我认为在不知道用户如何生成线程的情况下是不可能做到的。您需要在创建或销毁线程时精确地采取行动。boost::thread_specific_ptr 特别钩入 Win32 或 pthreads。您还必须解决删除特定 Foo 的 所有 相关 TLS 数据的反向问题,其中删除发生在一个线程中。我不认为 Boost 解决方案可以做到这一点。boost::thread_specific_ptr 的析构函数会删除调用线程的 TLS,但只能调用一次。其余的在线程退出时被删除。 - jared_schmitz
boost::thread_specific_ptr 特别针对 Win32 或 pthreads 进行了钩子处理。我不知道这一点。你有参考资料吗?我想知道为什么它选择了 Win32 和 pthreads。是因为大多数 C++11 线程实现也使用这两个吗?我之所以问所有这些问题,是因为如果已知 boost::pthread_specific_ptr 可在足够广泛的平台上工作,并且我可以依赖它,那么它就是解决我的实际问题的方案。(当然,我必须向我的用户广告宣传平台列表,但只要列表涵盖几乎所有人,那就没关系了。) - slyqualin
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接