我尝试的是达林更新后的答案版本,但没有我指出的比赛条件…警告,我不确定这最终是否完全摆脱了比赛条件。
private static int waiters = 0;private static volatile Lazy<object> lazy = new Lazy<object>(GetValueFromSomewhere);public static object Value{ get { Lazy<object> currLazy = lazy; if (currLazy.IsValueCreated) return currLazy.Value; Interlocked.Increment(ref waiters); try { return lazy.Value; // just leave "waiters" at whatever it is... no harm in it. } catch { if (Interlocked.Decrement(ref waiters) == 0) lazy = new Lazy<object>(GetValueFromSomewhere); throw; } }}更新:我以为发布此消息后发现了比赛情况。该行为实际上应该是可以接受的,只要您对一个罕见的情况感到满意,即
Lazy<T>在另一个线程已经从成功的快速返回之后,某个线程抛出了一个从慢速观察到的异常
Lazy<T>(将来的请求将全部成功)。
waiters
= 0- t1:一直运行到
Interlocked.Decrement
(waiters
= 1)之前 - t2:进入并运行到
Interlocked.Increment
(waiters
= 1)之前 - t1:进行
Interlocked.Decrement
并准备覆盖(waiters
= 0) - t2:一直运行到
Interlocked.Decrement
(waiters
= 1)之前 - t1:
lazy
用新的覆盖(称为lazy1
)(waiters
= 1) - t3:进入并在
lazy1
(waiters
= 2)处阻止 - t2:是否执行
Interlocked.Decrement
(waiters
= 1) - t3:从
lazy1
(waiters
现在不相关)获取并返回值 - t2:抛出异常
我无法提出一系列导致比“该线程在另一个线程产生成功结果之后引发异常”更糟糕的事情的事件。
Update2:声明
lazy为
volatile确保所有读者立即都能看到受保护的覆盖。有些人(包括我自己在内)看到
volatile并立即想到“好吧,可能是使用不正确”,他们通常是正确的。这就是我在这里使用它的原因:在上面示例中的事件序列中,t3仍然可以读取旧的,
lazy而不是
lazy1如果它位于
lazy.Valuet1修改
lazy为包含的那一刻之前
lazy1。
volatile防止这种情况,以便下次尝试可以立即开始。
我还提醒自己,为什么我脑子里有这样的话:“低锁并发编程很难,只需使用C#
lock语句!!!” 我一直在写原始答案。
Update3:只是更改了Update2中的一些文本,指出了
volatile必要的实际情况-
Interlocked此处使用的操作显然是在当今重要的CPU架构上全屏实现的,而不是我最初只是假设的半屏实现的,因此
volatile保护的范围比我原来想象的要窄得多。



