静态类成员在线程本地存储中的销毁

7

我正在编写一个快速的多线程程序,希望避免同步(需要同步的函数每秒调用大约500万次,因此即使使用互斥锁也过于沉重)。

场景如下:我拥有一个单一的全局类实例,每个线程都可以访问它。为了避免同步,除了一些类成员之外所有类中的数据都是只读的,并且这些成员被声明在TLS中(__thread或者__declspec(thread))。

不幸的是,为了使用编译器提供的__thread接口,这些类成员必须是静态的且没有构造函数/析构函数。我使用的类当然有自定义的构造函数,因此我将指向这些类的指针(类似于static __thread MyClass* _object)声明为类成员。

然后,当第一个线程从全局实例调用一个方法时,我会执行类似于“(if _object == NULL) object = new MyClass(...)”这样的操作。

我的最大问题是:有没有明智的方法来释放这个分配的内存?这个全局类是从一个库中获取的,程序中许多线程都在使用它,每个线程以不同的方式创建(即每个线程执行不同的函数),我无法每次线程终止时都放置代码片段。

谢谢各位。


1
第一印象是您需要对系统进行设计审查。需要共享哪些数据以及哪些数据将被修改,以及何时进行修改。 - YeenFei
2
你尝试过使用 at_exit 吗?(不确定在卸载库时是否有效) - Matthieu M.
C++11中的thread_local似乎是您正在寻找的,但我知道只有gcc-4.8实现了它。 - Marc Glisse
@MarcGlisse 嗯,我在Ubuntu Linux上有gcc-4.8.2,但似乎它无法工作。 - VF1
@MarcGlisse 哦,没事了,只需使用--enable-tls即可。 - VF1
显示剩余2条评论
5个回答

3

C++11中,这很容易实现:

static thread_local struct TlsCleaner {
    ~TlsCleaner() {
        cleanup_tls();
    }
} tls_cleaner;

cleanup_tls()会在每个线程终止时执行(前提是使用C++ API创建线程,如std::thread)。

但是,您也可以直接在TLS对象的析构函数中清理它们(这也会立即执行)。例如:static thread_local std::unique_ptr<MyClass> pMyClass;将在线程终止时删除MyClass

C++11之前,您可以使用GNU“链接器集”或MSVC“_tls_used”回调等技巧。

或者,从Windows 6(Vista)开始,FlsAlloc接受一个清理回调。


2

当传递DLL_THREAD_DETACH时,TLS清理通常在DllMain中完成。

如果您的代码全部在EXE中而不是DLL中,则可以创建一个虚拟DLL,以便EXE加载该DLL并在DLL_THREAD_DETACH上回调到EXE中。(我不知道有更好的方法让EXE代码在线程终止时运行。)

DLL调用回EXE的方法有几种:一种是EXE可以像DLL一样导出函数,DLL代码可以使用GetProcAddress获取EXE的模块句柄。更简单的方法是给DLL一个初始化函数,EXE调用此函数来显式传递函数指针。

请注意,在DllMain中可以执行的操作是有限制的(不幸的是,这些限制没有得到适当的文档记录),因此应将通过此方式执行的任何工作最小化。不要运行任何复杂的析构函数;只需使用一个直接的kernel32.dll API,如HeapAlloc释放内存并释放TLS插槽。

还要注意,对于在加载DLL时已经运行的线程,您将不会收到DLL_THREAD_ATTACH(但如果它们在DLL加载时退出,则仍将收到DLL_THREAD_DETACH),并且当最后一个线程退出时,您将收到(唯一的)DLL_PROCESS_DETACH。


这正是我试图避免的。我的程序完全可移植(它可以在VC++和GCC上编译),包含TLS数据的类被编译为静态库,然后链接到主exe(将生成线程的那个)。无论如何,如果我没有得到其他有趣的答案,最终我会编写一个可移植的“线程分离”处理程序。 - Gianluca
1
@Gianluca,对我来说,使用TLS似乎本质上是不可移植的。TLS的语法和行为因编译器而异,在某些情况下甚至也因操作系统版本而异(即使我们只谈论Windows版本)。使用TLS避免了大量代码重构是有意义的(我曾经历过这种情况,特别是当你的代码通过第三方库的回调函数被调用时!),但我认为你可能需要编写某种抽象/包装器来处理编译器/平台差异(或找到现有的;也许Boost有一些TLS相关的东西?)。 - Leo Davidson
1
实际上,Boost Thread确实具有TLS的概念,您可以提供一个清理函数,在线程退出时触发-当然,您必须使用Boost Threads才能使用该功能-这取决于OP用于线程处理的是什么... - Nim
你说得没错。我目前正在使用一个简单的包装器(基于ifdef)来处理编译器语法,我想我也应该编写一个可移植的线程销毁处理程序。不幸的是,我看了一些可移植实现(Poco和Boost),但它们似乎对我的需求来说有点太慢了,而且,我也不想使用它们的线程接口(出于你所说的同样原因:我不能重构成千上万行的代码)。 - Gianluca

2
如果你只是想要一个通用的清理函数,你仍然可以使用boost thread_specific_ptr。你不需要实际上使用存储在那里的数据,但是你可以利用自定义的清理函数。只需将该函数设置为任意值,你就可以做任何想做的事情。查看pthread函数

pthread_key_create

以获取直接的pthreads函数调用。
不幸的是,至少我还没有找到一种简单的方法来在线程退出时删除复杂对象。但是,这并不能阻止你自己去做。
你需要在线程退出时注册自己的处理程序。在pthread中,这将是pthread_cleanup_push。我不知道这在Windows上是什么。当然,这不是跨平台的。但是,可能你有完全控制线程和其入口点的权限。你可以在从线程返回之前显式调用清理函数。如果你无法添加此代码段,请调用特定于操作系统的函数以添加清理例程。
显然,为所有已分配的对象创建清理函数可能会很烦人。因此,你应该创建一个更多的线程本地变量:一个对象的析构函数列表。对于你创建的每个线程特定变量,你都会将一个析构函数推送到这个列表中。如果你没有公共的线程入口点,你将需要根据需要创建这个列表:有一个全局函数来调用,它接受你的析构函数,并在必要时创建列表,然后添加析构函数。
这个析构函数的具体形式取决于对象层次结构(你可能有简单的boost绑定语句,shared_ptr,在基类中的虚拟析构函数,或者两者的组合)。
你的通用清理函数可以遍历此列表并执行所有析构函数。

除非您有重度性能要求,否则请查看Boost Thread Local的答案,它可能也适用。 - edA-qa mort-ora-y
已经试图对此进行注释,如果您有高性能要求,则不能频繁生成新线程,这会降低性能。而如果您不频繁生成新线程,则每个线程的 TSL 初始化成本是完全可以忽略不计的。 - Andriy Tylychko
thread_specific_ptr 的使用比本地线程局部说明符更高。 - edA-qa mort-ora-y

1

1

我已经看了一下,对于一些普通情况来说似乎还不错。巨大的优势是线程本地数据结构的良好面向对象抽象。缺点是它不会利用链接时的线程本地存储,因此在支持该功能的平台上性能不会达到最优。 - edA-qa mort-ora-y
你的意思是在频繁创建线程时改善TLS性能吗?在所有其他情况下,TLS将只被创建和初始化一次。而频繁创建线程的情况应该通过线程池来改善。 - Andriy Tylychko

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