6

我有一个由许多 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),则不会再出现此问题 Root.dll 和 Child.dll 的 s_threadValues 相同。

这是预期的行为吗?无论如何在使用它的动态库之间共享静态库中定义的 TLS 共享?

4

2 回答 2

7

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

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

于 2014-01-03T22:54:37.200 回答
0

虽然作者已经接受了 Hans Passant 的回答,但其中没有明显的解决方案建议。所以这就是我想出的。不是很优雅,但将部分代码切片到 Common.dll 中可能会更糟/丑陋。

通用.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 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);
    }
};

子DLL.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
        }
    }
};
于 2018-03-11T16:48:15.490 回答