关于 C# 中 Events 和 Thread Safety

注:这是最近 team 里讨论的一个问题,恰好网上有一篇分析该问题的英文博客,就结合自己的理解大体译了一下。与此同时,删减了原文中一些冗长不必要的分析,并在容易产生疑问的地方,添加了我的 notes。原文地址在这里 http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx

假设你有这样一个 event: public event SomeDelegate Foo;

标准的 fire 这个 event 的 pattern 是:

#1 SomeDelegate temp = Foo;
#2 if( temp != null)
#3 {
#4     temp();
#5 }

有人可能会问,这里为什么不直接调用 Foo() 呢?

这个 pattern 首先确保了你不会调用一个 null delegate reference, 这会 throw 一个 null reference exception。

其次,temp 变量确保了这是“线程安全”的。注:其实这里也不是真的线程安全的,请继续往下读,所以原文在这里加了引号。

#1 //SomeDelegate temp = Foo;
#2 if( Foo != null)
#3 {
#4        Foo();
#5 }

如果这段代码在 thread a 和 thread b 中正在被同时执行,在 thread a 中执行到#2时不为 null,但在#3时被 thread b 修改为 null,然后 thread a 继续执行#4,那么还是会 throw null reference exception。

实际上,在该 pattern 中使用 temp 变量,会把当前所有的 event handler 做一个 copy。记住,multi-cast delegates 是 immutable 的。当你 add 或 remove 一个 handler 时,你就会用另外一个 delegate 对象来替换当前的 multi-cast delegate 对象。这里,你不会修改当前的对象,你修改的只是存储 event handler 的变量。所以,这里使用 temp 变量,其实是把旧的状态做了一个 copy。[注:对被更新的 delegate 对象来说是 copy on write,这里原始的 delegate 其实没有发生拷贝]

通常,大家对这种 pattern 的批评是:这其实是在拿一个 race condition 来交换另一个 race condition。好,下面让我们继续更仔细的讨论这个 scenario。

假设 event handler delegate 对象只有一个 handler:H,假设 thread α执行了 pattern 中的 #1,并进一步判断它不是 null。此时,thread β设置 handler 为 null,从而 H 将不会被 call 到,并摧毁了 H 要执行所必须依赖的那些状态。此时,thread α 再次获得控制权并执行 H。此时,因为 H 要正确执行所依赖的状态已经不复存在,所以它的执行结果将是难以预料的甚至会 crash。结论:在这种 race condition 中, thread β 尝试 unsubscribe H 就失败了。

假设应用程序不使用 temp 变量,那么取而代之的就是可能会发生 crash。这也就是说,这种代码有 failure mode;当然,如果能用一个清晰的 exception 来表明这种 failure,就会好过让它产生难以预料的行为。

这看起来很合情合理,但是,结论仍然是错误的。

(1) 假设我们删除了 temp 变量但是保留 null check。这能解决问题么?当然不能。我们仍然面临相同的 race condition。假设 event handler delegate 对象含有一个 H 的reference。Thread α 做 null check,发现不空,则 push 这个 object 到 runtime stack 上。那么,在 push 它之后和 invoke它 之间,thread β 仍旧有机会设置 event handler 为 null。我们还是会遇到前面提到过的 H 会被 invoke 但其所以来的状态已经被删除了,所以其行为还是难以预料的。

(2) 假设我们删除了 temp 变量和 null check,仅仅直接 invoke delegate。这也不能解决问题。我们还是有一样的 race condition。

上面两招只会比 pattern 代码更糟糕。上面的讨论中,其实有两个问题被合并到一起讨论了。

(1) null ref problem,event handler delegate 对象可以随时被设为 null;

(2) stale handler problem,即一个空挂的 handler 可以被 invoke 当它被另一个 thread 删除后,在上述 race condition 中;

这两个问题其实是正交的并且各自有不同的解决方案。

解决第一个问题的责任其实在于执行 invocation 的代码,它必须确保它不会访问 null reference。我们给出的这个 pattern 代码,确保了不会发生这个问题。(还有别的方式可以解决此问题,例如,用一个啥事也不干却永远也不会被删除的 handler 来初始化 delegate object。但是,做 null check 是一个标准模式。)

解决第二个问题的责任在于被 invoke 的代码,handler 需要保持健壮,即使在自己作为 handler 已经被删除的情况下。在本文所描述的 scenario 中,bug 其实源自 H。它应该在每次被执行前,检查它所依赖的状态是否还在那里(注:并且访问期间锁住这个状态),如果不在那里就要干净利索的退出。(或者,另一个选择就是,一些额外的锁机制需要被实现,它能确保 fire event 的代码和可能改变那些状态的代码合作,以确保 handler 的行为正确。)

究其根本原因,其实是 null ref problem 和 stale handler problem 这两个问题恰好在同一个调用地点发生了。你需要解决这两个问题,如果你想实现线程安全的 event。具体怎样做就取决于你了。

<EOF>

posted on 2012-11-28 19:25  Cary_Fan  阅读(473)  评论(0编辑  收藏  举报

导航