在块作用域中的静态变量的规则 (与之相对的是全局作用域的静态变? ? E序W一ơ执行到他的声明的时候进行初始化.
察看下面的竞争条?
int ComputeSomething() { static int cachedResult = ComputeSomethingSlowly(); return cachedResult; }
q段代码的意图是在该函数W一ơ被调用的时候去计算一些费? q且把结果缓冲v来待函数来再被调用的时候则直接q回q个值即?
q个基本技巧的变种,在网l上也被叫做 避免 "static initialization order fiasco". ( fiascoq个?在这个网上有非常棒的描q?因此我徏议大家去Mȝ后去理解?)
q段代码的问题是非线E安全的. 在局部作用域中的静态变量是~译时会在编译器内部转换成下面的样子:
int ComputeSomething() { static bool cachedResult_computed = false; static int cachedResult; if (!cachedResult_computed) { cachedResult_computed = true; cachedResult = ComputeSomethingSlowly(); } return cachedResult; }
现在竞争条g比较容易看C.
假设两个U程在同一时刻都调用这个函? W一个线E在执行 cachedResult_computed = true ? 被抢? W二个线E现在看到的 cachedResult_computed 是一个真? true ),然后qq了if分支的处?最后该函数q回的是一个未初始化的变量.
现在你看到的东西q不是一个编译器的bug, q个行ؓ C++ 标准所要求?
你也能写一个变体来产生一个更p糕的问?
class Something { ... }; int ComputeSomething() { static Something s; return s.ComputeIt(); }
同样的在~译器内部它会被重写 (q次, 我们使用C++伪代?:
class Something { ... }; int ComputeSomething() { static bool s_constructed = false; static uninitialized Something s; if (!s_constructed) { s_constructed = true; new(&s) Something; // construct it atexit(DestructS); } return s.ComputeIt(); } // Destruct s at process termination void DestructS() { ComputeSomething::s.~Something(); }
注意q里有多重的竞争条g. 像前面所说的, 一个线E很可能在另一个线E之前运行ƈ且在"s"q没有被构造前׃用它.
甚至更糟p的情况, W一个线E很可能在s_contructed 条g判定 之后,在他被设|成"true"之前被抢? 在这U场合下, 对象s׃?span style="font-weight: bold;">双重构?/span>?span style="font-weight: bold;">双重析构.
q样׃是很?
但是{等, qƈ不是全部, 现在(原文是Not,我认为是Now的笔?看看如果?span style="font-style: italic;">两个q行期初始化局部静态变量的话会发生什?
class Something { ... }; int ComputeSomething() { static Something s(0); static Something t(1); return s.ComputeIt() + t.ComputeIt(); }
上面的代码会被编译器转化Z面的伪C++代码:
class Something { ... }; int ComputeSomething() { static char constructed = 0; static uninitialized Something s; if (!(constructed & 1)) { constructed |= 1; new(&s) Something; // construct it atexit(DestructS); } static uninitialized Something t; if (!(constructed & 2)) { constructed |= 2; new(&t) Something; // construct it atexit(DestructT); } return s.ComputeIt() + t.ComputeIt(); }
Z节省I间, ~译器会把两?x_constructed" 变量攑ֈ一?bitfield ? 现在q里在变?construted"上就有多?span style="font-weight: bold;">无内部锁?/span>的读-?存操?
现在考虑一下如果一个线E尝试去执行 "constructed |= 1", 而在同一旉另一个线E尝试执?"constructed |= 2".
在x86q_? q条语句会被汇编?/p>
or constructed, 1 ... or constructed, 2q没?"lock" 前缀. 在多处理机器? 很有可能发生两个存储都去d一个旧值ƈ且互怋用冲H的D行碰?clobber).
?ia64 ?alphaq_? q个撞更加明?因ؓ它们么没有这L???/span>的单条指? 而是被编码成三条指o:
ldl t1,0(a0) ; load addl t1,1,t1 ; modify stl t1,1,0(a0) ; store
如果q个U程?load ?store之间被抢? q个存储的值可能将不再是它曄要写入的那个?
因此,现在考虑下面q个有问题的执行序:
- U程A 在测?"constructed" 条g后发C是零, q且正要准备把这个D定成1Q?但是它被抢占?
- U程B q入同样的函? 看到 "constructed" 是零ql去构?"s" ?"t", d?"constructed" {于3.
- U程A l箋执行q且完成它的 ???的指令序? 讑֮ "constructed" ?1, 然后构?"s" (W二?.
- U程A 然后l箋L?"t" (W二? q设?"constructed" (最l? ?3.
现在, 你可能会认ؓ你能用界区 (critical section) 来封装这个运行期初始化动?
int ComputeSomething() { EnterCriticalSection(...); static int cachedResult = ComputeSomethingSlowly(); LeaveCriticalSection(...); return cachedResult; }
因ؓ你现在把q个一ơ初始化攑ֈ了界区里面,而它线E安?
但是如果从同一个线E再一ơ调用这个函C怎样? ("我们跟踪了这个调? 它确实是来自q个U程!") 如果 ComputeSomethingSlowly() 它自己间接地调用 ComputeSomething()׃发生q个状况.
l论: 当你看见一个局部静态变量在q行期初始化? 你一定要心.