【问题标题】:Destruction of static class members in Thread local storage线程本地存储中静态类成员的销毁
【发布时间】:2011-06-03 06:49:36
【问题描述】:

我正在编写一个快速的多线程程序,我想避免同步(需要同步的函数必须每秒调用 5,000,000 次,所以即使是互斥锁也太重了)。

场景是:我有一个类的单个全局实例,每个线程都可以访问它。为了避免同步,类中的所有数据都是只读访问的,除了一堆类成员,然后在 TLS 中声明(使用 __thread 或 __declspec(thread))。

不幸的是,为了使用编译器提供的 __thread 接口,类成员必须是静态的并且没有构造函数/解构函数。我使用的类当然有自定义构造函数,所以我声明,作为类成员,一个指向该类的指针(类似于 static __thread MyClass* _object)。

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

我最大的问题是:有没有一种聪明的方法来释放这个分配的内存?这个全局类来自一个库,它被程序中的许多线程使用,每个线程都以不同的方式创建(即每个线程执行不同的函数),我不能每次都放一段代码线程将终止。 谢谢各位。

【问题讨论】:

  • 第一印象是您需要对系统进行设计审查。哪些数据需要共享,哪些数据将被修改,何时发生。
  • 不幸的是,我不是从头开始设计系统,但恕我直言,这是实现我的结果的最小干扰方式(就“没有改变接口”而言)
  • 你试过at_exit 吗? (不知道卸载库时是否有效)
  • Ehehe,这太容易了:-) 我的程序是一种守护进程(便携式 Win32/Linux),所以它基本上不会被杀死,但是线程会不断创建/销毁,并且每个线程创建基本上意味着巨大的内存泄漏。
  • C++11 thread_local 似乎是您正在寻找的,除了 gcc-4.8 是我所知道的唯一实现它的编译器。

标签: c++ multithreading synchronization


【解决方案1】:

TLS 清理通常在通过 DLL_THREAD_DETACH 时在 DllMain 中完成。

如果您的代码全部在 EXE 中而不是 DLL 中,那么您可以创建一个虚拟 DLL,EXE 加载该虚拟 DLL,然后调用 DLL_THREAD_DETACH 上的 EXE。 (我不知道在线程终止时运行 EXE 代码的更好方法。)

DLL 回调EXE 的方法有两种:一种是EXE 可以像DLL 一样导出函数,DLL 代码可以在EXE 的模块句柄上使用GetProcAddress。一个更简单的方法是给 DLL 一个 init 函数,EXE 调用它来显式传递一个函数指针。

请注意,您可以在 DllMain 中执行的操作是有限的(不幸的是,这些限制没有正确记录),因此您应该尽量减少以这种方式完成的任何工作。不要运行任何复杂的析构函数;只需使用 HeapAlloc 等直接 kernel32.dll API 释放内存并释放 TLS 插槽。

另请注意,您不会获得 DLL_THREAD_ATTACH 用于在加载 DLL 时已在运行的线程(但如果它们在加载 DLL 时退出,您仍将获得 DLL_THREAD_DETACH),并且您将获得(仅)最终线程退出时的 DLL_PROCESS_DETACH。

【讨论】:

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

如果您使用 pthreads,您可以查看清理操作吗?

http://man.yolinux.com/cgi-bin/man2html?cgi_command=pthread_cleanup_push

您可以在创建线程本地对象后立即推送清理操作,这样在退出时该对象就会被销毁。不确定 winapi 等价物是什么...

【讨论】:

    【解决方案3】:

    如果你只想要一个通用的清理函数,你仍然可以使用 boost thread_specific_ptr。您不需要实际使用存储在那里的数据,但您可以利用自定义清理功能。只需使该功能具有任意性,您就可以做任何您想做的事情。查看 pthread 函数 pthread_key_create 是否可以直接调用 pthreads 函数。


    不幸的是,没有简单的答案,至少我还没有遇到过。也就是说,没有常见的方法可以在线程退出时删除复杂对象。然而,没有什么能阻止你自己做这件事。

    您需要在线程退出时注册自己的处理程序。使用 pthread_cleanup_push 的 pthread。我不知道它在windows上是什么。这当然不是跨平台的。但是,大概您可以完全控制线程的启动及其入口点。您可以在从线程返回之前简单地显式调用清理函数。 我知道你提到你不能添加这个 sn-p,在这种情况下你将被留下调用操作系统特定的函数来添加一个清理例程。

    显然,为所有分配的对象创建清理函数可能很烦人。因此,您应该再创建一个线程局部变量:对象的析构函数列表。对于您创建的每个特定于线程的变量,您都会将析构函数推送到此列表中。如果您没有公共线程入口点,则必须按需创建此列表:有一个要调用的全局函数,该函数接受您的析构函数并根据需要创建列表,然后添加析构函数。

    这个析构函数的具体外观很大程度上取决于您的对象层次结构(您可能有简单的 boost 绑定语句、shared_ptr、基类中的虚拟析构函数或它们的组合)。

    然后,您的通用清理函数可以遍历此列表并执行所有析构函数。

    【讨论】:

    • 查看 Boost Thread Local 的答案,除非您有很高的性能要求,否则它也可以工作。
    • 已经尝试对此发表评论,如果您对性能有很高的要求,则不能频繁地生成新线程,这会降低性能。如果您不经常产生新线程,则 TSL 初始化(每个线程执行一次)的成本完全可以忽略不计
    • thread_specific_ptr的use高于本机线程本地说明符。
    【解决方案4】:

    【讨论】:

    • 我看过这个,对于一些一般情况来说似乎没问题。巨大的优势是线程本地数据结构的一个很好的 OOP 抽象。缺点是它不会利用链接时线程本地存储,因此在支持它的平台上性能不会是最佳的。
    • 您的意思是在频繁创建的线程上提高 TLS 性能吗?在所有其他情况下,TLS 将被创建和初始化一次。而频繁创建线程的情况应该通过线程池来改善
    【解决方案5】:

    在 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,它接受清理回调。

    【讨论】:

      猜你喜欢
      • 2012-09-11
      • 2015-07-23
      • 2018-08-13
      • 1970-01-01
      • 1970-01-01
      • 2018-05-02
      • 1970-01-01
      • 1970-01-01
      • 2012-11-17
      相关资源
      最近更新 更多