【问题标题】:How to call not-thread safe DLL in multi-thread in C++?如何在 C++ 中的多线程中调用非线程安全的 DLL?
【发布时间】:2014-01-01 03:20:59
【问题描述】:

我想与线程(而不是多进程)并行化使用 非线程安全的 DLL (Compute.dll) 的本机 C++ 代码。

实际上,我有一种可以并行化的循环:

for(int i = 0; i < x; i++) {
    ComputeDLL.DoWork(i); // DLL API Call not thread-safe
}

我确定了一种并行化本机代码的方法:克隆并重命名 Compute1.dll、Compute2.dll、...、ComputeN.dll 中的 Compute.dll 并按线程使用一个 dll。 因此,对于链接,我必须以同样的方式在 Compute1.lib、Compute2.lib、...、ComputeN.lib 中复制 Compute.lib

使用此解决方案,我必须在我的应用程序中复制代码以定义多个 ComputeDLL 类:ComputeDLL1、ComputeDLL2、...带有显式静态链接的 ComputeDLLN:

#pragma comment(lib,'Compute1.lib'); // for ComputeDLL1.h
#pragma comment(lib,'Compute2.lib'); // for ComputeDLL2.h
etc.

你能告诉我这个解决方案是否有效吗?

在这个解决方案中:

  • 编译前必须知道线程数
  • 我有太多重复的代码

是否有另一种更清洁的好方法来解决我的问题? 我可以使用 LoadLibrary() 吗?

谢谢

Nb:我不想使用多重处理,因为在我的实际情况下,我必须将大数据作为参数发送到我的 DLL(所以我不想使用文件进行通信,因为我需要性能)和DoWork 非常快(10 毫秒)。我想在没有套接字、Windows 消息队列等的情况下轻松地在内存中工作......将来,我可能需要线程之间的自定义同步 => 多线程模式对我来说是最好的

【问题讨论】:

  • 许多流程将是最安全的(也是最简单的)——为什么要做困难的事情?
  • 因为我必须将参数中的大数据发送到我的 DLL(所以我不想使用文件进行通信,因为我需要性能)并且 DoWork 非常快(10 毫秒)。我想在没有套接字、Windows 消息队列等的情况下轻松地在内存中工作......将来,我可能需要线程之间的自定义同步 => 多线程模式对我来说是最好的
  • 大量进程,不相互交谈,是最安全和最简单的。
  • 我的问题没有答案?

标签: c++ windows linker thread-safety


【解决方案1】:

使用许多 DLL 有几个缺点:

  • 您会遇到重复符号的问题。那就是链接器很难知道函数调用指向哪个 dll
  • 当 DLL 使用一些共享的全局数据时,仍然会出现线程问题。
  • 每个线程至少需要一个 DLL,这意味着如果您希望它与任意数量的线程一起工作,则每次都必须复制 DLL。非常笨重。

重复符号问题的解决方案是为您创建的每个线程使用 LoadLibrary / GetProcAddress:

  • 当线程启动时,您必须将 LoadLibrary 调用到之前未加载的 DLL
  • 然后使用 GetProcAddress 设置 thread-local 函数指针
  • 从现在开始,您将不得不使用这些线程局部函数指针来调用 DLL 中的函数

当您加载的 DLL 将加载另一个 DLL 时,这可能是个坏主意。 在这种情况下,wsock32 加载 ws2_32.dll。有问题的:

In [1]: import ctypes

In [2]: k=ctypes.windll.kernel32

In [3]: a=k.LoadLibraryA('wsock32_1.dll')

In [4]: b=k.LoadLibraryA('wsock32_2.dll')

In [5]: a
Out[5]: 1885405184

In [6]: b
Out[6]: 1885339648

In [7]: k.GetProcAddress(a, "send")
Out[7]: 1980460801

In [8]: k.GetProcAddress(b, "send")
Out[8]: 1980460801

这里从wsock32.dll 的单独副本加载的send 将指向相同的发送函数,因为 wsock32.dll 只是 ws2_32.dll 的蹦床。

但是,当您加载 ws2_32 时,您会获得不同的发送入口点。

In [1]: import ctypes

In [2]: k=ctypes.windll.kernel32

In [3]: a=k.LoadLibraryA('ws2_32_1.dll')

In [4]: b=k.LoadLibraryA('ws2_32_2.dll')

In [5]: a
Out[5]: 1874853888

In [6]: b
Out[6]: 1874591744

In [7]: k.GetProcAddress(a, "send")
Out[7]: 1874882305

In [8]: k.GetProcAddress(b, "send")
Out[8]: 1874620161

附加信息:LoadLibrary 将 dll 加载到调用进程的地址空间。 LoadLibrary记住你已经加载了一个 dll,所以通过使用不同的 dll 名称你可以强制 loadlibrary 将同一个 dll 加载到进程地址空间的不同位置。

更好的解决方案是从内存中手动加载 DLL 代码,这样可以省去在磁盘上维护同一个 dll 的不同副本的麻烦。

http://www.joachim-bauch.de/tutorials/loading-a-dll-from-memory/

【讨论】:

  • 非常感谢您的这些警告!
  • 最后,我将尝试使用Boost.Interprocess进行多进程和共享内存通信的另一种解决方案
【解决方案2】:
#pragma comment(lib,'Compute1.lib'); // for ComputeDLL1.h
#pragma comment(lib,'Compute2.lib'); // for ComputeDLL2.h

这将导致链接阶段出现重复符号。

但是,是的,您的想法可以通过LoadLibrary() 为每个线程分别动态加载dll 并通过GetProcAddress() 动态解析库函数来实现。正如您上面建议的那样,您必须为每个线程(具有不同的名称)拥有不同的 dll 文件。

【讨论】:

    【解决方案3】:

    流程方法是最简单的,但您说您有很多数据要传输。既然您可以控制调用者,那么让我尝试一个涉及 IPC(进程间通信)的答案。有multiple ways to do that

    这是一个如何使用命名管道的示例。

    1. 创建加载单线程 DLL 的单线程包装应用程序。
    2. 让包装器从命令行读取连接名称。
    3. 听那个连接。
    4. 反序列化传入的内容,调用 DLL,如果需要,将结果序列化回来。
    5. 添加一条特殊消息,用于拆除包装器进程。

    序列化有它自己的一系列问题,但是如果你可以假设两个进程都在同一个主机上并且是相同的(都是 32 位或两者都是64 位)。如果你不能做出这样的假设,试试protocol buffers

    使用这种方法,调用者将创建一个进程,并传递该进程rendez-vous

    wrapper.exe "\\.\wrapper_1"
    wrapper.exe "\\.\wrapper_2"
    wrapper.exe "\\.\wrapper_3"
    

    要调用 DLL 的实例 1,您只需序列化数据并将其写入 \\.\wrapper_1

    当我不得不这样做时,我在代码中重新创建了 DLL 的接口,这样我就可以用 #define 语句和重建的真实 DLL 替换包装器。它将有助于调试调用者,并隔离 IPC 问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-30
      相关资源
      最近更新 更多