多个动态链接库(DLL)是否可以从静态库(LIB)共享线程本地存储



我有一个由许多DLL文件组成的游戏。其中一些DLL链接到同一个静态库(LIB)。

这样的东西:

Game.exe -> Root.dll -> Child.dll
|            |
|            '-> Common.lib (contains __declspec(thread))
|
'-> Common.lib (contains __declspec(thread))

Root.dll加载静态链接Common.lib的Child.dll。Root也静态链接Common.lib。因为Common是静态链接的,所以它会直接编译到加载dll中(例如Root和Child)。

Common.lib包含一个使用线程本地存储(TLS)的变量。

__declspec(thread) static void* s_threadValues[PlatformThreadSlotsMax];

这会导致一些有问题的行为:Root.dll和Child.dll都包含TLS数据(s_threadValues)的不同实例。即使在同一个线程上,如果Root.dll调用Common.lib中定义的函数,s_threadValues的值也会与从Child.dll调用相同函数时的值不同。

由于两个DLL都是从同一个线程访问此TLS,我希望TLS是共享的,但事实并非如此。

现在,如果我将Common.lib更改为动态链接(例如Common.dll),则不会再出现问题:s_threadValues对于Root.dll和Child.dll都是相同的。

这是预期的行为吗?有没有在使用TLS的动态库之间共享的静态库中定义TLS共享?

这是完全正常的。每个DLL都有自己的库代码和数据副本。以及它自己的线程本地状态。

你可以通过假设它会按照你预期的方式工作来解释这一点。然后,两个DLL可能会意外地在不同的DLL之间共享它们自己的线程本地变量。显然,这将是灾难性的。它不能以这种方式工作,因为没有跨模块共享TLS实例的机制。插槽索引有意保留为模块专用,没有机制可以获得__declspec(线程)变量的插槽索引。显式调用TlsAlloc()并共享索引是一种解决方法。不要去那里。

虽然作者接受了Hans Passant的回答,但其中没有明显的解决方案建议。虽然不是很优雅,但将部分代码切片到Common.dll中可能会更糟糕/更难看。

Common.h

class IContext
{
public:
static thread_local IContext* g_ctx;
virtual void setThreadContext() = 0;
virtual void print(int) = 0;
};
// Example
class Log
{
public:
// This code is static so will be compiled in each module. Thus
// gets access to different "g_ctx" per thread per module
// With TlsAlloc() approach we need another static variable for index, 
// while thread_local will get it from _tls_index
// (see Visual StudioVCcrtsrcvcruntimetlssup.cpp)
//
// mov r9d, dword ptr [_tls_index (07FEE05E1D50h)]
// mov rax, qword ptr gs:[58h]
// mov rsi, qword ptr [rax+r9*8]  // g_ctx address is now in rsi
static void print(int x)
{
IContext::g_ctx->print(x);
}
};

ChildDLL.cpp

#include "Common.h"
thread_local IContext* IContext::g_ctx = nullptr;
DLLEXPORT void setDllThreadContext(IContext* ctx)
{
// sets g_ctx in this module for current thread
IContext::g_ctx = ctx;
}
DLLEXPORT void runTask(IContext* ctx)
{
createThread(&someThreadFunc, ctx);
}
void someThreadFunc(IContext* ctx)
{
// will call setDllThreadContext() above
ctx->setThreadContext();
...
}

main.cpp(根exe或dll)

#include "Common.h"
thread_local IContext* IContext::g_ctx = nullptr;
// pointers to setDllThreadContext from each loaded DLL (engine plugins use case)
/*static*/ std::vector<void(*)(IContext*)> GameEngine::modules;
class Context : public IContext
{
public:
void print(int) override {...}
void setThreadContext() override
{
g_ctx = this; // sets context for this module (where setThreadContext is compiled)
for(auto setDllContext : GameEngine::modules)
{
setDllContext(this); // sets context for module where setDllContext was compiled
}
}
};

最新更新