У меня есть игра, состоящая из многих файлов DLL. Некоторые из этих библиотек DLL связаны с одной и той же статической библиотекой (LIB).
Так что-то вроде этого:
Game.exe -> Root.dll -> Child.dll
| |
| '-> Common.lib (contains __declspec(thread))
|
'-> Common.lib (contains __declspec(thread))
Root.dll загружает Child.dll, которая статически связывает Common.lib. 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, определенный в статической библиотеке, совместно используемой динамическими библиотеками, использующими его?
Это совершенно нормально. Каждая DLL имеет свою собственную копию кода библиотеки и данных. И собственный поток — местное государство.
Вы можете обосновать это, предполагая, что это будет работать так, как вы ожидали. Тогда две DLL могут случайно иметь свои собственные локальные переменные потока, совместно используемые разными DLL. Понятно, что это будет иметь катастрофические последствия. Это не может работать таким образом, потому что нет механизма для совместного использования экземпляра TLS между модулями. Индексы слотов намеренно остаются закрытыми для модуля, нет механизма для получения индекса слотов переменной __declspec (thread). Явный вызов TlsAlloc () и совместное использование индекса — это обходной путь. Не ходи туда.
Хотя автор принял ответ Ханса Пассанта, в нем нет очевидного предложения по решению. Так вот что я придумал. Не очень элегантно, но нарезка части кода в Common.dll может быть еще хуже / уродливее.
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 Studio\VC\crt\src\vcruntime\tlssup.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);
}
};
#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();
...
}
#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
}
}
};