UPDATE

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

SomeEvent?.Invoke(this, e);



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

始终在检查事件是否为null并将其触发之前,先对其进行复制。这将消除潜在的线程问题,即在检查空值和触发事件的位置之间的位置,该事件变为null。从关于优化的阅读中想到,这可能还要求事件成员具有可变性,但是Jon Skeet在回答中指出CLR并没有优化副本。

但是与此同时,甚至发生问题,另一个线程也必须做这样的事情:

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

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


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

// 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...


问题是OnTheEvent在作者取消订阅后运行,但为避免这种情况,他们只是专门取消订阅。当然,真正需要的是在addremove访问器中具有适当同步的自定义事件实现。另外,如果在触发事件时保持锁定,则可能会导致死锁。

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

(而且在成员声明中分配空的delegate { }难道不是那么简单吗,这样您根本就不需要检查null吗?)

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

所以我认为仅执行此复制/检查习惯是一种可崇拜的编程,会给您的代码增加混乱和噪音。要真正保护自己免受其他线程的攻击,还需要做更多的工作。

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

所以事件处理程序我错过了一件主要的事情: “即使在取消订阅事件之后,事件处理程序也必须在面对调用时保持健壮”,因此显然,我们只需要关心事件委托为null的可能性。对事件处理程序的要求是否记录在任何地方?

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

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

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

评论

这个问题是在不久前的一次内部讨论中提出的。我已经打算现在写博客了一段时间。我关于这个主题的文章在这里:活动和比赛

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

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

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

#1 楼

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

如果没有volatile修饰符,则可能会导致本地副本已过期,但这就是所有。它不会引起NullReferenceException

是的,肯定有竞争条件-但总会有这种情况。假设我们只是将代码更改为:

TheEvent(this, EventArgs.Empty);


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

使用空代理当然可以避免无效检查,但不能解决竞争条件。这也不能保证您总是“看到”变量的最新值。

评论


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

–布拉德利·格兰杰(Bradley Grainger)
09年4月29日在20:58

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

–丹尼尔(Daniel Earwicker)
09年5月5日在16:40

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

–丹尼尔(Daniel Earwicker)
09年5月5日在16:40

关于空代理,另请参阅以下问题:stackoverflow.com/questions/170907/…。

–弗拉基米尔
14-10-17在10:08

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

–乔恩·斯基特(Jon Skeet)
14-10-18在7:28

#2 楼

我看到很多人都在使用扩展方法来执行此操作...

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() );


由于在方法调用时捕获了本地副本,因此它也删除了本地副本。

评论


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

–西蒙·吉尔比
09年5月1日17:14

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

–JP Alioto
09年5月1日在18:48

是的,请参阅以下问题:stackoverflow.com/questions/192980/…

– Benjol
2009年6月3日,11:32

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

– Niels van der Rest
11 Mar 10 '11 at 9:53

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

–user11937
2012年6月18日上午9:20

#3 楼

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

我怀疑这可能是因为空检查更有效。

如果创建事件时总是订阅空的委托给您的事件,会产生一些开销:


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

(请注意,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控件来说很常见,事件很多),该事件用EM pty委托的速度明显较慢(迭代超过5000万……)

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

(我的测试设置可能有缺陷,请随时下载源代码并自己检查它。非常感谢任何反馈。)

评论


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

–丹尼尔(Daniel Earwicker)
09年5月11日在7:02

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

–丹尼尔(Daniel Earwicker)
09年5月11日在7:07

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

–米奇·珀尔斯坦(Mickey Perlstein)
2012年4月19日在8:01



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

–超级猫
13年10月21日在20:31

#4 楼

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

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

1-空问题)为什么不将事件设置为.Empty而不是null?为进行空检查或必须将= delegate {}粘贴到声明上,将保存几行代码?让编译器处理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方面,它是在后编译步骤中完成的。 :)

–史蒂文·杰里斯(Steven Jeuris)
2012年3月26日在0:09

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

–超级猫
13年4月4日在23:51

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

– crokusek
13-10-21在18:35

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

–超级猫
13-10-21在19:42

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

–超级猫
13-10-21在19:43

#5 楼

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

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

这是MSDN文档。

#6 楼

杰弗里·里希特(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年7月19日在13:17

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

–超级猫
13年2月28日在21:07

#7 楼

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

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);
  }
}


这些天,我主要在Android平台上使用Mono,而Android并没有当您尝试将视图的“活动”发送到后台后尝试更新视图时,似乎不喜欢它。

评论


实际上,我看到有人在这里使用非常相似的模式:stackoverflow.com/questions/3668953/…

–灰
13年7月16日在21:54



#8 楼

这种做法与强制执行特定顺序无关。实际上,这是关于避免空引用异常的原因。
人们关心空引用异常而不是种族状况的原因需要进行深入的心理学研究。我认为与解决null引用问题要容易得多有关。一旦解决此问题,他们将在其代码上悬挂一个大的“ Mission Completed”横幅,并解压缩其飞行服。

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

评论


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

–丹尼尔(Daniel Earwicker)
09年4月24日在16:40

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

–dss539
09年4月24日在17:15

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

–丹尼尔(Daniel Earwicker)
09年4月24日在20:54

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

–超级猫
2012年12月13日19:20在

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

–超级猫
13年2月28日在21:19

#9 楼

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

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

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

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

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

评论


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

–丹尼尔(Daniel Earwicker)
09年5月1日,7:17

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

–利维
09年5月1日在18:56

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

–利维
09年5月1日19:00

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

–丹尼尔(Daniel Earwicker)
09年5月5日在16:32

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

–超级猫
2012年5月21日在22:19

#10 楼

对于单线程应用程序来说,这是没有问题的。

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

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

您是对的,消费者dto dto取消订阅是为了使这种情况发生,这是正确的,但是如果他们超过了临时副本,则认为该消息已经在传输中。您不使用临时变量,也不使用空的委托,并且有人取消订阅,您将得到null引用异常,这是致命的,因此我认为这样做值得。

#11 楼

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

我做错了吗?

评论


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

–丹尼尔(Daniel Earwicker)
09年4月24日在20:53

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

– Greg D
09年4月24日在22:29

@GregD取决于方法的复杂程度和使用的数据。如果它影响内部成员,并且您决定以线程/任务状态运行,那将给您带来很多伤害

–米奇·珀尔斯坦(Mickey Perlstein)
2012年4月19日在8:04

#12 楼

将所有活动都安排在施工中,不要理会它们。正如我将在本文的最后一段中解释的那样,Delegate类的设计可能无法正确处理任何其他用法。首先,没有必要尝试在您的计算机上拦截事件通知时事件处理程序必须已经就是否/如何响应该通知做出了同步决策。

任何可能被通知的内容都应该得到通知。如果您的事件处理程序正确地处理了通知(即,他们可以访问权威的应用程序状态并仅在适当的时候做出响应),那么可以随时通知他们并相信它们会正确响应。

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

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

评论


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

–丹尼尔(Daniel Earwicker)
09年5月27日20:38

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

–丹尼尔(Daniel Earwicker)
09年5月27日在20:40

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

–丹尼尔(Daniel Earwicker)
09年5月27日在20:42

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

–丹尼尔(Daniel Earwicker)
09年5月27日在20:43

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

– Triynko
09年5月28日在13:09

#13 楼

请在此处查看:http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety
这是正确的解决方案,应始终使用,而不是其他所有替代方法。 />
“您可以通过不执行匿名方法初始化内部调用列表,以确保内部调用列表始终至少具有一个成员。因为没有外部方可以引用匿名方法,所以没有外部方可以删除该方法,因此委托将永远不会为空。”
—编程.NET组件,第二版,由JuvalLöwy

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);  
}  


#14 楼

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

安全地提高事件线程-最佳实践


能力/在取消操作时从任何线程取消订阅(取消种族
条件)
在类级别上+ =和-=的操作符重载。
泛型调用方定义的委托


#15 楼

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

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

/// <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();
    }


测试

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

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#中实现线程安全事件。

您怎么看?

评论


对我来说看起来不错。尽管我认为在理论上有可能在foo.SomeEvent-= Handler之前发生处置= true,但会产生误报。但是除此之外,您可能还需要更改一些内容。您真的想对锁使用try ...最后-这将帮助您使它不仅具有线程安全性,而且具有中止安全性。更不用说您可以摆脱那种愚蠢的尝试。而且,您无需检查在“添加/删除”中传递的委托-它可以为空(您应立即在“添加/删除”中抛出)。

–罗安
15年5月6日在8:48