C++ Windows';s线程本地存储初始化值?

C++ Windows';s线程本地存储初始化值?,c++,winapi,thread-local-storage,C++,Winapi,Thread Local Storage,我发现MSDN中关于线程本地存储的初始值存在矛盾。说: 创建线程时,系统为TLS分配一个LPVOID值数组,这些值初始化为NULL 这让我相信,如果我从一个从未为同一个索引调用过TlsSetValue的线程中使用有效索引调用TlsGetValue,那么我应该得到一个空指针 然而,他说: 这是由程序员来确保。。。线程在调用TlsGetValue之前调用TlsSetValue 这意味着您不能依赖从TlsGetValue返回的值,除非您确定它已使用TlsSetValue显式初始化 然而,通过同时说:

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

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

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

然而,他说:


这是由程序员来确保。。。线程在调用TlsGetValue之前调用TlsSetValue

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

然而,通过同时说:

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

所以我有两条语句说数据被初始化为null(或0),还有一条语句说我必须在读取值之前显式初始化它。从实验上看,这些值似乎是自动初始化为空指针的,但我无法知道我是否只是运气好,以及是否总是这样

我试图避免使用DLL只是在DLL\u线程\u附加上进行分配。我想按照以下方式进行惰性分配:

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);
这是一种可靠和安全的模式吗?或者,在尝试读取每个线程中的插槽之前,是否必须显式初始化它


更新:文档中也说明了这一点。因此,一个插槽以前是否被程序的另一部分使用似乎无关紧要。

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

当文档说系统为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留下的垃圾值
因此,由程序员在调用
TlsGetValue
之前确保线程调用
TlsSetValue
。否则,您的
TlsGetValue
将读取剩余垃圾

有误导性的文档说“剩余垃圾的默认值为零”,但这并没有帮助,因为您不知道在系统初始化它和它最终被提供给您之间的插槽发生了什么


Adrian的后续工作促使我再次研究这种情况,事实上,当组件A调用
TlsFree(1)
时,内核将插槽归零,因此当组件B调用
TlsGetValue(1)
时,它将归零。这假设组件a中没有在
TlsFree(1)
之后调用
TlsSetValue(1)
的bug,Raymond Chen的推理非常有道理,所以我昨天接受了他的回答,但今天我在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.
}
void TlsExperiment(){
DWORD index1=::TlsAlloc();
断言(index1!=TLS_OUT_OF_索引);
LPVOID value1=::TlsGetValue(index1);
assert(value1==0);//还没有其他人使用过此插槽。
值1=重新解释强制转换(0x1234ABCD);
::tLSetValue(index1,value1);
断言(value1==::TlsGetValue(index1));
::TlsFree(index1);
DWORD index2=::TlsAlloc();
//没有任何东西需要TlsAlloc将最近释放的插槽还给我们,
//但它恰好如此,这对我们的实验很方便。
断言(index2==index1);//如果此断言失败,则实验无效。
LPVOID value2=::TlsGetValue(index2);
assert(value2==0);//如果TlsAlloc文档正确,则value2==0。
//如果是错误的,则期望值2==0x1234ABCD。
}
我在Windows7上运行了这个实验,用VS2010编译了32位和64位测试程序

结果支持TlsAlloc将值重新初始化为0的想法。我想TlsAlloc可能在做一些蹩脚的事情,比如只为当前线程调零值,但文档中明确地说了“slot”(复数),因此可以安全地假设,如果线程尚未使用slot,那么该值将为0

更新: