6

我在 MSDN 中发现了关于线程本地存储的初始值的矛盾。 这个页面说:

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

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

但是,此页面说:

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

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

然而,第二页同时通过以下内容强化了初始化为空的行为:

存储在 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 将已分配索引的插槽归零。因此,程序的另一部分以前是否使用过插槽似乎无关紧要。

4

3 回答 3

10

当文档说系统为 TLS 分配的初始值为零时,文档太有用了。该陈述是正确的,但没有用。

原因是应用程序可以通过调用来释放 TLS 插槽TlsFree,因此当您分配一个插槽时,并不能保证您是第一个获得该插槽的人。因此,您不知道该值是系统分配的初始 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)它会归零。TlsSetValue(1)这假定它在之后调用的组件 A 中没有错误TlsFree(1)

于 2013-02-07T18:56:14.750 回答
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 的组件的风险。那将是另一个组件中的错误,但它会影响您的代码。

于 2013-02-08T17:37:34.807 回答
1

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

于 2013-02-07T17:01:47.690 回答