如何在C ++中调用多线程中的非线程安全DLL?

时间:2013-12-13 11:12:43

标签: c++ windows linker thread-safety

我希望与线程(不是多进程)并行化本机c ++代码,该代码使用非线程安全的DLL(Compute.dll)。

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

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

我找到了一种并行化我的本机代码的方法:克隆并重命名Compute1.dll中的Compute.dll,Compute2.dll,...,ComputeN.dll并通过线程使用一个dll。 所以对于链接,以同样的方式,我必须复制Compute1.lib中的Compute.lib,Compute2.lib,...,ComputeN.lib

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

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

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

在此解决方案中:

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

还有另一种清洁方法可以解决我的问题吗? 我可以使用LoadLibrary()?

由于

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

3 个答案:

答案 0 :(得分:3)

使用许多DLL有几个缺点:

  • 您将遇到重复符号问题。也就是说,链接器将很难知道函数调用引用的是哪个dll
  • 当DLL使用某些共享的全局数据时,仍然可能发生线程问题。
  • 每个线程至少需要一个DLL,这意味着如果您希望它可以使用任意数量的线程,则必须每次都复制DLL。非常笨重。

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

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

此处从send的单独副本加载的wsock32.dll将指向相同的发送函数,因为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加载到调用进程的地址空间。已加载dll后,LoadLibrary 会记住,因此通过使用不同的dll名称,您可以强制loadlibrary将相同的dll加载到进程地址空间中的不同位置。

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

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

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:0)

过程方法是最简单的,但是你说你有很多要转换的数据。由于您可以控制调用者,让我尝试一个涉及IPC(进程间通信)的答案。有multiple ways to do that

以下是如何使用命名管道的示例。

  1. 创建一个加载单线程DLL的线程包装器应用程序。
  2. 让包装器从命令行读取连接名称。
  3. 听取那个连接。
  4. 反序列化所发生的内容,调用DLL,如果需要,则将结果序列化。
  5. 添加一条特殊消息,用于撕下包装器进程。
  6. 序列化带来了它自己的一系列问题,但如果你可以假设两个进程都在同一个主机上并且是相同的 bitness (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问题。