我发现MSDN中有关线程局部存储的初始值的矛盾。 This page说:
创建线程时,系统会为TLS分配一个LPVOID值数组,并将其初始化为NULL。
这让我相信,如果我用一个从未为同一个索引调用TlsSetValue的线程调用TlsGetValue,那么我应该得到一个空指针。
然而,程序员应该确保...在调用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。因此,程序的另一部分之前是否已经使用了一个插槽似乎无关紧要。
答案 0 :(得分:10)
当文档说系统为TLS分配的初始值为零时,文档太有用了。该陈述是真实的但没有用。
原因是应用程序可以通过调用TlsFree
释放TLS插槽,因此当您分配插槽时,无法保证您是第一个获得该插槽的人。因此,您不知道该值是由系统分配的初始值0还是由前一个插槽所有者分配的其他垃圾值。
考虑:
TlsAlloc
并获得分配的插槽1.插槽1从未使用过,因此它包含初始值0。TlsSetValue(1, someValue)
。TlsGetValue(1)
并返回someValue
。TlsFree(1)
。TlsAlloc
并获得分配的插槽1。TlsGetValue(1)
并返回someValue
,因为这是组件A留下的垃圾值。因此,程序员必须确保线程在调用TlsSetValue
之前调用TlsGetValue
。否则,您的TlsGetValue
将读取剩余的垃圾。
误导性文档说“剩余垃圾的默认值为零”,但这没有用,因为你不知道系统初始化它之间的插槽发生了什么得到了你。
Adrian的后续跟踪促使我再次研究这种情况,当组件A调用TlsFree(1)
时,内核确实将插槽清零,这样当组件B调用TlsGetValue(1)
时它就会变为零。这假设组件A中没有错误,它在TlsSetValue(1)
之后调用TlsFree(1)
。
答案 1 :(得分:2)
如果函数成功,则返回值为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
可能是完美的有效的用户设置值!)并且由您来确保您请求的索引是您想要的索引并且它包含有效数据。