在动态 DLL 中使用线程私有变量?

Posted

技术标签:

【中文标题】在动态 DLL 中使用线程私有变量?【英文标题】:Using thread private variables in dynamic DLLs? 【发布时间】:2012-10-02 03:29:36 【问题描述】:

我正在寻找有关 OpenMP 的一些建议。我对线程私有变量的使用感到困惑。问题可以用下面的例子来表示:

-----------

// The code below is located the dynamically loaded DLL.
/* Global variable. */
int *p;
#pragma omp threadprivate(p)

extern "C" __declspec(dllexport) int MyFunc1(void)

  int i;
  #pragma omp parallel for
  for (i = 0; i < n; i++) 
     MyFunc2(i);
  
   return TRUE;


void MyFunc2(void)

    p  = malloc(sizeof(int));
   *p  = 0;
    printf(“value = %d”,*p);
    free(p);


-----------

在这里,我希望每个线程都有一个全局线程独立变量的单独副本,该变量将在线程的所有函数中可见。该变量将在线程内被初始化和销毁​​。

这里的“问题”是所有代码包括全局变量“p”的定义都位于动态加载的DLL(通过LoadLibrary)。

微软说http://msdn.microsoft.com/en-us/library/2z1788dd.aspx:“你不能在任何将通过 LoadLibrary 加载的 DLL 中使用 threadprivate。这包括使用 /DELAYLOAD(延迟加载导入)加载的 DLL,它也使用 LoadLibrary。” 所以,如果我没看错的话,上面的代码是不正确的——线程私有变量和动态加载的 DLL 不能混合使用。

为了验证这一点,我创建了一个动态加载 DLL 的测试项目,添加运行一个函数并行使用 threadprivate,如上所述。一切都很好!

嗯......现在我很困惑,因为那个项目不应该工作。

我真的可以在动态 DLL 中使用线程私有变量吗?或者有什么诀窍吗?

谢谢,

亚历克斯

【问题讨论】:

它没有正常工作。你预计它会失败,它并没有失败。不做你期望的事情永远不应该被描述为工作得很好。你违反了规则,它没有达到你的预期。 这就是你一开始就应该预料到的。 【参考方案1】:

MS OpenMP 实现中的threadprivate 被转换为__declspec(thread),它将声明的变量放入静态 TLS(线程本地存储)中。当程序启动时,TLS 的大小是通过考虑可执行文件所需的 TLS 大小以及所有其他隐式加载的 DLL 的 TLS 要求来确定的。当您使用LoadLibrary 动态加载另一个DLL 或使用FreeLibrary 卸载它时,系统必须检查所有正在运行的线程并相应地扩大或压缩它们的TLS 存储。根据KB118816:

这个过程对于操作系统来说管理太多,当动态加载 DLL 或代码引用数据时可能会导致异常。

对此类变量的访问被视为未定义行为。它适用于您的情况,但这并不意味着它可以随时随地使用。 Here 您可以阅读为什么它在 Windows XP/2003 和更早的 Windows 版本上很可能会失败。根据同一来源,隐式 TLS 处理在 Windows Vista 中被重写,因此 OpenMP threadprivate__declspec(thread) 从那时起应该在运行时加载的 DLL 中正常运行。建议的解决方案是改用TlsAlloc

DWORD dwTlsIdx;

extern "C" __declspec(dllexport) int MyFunc1(void)

   int i;
   #pragma omp parallel for
   for (i = 0; i < n; i++) 
      MyFunc2(i);
   
   return TRUE;


void MyFunc2(void)

    int **pp = (int **)TlsGetValue(dwTlsIdx);
    *pp = malloc(sizeof(int));
    **pp  = 0;
    printf(“value = %d”,**pp);
    free(*pp);

dwTlsIdx 应该在DllMain 中的进程附加上初始化,并调用TlsAlloc。应该在线程附加上分配足够的内存来保存int *,并且它的地址应该设置为dwTlsIdx TLS 索引的值。或者,您可以在第一次致电 MyFunc2 时进行:

void MyFunc2(void)

    int **pp = (int **)TlsGetValue(dwTlsIdx);
    if (pp == NULL)
    
       pp = malloc(sizeof(int *));
       TlsSetValue(dwTlsIdx, pp);
    
    *pp = malloc(sizeof(int));
    **pp  = 0;
    printf(“value = %d”,**pp);
    free(*pp);

请参阅here 了解更多详情(带错误检查)。

【讨论】:

我在提到的文章中没有看到与“如果没有足够的资源,功能可能会失败”有什么不同。他们没有指出任何具体问题。他们没有提到任何最近的 Windows 版本。您答案中的代码示例仍将数据存储在同一个堆中。您对malloc 的调用很容易以完全相同的方式失败。【参考方案2】:

我没有尝试使用 OMP,但我有很好的使用经验

DWORD WINAPI TlsAlloc(void);

和其他TlsXxx 函数。我很确定threadprivate(p) 是最终实现此功能的包装器。此功能在 exe、静态和动态加载的 dll 等中完美运行。

在一种情况下,我在处理过程中同时看到大约 800 个 TLS 索引。每个线程(大约 200 个线程)在其线程本地存储中都有这个数量的对象。 NT 在堆中分配缓冲区来处理这些数据。这一切都很好。

可能是在写 MSDN 文章的时候出现了一些问题,但现在很可能已经修复了。

我的 2 美分。

【讨论】:

显然它在 Windows XP 上仍然会导致问题 - 请参阅 this ticket。【参考方案3】:

赫里斯托和基里尔,

感谢您的回复。

看来微软已经解决了动态 DLL 中的 TLS 问题(至少在我使用的 Windows 7 中)。也许他们只是没有更新文档。我一直在祈祷。

我创建了一个测试项目,在其中通过 __declspec(thread) pragmas 使用 TLS 测试了这个想法(正如 Hristo Iliev 在另一篇文章中向我建议的那样)。该项目使用了大量的 TLS 变量(只是为了测试)并且一切正常。然后我在工作项目中移动了代码。这是一个加载许多动态 DLL 的大型项目(约 100 万行代码)。这一切也都在那里工作。

对于最坏的情况,我仍然坚持使用 TlsAlloc、TlsSetValue、TlsGetValue 函数的想法。但它不适合我,因为我担心这些功能会给我拥有的数字运算代码增加大量偷听。我的库只需要一些 TLS 变量,但这些变量在代码中被广泛使用,包括位于堆栈深处的低级函数。

干杯,

亚历克斯·亚姆奇科夫

【讨论】:

您可以在并行区域的开头获取指向 TLS 区域的指针,并将其向下传递到调用堆栈给使用线程私有变量的函数。隐式 TLS 访问也是有代价的 - 64 位模式下的 4 个 MOV 指令实现了 TlsGetValue(_tls_index) 的精简内联版本。

以上是关于在动态 DLL 中使用线程私有变量?的主要内容,如果未能解决你的问题,请参考以下文章

使用 gdb 调试多线程代码但无法访问私有变量?

类的私有变量和私有方法

实例方法内的私有变量为什么不会出现线程安全问题

线程局部存储区

iOS动态性 运行时runtime初探(强制获取并修改私有变量,强制增加及修改私有方法等)

多线程——volatile