【问题标题】:Does Windows's thread-local storage initialize values?Windows 的线程本地存储是否初始化值?
【发布时间】:2013-02-07 16:53:38
【问题描述】:

我在 MSDN 中发现关于线程本地存储的初始值的矛盾。 This page 说:

创建线程时,系统为 TLS 分配一个 LPVOID 值数组,初始化为 NULL。

这让我相信,如果我从一个从未为同一索引调用过 TlsSetValue 的线程使用有效索引调用 TlsGetValue,那么我应该得到一个空指针。

This page 然而,却说:

程序员有责任确保......线程在调用 TlsGetValue 之前调用 TlsSetValue。

这表明您不能依赖从 TlsGetValue 返回的值,除非您确定它已使用 TlsSetValue 显式初始化。

然而second page 同时强化了初始化为空的行为,它还说:

存储在 TLS 槽中的数据可以具有值 0,因为它仍然具有其初始值,或者因为线程调用 TlsSetValue 函数时使用 0。

所以我有两个语句说数据被初始化为 null(或 0),一个说我必须在读取值之前显式初始化它。实验上,这些值似乎确实被自动初始化为空指针,但我无法知道我是否只是幸运,以及是否会一直如此。

我试图避免使用 DLL 来分配 DLL_THREAD_ATTACH。我想按照以下方式进行惰性分配:

LPVOID pMyData = ::TlsGetValue(g_index);
if (pMyData == nullptr) {
  pMyData = /* some allocation and initialization*/;
  // bail out if allocation or initialization failed
  ::TlsSetValue(g_index, pMyData);
}
DoSomethingWith(pMyData);

这是一种可靠且安全的模式吗?或者我是否必须在尝试读取之前显式初始化每个线程中的插槽?

更新:文档还说TlsAlloc zeros out the slots for the allocated index。因此,一个插槽是否以前被程序的其他部分使用过似乎无关紧要。

【问题讨论】:

    标签: c++ winapi thread-local-storage


    【解决方案1】:

    当文档说系统为 TLS 分配的初始值为零时,它太有帮助了。这句话是对的,但没有用。

    原因是应用程序可以通过调用TlsFree 来释放 TLS 插槽,因此当您分配插槽时,并不能保证您是第一个获得该插槽的人。因此,您不知道该值是系统分配的初始 0 还是插槽的先前所有者分配的其他垃圾值。

    考虑:

    • 组件 A 调用 TlsAlloc 并获得分配的插槽 1。插槽 1 从未使用过,因此它包含其初始值 0。
    • 组件 A 调用 TlsSetValue(1, someValue)
    • 组件 A 调用 TlsGetValue(1) 并返回 someValue
    • 组件 A 已完成并调用 TlsFree(1)
    • 组件 B 调用 TlsAlloc 并获得分配的插槽 1。
    • 组件 B 调用 TlsGetValue(1) 并返回 someValue,因为这是组件 A 留下的垃圾值。

    因此,程序员应确保线程在调用TlsSetValue 之前调用TlsGetValue。否则,您的TlsGetValue 将读取剩余垃圾。

    误导性文档说“剩余垃圾的默认值为零”,但这没有用,因为您不知道从系统初始化到最终给您的时间槽发生了什么。

    Adrian 的跟进促使我再次研究情况,确实当组件 A 调用 TlsFree(1) 时内核将插槽归零,因此当组件 B 调用 TlsGetValue(1) 时它会归零。这假定组件 A 中不存在在 TlsFree(1) 之后调用 TlsSetValue(1) 的错误。

    【讨论】:

    • 很好的推理,但事实证明 TlsAlloc 会将插槽重新初始化为零,因此组件 B 将返回 0 而不是组件 A 留下的垃圾值。我添加了一个答案更多细节。
    • @Adrian 它为当前线程重新初始化槽。我相信其他螺纹仍然拧紧。 [更新:其他线程确实没有拧紧。它们在 TlsFree 时间归零。]
    • 感谢您的额外关注。我也玩了足够多的时间,看到它们在 TlsFree 上重新初始化,但我还没有检查其他线程。
    【解决方案2】:

    Raymond Chen 的推理非常有道理,所以我昨天接受了他的回答,但今天我在 TlsAlloc 的 MSDN 文档中发现了另一个关键行:

    如果函数成功,返回值是一个 TLS 索引。 索引的槽被初始化为零。

    如果 TlsAlloc 确实将插槽初始化为零,那么您不必担心该插槽的前一个用户留下的垃圾值。为了验证 TlsAlloc 确实以这种方式运行,我进行了以下实验:

    void TlsExperiment() {
      DWORD index1 = ::TlsAlloc();
      assert(index1 != TLS_OUT_OF_INDEXES);
      LPVOID value1 = ::TlsGetValue(index1);
      assert(value1 == 0);  // Nobody else has used this slot yet.
      value1 = reinterpret_cast<LPVOID>(0x1234ABCD);
      ::TlsSetValue(index1, value1);
      assert(value1 == ::TlsGetValue(index1));
      ::TlsFree(index1);
    
      DWORD index2 = ::TlsAlloc();
      // There's nothing that requires TlsAlloc to give us back the recently freed slot,
      // but it just so happens that it does, which is convenient for our experiment.
      assert(index2 == index1); // If this assertion fails, the experiment is invalid.
    
      LPVOID value2 = ::TlsGetValue(index2);
      assert(value2 == 0);  // If the TlsAlloc documentation is right, value2 == 0.
                            // If it's wrong, you'd expect value2 == 0x1234ABCD.
    }
    

    我在 Windows 7 上运行实验,使用 32 位和 64 位测试程序,使用 VS 2010 编译。

    结果支持 TlsAlloc 将值重新初始化为 0 的想法。我想 TlsAlloc 可能会做一些蹩脚的事情,比如仅为当前线程清零值,但文档明确表示“插槽”(复数),所以假设如果您的线程尚未使用插槽,则该值将为 0,这似乎是安全的。

    更新:进一步的实验表明,将插槽重新初始化为 0 的是 TlsFree 而不是 TlsAlloc。因此,正如 Raymond 所指出的,另一个名为 TlsSetValue 的组件仍然存在风险释放一个槽之后。这将是其他组件中的错误,但会影响您的代码。

    【讨论】:

      【解决方案3】:

      我看不出有什么矛盾。第二页只是告诉您系统将所有 TLS 插槽初始化为 0,但除了一些基本的边界检查之外,无法知道特定 TLS 索引是否包含有效数据(0 可能是一个完全有效的 set-by -the-user 值!)并且由您来确保您请求的索引是您想要的索引并且它包含有效数据。

      【讨论】:

      • “由程序员来确保......线程在调用 TlsGetValue 之前调用 TlsSetValue”,这部分听起来很矛盾。
      猜你喜欢
      • 1970-01-01
      • 2014-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-10
      • 2020-09-28
      • 2016-06-28
      相关资源
      最近更新 更多