C#事件和线程安全


237

更新

从C#6开始,此问题的答案是:

SomeEvent?.Invoke(this, e);

我经常听到/阅读以下建议:

在检查null并触发事件之前,请务必复制事件。这将消除潜在的线程问题,即事件null在您检查空值和触发事件的位置之间的位置变为:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

更新:我从阅读有关优化的内容中认为,这可能还要求事件成员具有可变性,但是Jon Skeet在回答中指出CLR不会优化副本。

但是,与此同时,为了使此问题发生,另一个线程必须执行以下操作:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

实际的顺序可能是这种混合:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

关键在于OnTheEvent作者取消订阅之后,但是他们只是专门取消订阅以避免这种情况的发生。当然,真正需要的是在add和中remove访问。此外,如果在触发事件时持有锁,则可能会出现死锁。

那么这是《货运崇拜编程》吗?似乎是这样-很多人必须采取这一步骤来保护自己的代码免受多个线程的侵害,而在我看来,实际上,在将事件用作多线程设计的一部分之前,事件需要比这多得多的关注。因此,那些没有特别注意的人也可能会忽略此建议-对于单线程程序来说这根本不是问题,并且实际上,由于volatile大多数在线示例代码中都没有,因此该建议可能没有完全没有效果。

(并且delegate { }在成员声明中分配空值是否更简单,这样您就不必首先检查null?)

更新:如果不清楚,我确实掌握了建议的意图-避免在所有情况下都出现空引用异常。我的观点是,仅当另一个线程从该事件中退出时,才会发生此特定的null引用异常,而这样做的唯一原因是确保不会通过该事件接收到进一步的调用,而该技术显然无法实现。您可能会隐藏种族状况-最好公开一下!空异常有助于检测对组件的滥用。如果希望保护组件免受滥用,则可以遵循WPF的示例-将线程ID存储在构造函数中,如果另一个线程尝试直接与组件进行交互,则抛出异常。否则,实现一个真正的线程安全组件(不是一件容易的事)。

因此,我认为仅执行此复制/检查惯用语便是一种狂热的编程,这会给您的代码增加混乱和噪音。要真正保护自己免受其他线程的攻击,需要进行大量工作。

更新以回应Eric Lippert的博客文章:

因此,关于事件处理程序,我错过了一件主要的事情:“即使在事件未订阅后,事件处理程序也必须在被调用时保持健壮”,因此,显然,我们只需要关心事件的可能性代表被null对事件处理程序的要求是否记录在任何地方?

因此:“还有其他方法可以解决此问题;例如,初始化处理程序以使其具有从未删除的空动作。但是,执行空检查是标准模式。”

因此,我的问题剩下的一个片段是,为什么要显式-空检查“标准模式”?另一种方法是分配空的委托人,只需= delegate {}要将其添加到事件声明中,这样就消除了在引发事件的每个位置上的一堆臭臭的仪式。确保空委托的实例化很便宜。还是我还缺少什么?

当然一定是(正如Jon Skeet所建议的那样),这仅仅是.NET 1.x的建议还没有像2005年那样被淘汰?


4
这个问题是在不久前的一次内部讨论中提出的。我一直打算现在写博客。我关于这个主题的信息在这里:事件与竞赛
埃里克·利珀特

3
Stephen Cleary在CodeProject上的文章对此进行了研究,他得出一个普遍的结论,即不存在“线程安全”的解决方案。基本上,由事件调用者来确保委托不为空,而由事件处理程序来决定是否取消订阅后,才能够处理被调用。
rkagerer 2011年

3
@rkagerer-实际上,即使不涉及线程,事件处理程序有时也必须处理第二个问题。如果一个事件处理程序告诉另一个处理程序取消订阅当前正在处理的事件,则该事件可能发生,但是第二个订阅者无论如何都会接收到该事件(因为在处理过程中取消订阅该事件)。
丹尼尔·艾威克

3
与为零个订户添加事件的订阅,删除该事件的唯一订阅,为零个订户调用一个事件以及仅由一个订户调用一个事件相比,添加/删除/调用涉及其他数量的场景的操作要快得多。订阅者。添加虚拟委托会减慢常见情况。C#的真正问题在于其创建者决定EventName(arguments)无条件地调用事件的委托,而不是让它仅在非null时调用委托(如果为null,则不执行任何操作)。
2012年

Answers:


100

由于条件的原因,不允许JIT执行您在第一部分中讨论的优化。我知道这是在不久前提出来的,但这是无效的。(前一段时间我曾与Joe Duffy或Vance Morrison进行过核对;我不记得是哪个。)

如果没有volatile修饰符,则所获取的本地副本可能会过时,仅此而已。它不会导致NullReferenceException

是的,肯定有比赛条件-但总会存在。假设我们将代码更改为:

TheEvent(this, EventArgs.Empty);

现在假设该委托的调用列表有1000个条目。在另一个线程取消订阅列表末尾的处理程序之前,很有可能在列表开头的操作已经执行。但是,该处理程序将仍然执行,因为它将是一个新列表。(代表们是一成不变的。)据我所知,这是不可避免的。

使用空的委托当然可以避免无效检查,但不能解决竞争条件。它还不能保证您始终“看到”变量的最新值。


4
Joe Duffy的“ Windows并行编程”涵盖了问题的JIT优化和内存模型方面。参见code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger 2009年

2
我已经接受有关基于C#2之前的“标准”建议的评论,并且没有听到有人对此提出异议。除非实例化事件args确实很昂贵,否则只需在事件声明的末尾添加'=委托{}',然后直接将它们当作方法调用即可;永远不要给他们分配空值。(我带来的其他有关确保除名后不调用处理程序的内容,这些都是无关紧要的,并且即使是单线程代码也无法确保,例如,如果处理程序1要求处理程序2退出处理程序,则处理程序2仍会被调用接下来)
。–丹尼尔·艾尔威克

2
唯一的问题案例(一如既往)是结构,在该结构中,您不能确保使用成员中的空值以外的任何实例来实例化它们。但是结构很烂。
Daniel Earwicker,2009年

1
关于空代理,另请参阅以下问题:stackoverflow.com/questions/170907/…
弗拉基米尔

2
@Tony:从根本上说,在订阅/取消订阅与执行委托之间存在竞争条件。您的代码(刚刚对其进行了简要浏览)通过允许订阅/取消订阅在提出的同时生效来减少竞争状况,但是我怀疑在大多数情况下正常行为还不够好,但这也不是。
乔恩·斯基特

52

我看到很多人都倾向于这样做的扩展方法...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

那给你更好的语法来引发事件...

MyEvent.Raise( this, new MyEventArgs() );

并且也取消了本地副本,因为它是在方法调用时捕获的。


9
我喜欢这种语法,但是让我们清楚一点……即使取消注册,过时的处理程序也无法解决问题。这解决了空解除引用问题。当我喜欢语法时,我质疑它是否真的比以下更好:public event EventHandler <T> MyEvent = delete {}; ... MyEvent(this,new MyEventArgs()); 这也是我非常喜欢的一种非常低摩擦的解决方案。
西蒙·吉尔比

@Simon我看到不同的人对此有不同的说法。我已经对其进行了测试,所做的工作向我表明这确实可以处理null处理程序问题。即使在处理程序!= null检查之后,原始接收器也从事件中注销,该事件仍将引发并且不会引发异常。
JP Alioto

是的,请参见以下问题:stackoverflow.com/questions/192980/…– Benjol
2009年

1
+1。我只是自己写这个方法,开始考虑线程安全性,做了一些研究,偶然发现了这个问题。
Niels van der Rest

如何从VB.NET调用它?还是'RaiseEvent'已经适合多线程方案?

35

“为什么显式-空检查'标准模式'?”

我怀疑这可能是因为空检查性能更高。

如果在创建事件时始终为事件委派空的委托,则将产生一些开销:

  • 构造空委托的成本。
  • 构建包含它的委托链的成本。
  • 每次引发事件时调用无意义的委托的成本。

(请注意,UI控件通常具有大量事件,其中大多数事件从未订阅。必须为每个事件创建虚拟订阅者,然后调用它,这可能会严重打击性能。)

我进行了一些粗略的性能测试,以了解subscribe-empty-delegate方法的影响,这是我的结果:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

请注意,对于零个或一个订阅者(对于UI控件来说是常见的,事件很多),用空的委托预先初始化的事件要慢得多(超过5000万次迭代...)

有关更多信息和源代码,请访问此博客文章。NET事件调用线程安全性是我在提出此问题的前一天发布的(!)

(我的测试设置可能有缺陷,因此可以随时下载源代码并亲自检查它。非常感谢您提供任何反馈。)


8
我认为您在博客文章中指出了关键点:在性能成为瓶颈之前,无需担心性能影响。为什么让丑陋的方式成为推荐的方式?如果我们想要过早的优化而不是清晰,那么我们将使用汇编程序-所以我的问题仍然存在,我认为可能的答案是该建议早于匿名代表,并且人类文化转移旧建议需要很长时间,例如在著名的“锅烤故事”中。
Daniel Earwicker,2009年

13
而且您的数据很好地证明了这一点:每次引发的事件(预初始化与经典null)的开销减少到只有NANOSECONDS(!!!)两个半。在几乎有实际工作要做的应用中,这几乎是无法检测到的,但是鉴于大多数事件使用都是在GUI框架中进行的,因此您必须将其与在Winforms中重新绘制屏幕部分的成本等进行比较,因此在大量实际CPU工作和等待资源的情况下,它变得更加不可见。无论如何,您会从我这里得到+1。:)
丹尼尔·厄尔威克

1
@DanielEarwicker说的没错,您已使我成为公共事件WrapperDoneHandler OnWrapperDone =(x,y)=> {}的信徒。模型。
米奇·佩尔斯坦

1
这也可能是很好的时间Delegate.Combine/ Delegate.Remove在该事件具有零个,一个或两个用户的情况下对; 如果一个重复地添加和删除相同的委托实例,则案例之间的成本差异将特别明显,因为Combine当其中一个参数为null(仅返回另一个)时具有特殊情况下的快速行为,而Remove当两个参数为相等(只返回null)。
2013年

12

我真的很喜欢这篇读物-不!即使我需要它与称为事件的C#功能一起使用!

为什么不在编译器中解决此问题?我知道有MS人士在阅读这些帖子,所以请不要对此发火!

1-空问题)为什么不首先将事件设置为.Empty而不是null?为了进行空检查或必须= delegate {}在声明上粘贴a,将保存几行代码?让编译器处理Empty情况,IE不执行任何操作!如果这对事件的创建者来说很重要,他们可以检查.Empty并做任何自己关心的事情!否则,所有的null检查/委托添加都会解决该问题!

老实说,我厌倦了每次事件都必须执行此操作-又名样板代码!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2-竞赛条件问题)我阅读了Eric的博客文章,我同意H(处理程序)在取消引用自己时应进行处理,但是不能将事件设为不可变的/线程安全的吗?IE,在其创建时设置了一个锁定标志,以便无论何时调用它,它在执行时都将锁定所有订阅和取消订阅该订阅?

结论

现代语言是否不应该为我们解决此类问题?


同意,在编译器中对此应该有更好的支持。在此之前,我创建了一个PostSharp方面,它是在后编译步骤中完成的。:)
史蒂芬·杰里斯

4
在等待任意外部代码完成时,使线程订阅/取消订阅请求阻塞很远比取消订阅后让订阅者接收事件要糟糕得多,尤其是因为可以通过简单地让事件处理程序检查标志以查看是否可以轻松解决后者的“问题” 他们仍然对接收他们的事件感兴趣,但是以前的设计导致的僵局可能很棘手。
2013年

@supercat。Imo,“严重得多”的评论取决于应用程序。谁不想在没有其他标志的情况下进行非常严格的锁定?死锁仅应在事件处理线程正在等待另一个线程(正在订阅/取消订阅)时发生,因为锁是同一线程可重入的,并且原始事件处理程序中的订阅/取消订阅不会被阻止。如果作为事件处理程序的一部分有跨线程等待,那将是设计的一部分,那么我希望进行重做。我来自具有可预测模式的服务器端应用程序角度。
crokusek

2
@crokusek:如果有向图中没有周期将每个锁连接到持有时可能需要的所有锁,那么证明系统没有死锁所需的分析很容易[缺乏周期证明了系统无死锁]。允许在持有锁的同时调用任意代码将在“可能需要”图中从该锁到任意代码可能获取的任何锁(并非系统中的每个锁,但距离它都不远)上都创建了一条边)。随之而来的周期的存在并不意味着就会发生僵局,而是……
超级猫

1
……将大大提高证明它不能进行分析的必要水平。
2013年

8

C#6及更高版本中,可以使用new ?.运算符来简化代码,如下所示:

TheEvent?.Invoke(this, EventArgs.Empty);

是MSDN文档。


5

根据杰弗里·里希特(Jeffrey Richter)在通过C#编写的CLR书中,正确的方法是:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

因为它强制引用副本。有关更多信息,请参见书中的“事件”部分。


可能是我错过了某事,但是如果Interlocked.CompareExchange的第一个参数为null,则抛出NullReferenceException,这正是我们要避免的情况。msdn.microsoft.com/zh-CN/library/bb297966.aspx
Kniganapolke 2011年

1
Interlocked.CompareExchange如果以某种方式传递了null ref,将失败,但这与ref将a 传递给NewMail存在且最初持有 null引用的存储位置(例如)不同。
2013年

4

我一直在使用这种设计模式来确保事件处理程序在退订后不会执行。到目前为止,它运行良好,尽管我还没有尝试任何性能分析。

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

这些天来,我主要使用的是Mono for Android,当您将View的Activity发送到后台后尝试更新View时,Android似乎不喜欢它。


其实,我看到别人在这里使用一个非常相似的模式:stackoverflow.com/questions/3668953/...

2

这种做法与强制执行特定顺序无关。它实际上是在避免空引用异常。

人们关心空参考异常而不是种族状况的背后原因需要进行深入的心理学研究。我认为这与解决空引用问题要容易得多有关。解决此问题后,他们会在代码上悬挂一个大的“任务完成”横幅,并解压缩其飞行服。

注意:解决竞争条件可能涉及使用同步标志跟踪处理程序是否应运行


我不是要解决这个问题。我想知道为什么有广泛的建议在事件触发周围喷洒额外的代码-message,当它仅在存在难以检测的竞争条件时才避免空异常,这种情况仍然存在。
Daniel Earwicker 09年

1
那是我的意思。他们不在乎比赛条件。他们只关心空引用异常。我将其编辑成我的答案。
dss539

我的观点是:为什么关心空引用异常而不关心竞争条件为什么有意义?
Daniel Earwicker 09年

4
应当编写适当的事件处理程序来处理以下事实:引发事件的任何特定请求(其处理可能与添加或删除该请求的请求重叠)可能会或可能不会引发正在添加或删除的事件。程序员不关心竞争条件的原因是,在正确编写的代码中,谁赢并不重要
supercat

2
@ dss539:虽然可以设计一个事件框架,该框架将阻止取消订阅的请求,直到未完成的事件调用完成为止,但这种设计将使任何事件(甚至是类似Unload事件的事件)都无法安全地取消对象对其他事件的订阅。讨厌。最好是简单地说,事件取消订阅请求将导致事件“最终”被取消订阅,并且事件订阅者应检查何时调用它们,是否有任何有用的事情要做。
2013年

1

所以我在这里参加聚会有点晚了。:)

至于使用null而不是null对象模式来表示没有订阅者的事件,请考虑这种情况。您需要调用一个事件,但是构造对象(EventArgs)并非易事,通常情况下,您的事件没有订阅者。如果您可以优化代码以检查是否有任何订阅者,然后再致力于构造自变量和调用事件,这将对您有所帮助。

考虑到这一点,一种解决方案是说“嗯,零个订户用null表示”。然后只需执行空检查,然后再执行昂贵的操作即可。我想执行此操作的另一种方法是在Delegate类型上具有Count属性,因此,仅当myDelegate.Count> 0时,才执行昂贵的操作。使用Count属性是解决原始问题的一种不错的模式允许优化,它还具有可以被调用而不会引起NullReferenceException的不错的属性。

但是请记住,由于委托是引用类型,因此它们可以为空。也许根本没有什么好办法可以将这一事实隐藏在幕后,并且仅支持事件的空对象模式,因此,替代方法可能是迫使开发人员同时检查零订户和零订户。那将比目前的情况还要糟糕。

注意:这纯粹是猜测。我不参与.NET语言或CLR。


我假设您的意思是“使用空委托而不是...”,并且已经将事件初始化为空委托了,您已经可以按照您的建议进行操作。如果初始空委托是列表中唯一的对象,则测试(MyEvent.GetInvocationList()。Length == 1)将为true。仍然没有必要先进行复制。虽然我认为您所描述的情况无论如何都是极为罕见的。
Daniel Earwicker

我认为我们在这里混淆了代表和活动的想法。如果我在类上有一个事件Foo,则当外部用户调用MyType.Foo + = /-=时,他们实际上是在调用add_Foo()和remove_Foo()方法。但是,当我从定义它的类中引用Foo时,实际上实际上是直接引用基础委托,而不是add_Foo()和remove_Foo()方法。并且由于存在诸如EventHandlerList之类的类型,因此没有任何要求强制委托和事件位于同一位置。这就是我在回复中“记住”一段的意思。
李维

(续)我承认这是一个令人困惑的设计,但是替代方案可能更糟。由于最后您只拥有一个委托-您可以直接引用基础委托,可以从集合中获取它,也可以即时实例化它-从技术上讲,除了“ check for空”模式。

当我们谈论触发事件时,我看不到为什么添加/删除访问器在这里很重要。
丹尼尔·艾威克

@Levi:我真的不喜欢C#处理事件的方式。如果我有德鲁特人,那么该代表的名字将与活动不同。从类外部,对事件名称的唯一允许的操作是+=-=。在该类内,允许的操作还包括调用(带有内置的null检查),针对进行测试null或设置为null。对于其他任何事情,都必须使用其名称为带有特定前缀或后缀的事件名称的委托。
2012年

0

对于单线程应用程序,您是正确的,这不是问题。

但是,如果您要制作一个暴露事件的组件,则不能保证该组件的使用者不会使用多线程,在这种情况下,您需要为最坏的情况做准备。

使用空委托确实可以解决问题,但也会导致每次调用事件时性能下降,并且可能会对GC产生影响。

您是正确的,消费者trie dto取消订阅是为了使这种情况发生,但是,如果他们超过了临时副本,则认为该消息已经在传输中。

如果您不使用临时变量,并且不使用空的委托,而有人取消订阅,则会得到null引用异常,这是致命的,因此我认为这样做值得。


0

我从来没有真正考虑过这是一个大问题,因为我通常只在可重用组件上的静态方法(等)中防止此类潜在的线程错误,并且我不进行静态事件。

我做错了吗?


如果您分配一个具有可变状态的类的实例(更改其值的字段),然后让多个线程同时访问同一实例,而没有使用锁定来保护这些字段不被两个线程同时修改,那么您就是可能做错了。如果所有线程都有自己的单独实例(不共享任何内容),或者所有对象都是不可变的(一旦分配,它们的字段值永远不会改变),那么您可能还可以。
Daniel Earwicker 09年

我的一般方法是将同步留给调用方,但静态方法除外。如果我是呼叫者,那么我将在更高级别进行同步。(当然,唯一目的是处理同步访问的对象除外:))
Greg D

@GregD取决于方法的复杂程度和使用的数据。如果它影响内部成员,并且您决定以线程状态/任务状态运行,则会遭受很多伤害
Mickey Perlstein 2012年

0

将所有活动都安排在施工中,不要理会它们。正如我将在本文的最后一段中解释的那样,Delegate类的设计可能无法正确处理任何其他用法。

首先,当事件处理程序必须已经就是否/如何响应通知做出同步决策时,尝试拦截事件通知毫无意义。

任何可能被通知的东西都应该被通知。如果您的事件处理程序正确地处理了通知(即,他们可以访问权威应用程序状态并仅在适当的时候进行响应),则可以随时通知它们并相信它们将正确响应。

唯一不应该通知处理程序已发生事件的时间是,如果该事件实际上尚未发生!因此,如果您不希望收到处理程序的通知,请停止生成事件(即首先禁用控件或负责检测和使事件变为现实的任何操作)。

老实说,我认为Delegate类是不可挽救的。合并/转换为MulticastDelegate是一个巨大的错误,因为它有效地将事件的(有用的)定义从单个时刻发生的事情更改为某个跨时间发生的事情。这样的更改需要一种同步机制,该机制可以在逻辑上将其折叠回单个瞬间,但是MulticastDelegate缺少任何这种机制。同步应涵盖事件发生的整个时间范围或瞬间,以便一旦应用程序做出同步决定以开始处理事件,它便会完全(以事务方式)完成处理。由于黑盒是MulticastDelegate / Delegate混合类,因此这几乎是不可能的,因此坚持使用单用户和/或实现自己的MulticastDelegate类型,该类型具有一个同步句柄,可以在使用/修改处理程序链时将其取出。我建议这样做,因为替代方法是在所有处理程序中冗余地实现同步/事务完整性,这将是荒谬的/不必要的复杂。


[1]没有在“单个时刻”发生的有用事件处理程序。所有操作都有时间跨度。任何单个处理程序都可以执行一系列简单的步骤。支持处理程序列表不会改变任何内容。
丹尼尔·艾威克

[2]在事件触发时按住锁是完全疯狂的。它不可避免地导致僵局。源拿出锁A,触发事件,接收器拿出锁B,现在持有两个锁。如果另一个线程中的某些操作导致锁以相反的顺序被取出怎么办?当锁的责任划分在单独设计/测试的组件(这是整个事件的重点)之间时,如何排除这种致命的组合?
丹尼尔·艾威克

[3]这些问题都没有以任何方式降低普通多播委托/事件在组件的单​​线程组成中的普遍普及性,尤其是在GUI框架中。该用例涵盖了事件的绝大多数使用。以自由线程方式使用事件的价值值得怀疑。这在任何意义上都不会以任何方式使它们的设计或明显的有用性失效。
丹尼尔·艾威克

[4]线程+同步事件本质上是一个红色鲱鱼。排队异步通信是必经之路。
丹尼尔·艾威克

[1]我不是指测量的时间...我所说的是原子操作,该操作在逻辑上是瞬时发生的...而且,我的意思是,在事件发生时,涉及他们使用的相同资源的其他任何事物都不会改变因为它是用锁序列化的。
Triynko

0

请在此处查看:http : //www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety 这是正确的解决方案,应始终使用它代替所有其他解决方法。

“您可以使用不执行任何操作的匿名方法初始化内部调用列表,以确保内部调用列表始终至少具有一个成员。因为没有外部方可以引用匿名方法,所以没有外部方可以删除该方法,因此委托永远不会为空。” — JuvalLöwy编写的.NET Components,第二版

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

我不认为问题仅限于C#“事件”类型。取消该限制,为什么不稍微重新发明轮子并按照这些原则做些什么呢?

安全地引发事件线程-最佳实践

  • 能够在提升期间(取消竞争条件)从任何线程进行订阅/取消订阅
  • 在类级别上+ =和-=的运算符重载。
  • 通用调用方定义的委托

0

感谢您的有益讨论。我最近正在研究此问题,并制作了以下类,该类稍慢一些,但可以避免调用已处置的对象。

这里的要点是,即使引发事件,也可以修改调用列表。

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

用法是:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

测试

我以以下方式对其进行了测试。我有一个线程可以创建和销毁这样的对象:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

Bar(侦听器对象)构造函数中,我订阅SomeEvent(如上所示实现)并在下进行退订Dispose

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

我也有几个线程在循环中引发事件。

所有这些动作都是同时执行的:创建并销毁了许多侦听器,同时触发了事件。

如果存在比赛条件,我应该在控制台中看到一条消息,但该消息为空。但是,如果像往常一样使用clr事件,我将看到警告消息。因此,我可以得出结论,可以在c#中实现线程安全事件。

你怎么看?


对我来说看起来不错。尽管我认为在理论上有可能在您的测试应用程序disposed = true之前发生foo.SomeEvent -= Handler,从而产生假阳性。但是除此之外,您可能还需要更改一些内容。您确实想使用try ... finally这些锁-这将帮助您使它不仅具有线程安全性,而且具有中止安全性。更不用说您可以摆脱那种愚蠢的了try catch。而你不检查传入的委托Add/ Remove-它可能是null(你应该扔在马上Add/ Remove)。
a安2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.