当前位置: 首页 > 科技 > 人工智能 > 线程安全:局部静态变量的初始化_腾讯新闻

线程安全:局部静态变量的初始化_腾讯新闻

天乐
2020-12-25 05:19:12 第一视角

一个简单的例子

关于代码块中的静态变量(相对于全局范围中的静态变量)的一个原则是:当代码执行流第一次运行到静态变量的声明处时才会进行初始化,且仅此一次。

下面的代码,会存在潜在的竞争条件:

上面代码的本意是:只在第一次函数被调用的时候,才进行耗时久的计算,下一次则直接返回一个缓存值。

问题在于,这段代码不是线程安全的。代码块中的静态变量会被编译器内部转换为如下的代码:

通过上面的代码,我们就能很容易地看出来这个潜在的竞争条件了。

假设有两个线程,都是第一次调用这个函数。第一个线程执行到了”cachedResult_computed = true”,然后系统将它的时间片抢占并执行第二个线程。第二个线程看到,此时cachedResult_computed为true,则它会跳过下面if语句并返回一个未初始化的变量。

上面这个行为,并不是一个编译器Bug,这个行为是定义在C++标准里的。

基于上面的代码,我们还可以编写更加糟糕的竞争条件代码,如下图所示:

编译器将上面的代码翻译如下:

请注意了,上面的代码中,存在多个竞争条件。和之前类似,有可能一个线程比另一个线程早运行,并使用了为初始化的”s”。

更加糟糕的是,还有一种可能是:就在第一个线程刚刚完成s_constructed的判断且没有将它设置为true这个时间点时,系统就将执行流切换到另一个线程。这种情况下,对象s会自行构造函数和析构函数两次,那可不是什么好事儿。

但是,等等,这好像还不是全部的呢。

另一个例子

考察下面的代码,我们定义了两个局部的静态变量:

下面是编译器翻译的代码:

编译器为了节省空间,它将两个”x_constructed”放入到了一个位域(BitField)中。这就可能导致在变量”constructed”上发生多次非锁定情况下的读取-写入-存储操作。

让我们假设,如果一个线程尝试执行”constructed |= 1″的同时,另一个线程尝试执行”constructed |= 2″,会发生什么呢?

在x86架构上,以上两个语句可以表示为下面两条汇编指令:

or constructed, 1

or constructed, 2

因为使用锁定机制,在一台多处理器的机器上,有可能会导致存储的时候读取到了旧值并使得数据结果自相矛盾。

在一台x64的机器上,这种情况会变得更加明显,因为x64平台没有一条单独的”读取-写入-存储”的指令,必须使用如下指令集合来实现:

ldl t1,0(a0) ; load

addl t1,1,t1 ; modify

stl t1,1,0(a0) ; store

如果线程在执行load和store之间被抢占,则被存储的值可能就是所预期的值了。

让我们考虑下列线程执行序列:

> 线程A判断变量constructed,然后发现它的值为0,然后就准备将它设置为1,但是此时它被抢占了。

> 线程B进入相同的函数,它发现变量constructed是0,于是准备创建s和t,变量constructed变为了3。

> 线程A恢复执行并完成它的”读取-写入-存储”操作序列,将变量constructed设置为1,然后第二次创建了s。

> 线程A接下来也会第二次创建t,并将constructed最后设置为3。

你可能会想到,使用临界区来对确保只有一个线程能执行关键代码片段,如下图所示:

通过使用临界区,我们将静态变量的一次性初始化工作放到了临界区执行,现在的版本就是线程安全的了。

但是,如果第二个调用来自同一线程,该怎么办? (我们已经跟踪了该调用;它来自线程内部)

如果ComputeSomethingSlowly()本身调用ComputeSomething()(可能是间接调用),则可能会发生这种情况。

由于该线程已经进入临界区,因此代码能正常执行,而我们会再次返回一个未初始化的变量。

总结

当你看到代码中有局部静态变量的初始化时,请特别小心。

另外,从C++11开始,标准经过了修改,在C++11之后,局部静态变量将是线程安全的了。

最后

Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。

本文来自:《C++ scoped static initialization is not thread-safe, on purpose!》

提示:支持键盘“← →”键翻页
为你推荐
加载更多
意见反馈
返回顶部