Windows的线程本地存储是否初始化值?

时间:2013-02-07 16:53:39

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

我发现MSDN中有关线程局部存储的初始值的矛盾。 This page说:

  

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

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

然而,

This page说:

  

程序员应该确保...在调用TlsGetValue之前调用TlsSetValue。

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

然而,second page同时强调了初始化为null的行为:

  

存储在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。因此,程序的另一部分之前是否已经使用了一个插槽似乎无关紧要。

3 个答案:

答案 0 :(得分:10)

当文档说系统为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中没有错误,它在TlsSetValue(1)之后调用TlsFree(1)

答案 1 :(得分: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上运行了实验,包括使用VS 2010编译的32位和64位测试程序。

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

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

答案 2 :(得分:1)

我没有看到矛盾。第二页只是告诉您系统将所有TLS槽初始化为0但超出一些基本边界检查,无法知道特定TLS索引是否包含有效数据(0可能是完美的有效的用户设置值!)并且由您来确保您请求的索引是您想要的索引并且它包含有效数据。