简体   繁体   English

C# 事件和线程安全

[英]C# Events and Thread Safety

I frequently hear/read the following advice:我经常听到/阅读以下建议:

Always make a copy of an event before you check it for null and fire it.在检查它是否为null并触发它之前,请始终制作一个事件的副本。 This will eliminate a potential problem with threading where the event becomes null at the location right between where you check for null and where you fire the event:这将消除线程在您检查 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

Updated : I thought from reading about optimizations that this might also require the event member to be volatile, but Jon Skeet states in his answer that the CLR doesn't optimize away the copy.更新:我从阅读有关优化的文章中想到,这可能还需要事件成员是易失的,但 Jon Skeet 在他的回答中指出 CLR 不会优化副本。

But meanwhile, in order for this issue to even occur, another thread must have done something like this:但与此同时,为了让这个问题发生,另一个线程必须做了这样的事情:

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

The actual sequence might be this mixture:实际的顺序可能是这种混合:

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

The point being that OnTheEvent runs after the author has unsubscribed, and yet they just unsubscribed specifically to avoid that happening.关键是OnTheEvent在作者取消订阅后运行,但他们只是专门取消订阅以避免这种情况发生。 Surely what is really needed is a custom event implementation with appropriate synchronisation in the add and remove accessors.当然,真正需要的是在addremove访问器中具有适当同步的自定义事件实现。 And in addition there is the problem of possible deadlocks if a lock is held while an event is fired.此外,如果在触发事件时持有锁,则可能会出现死锁问题。

So is this Cargo Cult Programming ?这就是Cargo Cult 编程吗? It seems that way - a lot of people must be taking this step to protect their code from multiple threads, when in reality it seems to me that events require much more care than this before they can be used as part of a multi-threaded design.看起来是这样 - 很多人必须采取这一步骤来保护他们的代码免受多线程的影响,而实际上在我看来,事件需要比这更多的关注才能被用作多线程设计的一部分. Consequently, people who are not taking that additional care might as well ignore this advice - it simply isn't an issue for single-threaded programs, and in fact, given the absence of volatile in most online example code, the advice may be having no effect at all.因此,没有采取额外注意的人可能会忽略此建议 - 对于单线程程序来说这根本不是问题,事实上,鉴于大多数在线示例代码中没有volatile ,该建议可能有完全没有效果。

(And isn't it a lot simpler to just assign the empty delegate { } on the member declaration so that you never need to check for null in the first place?) (在成员声明上分配空delegate { }是不是更简单,这样您就不需要首先检查null了?)

Updated: In case it wasn't clear, I did grasp the intention of the advice - to avoid a null reference exception under all circumstances.更新:如果不清楚,我确实掌握了建议的意图——在任何情况下都避免出现空引用异常。 My point is that this particular null reference exception can only occur if another thread is delisting from the event, and the only reason for doing that is to ensure that no further calls will be received via that event, which clearly is NOT achieved by this technique.我的观点是,这个特定的空引用异常只有在另一个线程从事件中删除时才会发生,这样做的唯一原因是确保不会通过该事件接收到进一步的调用,这显然不是通过这种技术实现的. You'd be concealing a race condition - it would be better to reveal it!你会隐瞒比赛条件 - 最好揭示它! That null exception helps to detect an abuse of your component.该空异常有助于检测组件的滥用。 If you want your component to be protected from abuse, you could follow the example of WPF - store the thread ID in your constructor and then throw an exception if another thread tries to interact directly with your component.如果您希望您的组件免受滥用,您可以按照 WPF 的示例 - 将线程 ID 存储在您的构造函数中,然后在另一个线程尝试直接与您的组件交互时抛出异常。 Or else implement a truly thread-safe component (not an easy task).或者实现一个真正线程安全的组件(不是一件容易的事)。

So I contend that merely doing this copy/check idiom is cargo cult programming, adding mess and noise to your code.所以我认为仅仅做这个复制/检查习惯就是货物崇拜编程,给你的代码添加混乱和噪音。 To actually protect against other threads requires a lot more work.要真正防止其他线程需要更多的工作。

Update in response to Eric Lippert's blog posts:更新以响应 Eric Lippert 的博客文章:

So there's a major thing I'd missed about event handlers: "event handlers are required to be robust in the face of being called even after the event has been unsubscribed", and obviously therefore we only need to care about the possibility of the event delegate being null .所以我错过了关于事件处理程序的一件主要事情:“即使在事件被取消订阅后,事件处理程序也必须在被调用时保持健壮”,因此显然我们只需要关心事件的可能性委托为null Is that requirement on event handlers documented anywhere?对事件处理程序的要求是否记录在任何地方?

And so: "There are other ways to solve this problem; for example, initializing the handler to have an empty action that is never removed. But doing a null check is the standard pattern."所以:“还有其他方法可以解决这个问题;例如,初始化处理程序以拥有一个永远不会删除的空操作。但是进行空检查是标准模式。”

So the one remaining fragment of my question is, why is explicit-null-check the "standard pattern"?所以我的问题剩下的一个片段是,为什么显式空检查是“标准模式”? The alternative, assigning the empty delegate, requires only = delegate {} to be added to the event declaration, and this eliminates those little piles of stinky ceremony from every place where the event is raised.另一种方法是分配空委托,只需要将= delegate {}添加到事件声明中,这样就消除了每个引发事件的地方的那些小堆臭名昭著的仪式。 It would be easy to make sure that the empty delegate is cheap to instantiate.很容易确保空委托的实例化成本很低。 Or am I still missing something?还是我还缺少什么?

Surely it must be that (as Jon Skeet suggested) this is just .NET 1.x advice that hasn't died out, as it should have done in 2005?肯定是(正如 Jon Skeet 所建议的)这只是 .NET 1.x 的建议,并没有像 2005 年那样消亡吗?


UPDATE更新

As of C# 6, the answer to this question is:从 C# 6 开始,这个问题的答案是:

SomeEvent?.Invoke(this, e);

The JIT isn't allowed to perform the optimization you're talking about in the first part, because of the condition.由于条件的原因,不允许 JIT 执行您在第一部分中谈论的优化。 I know this was raised as a spectre a while ago, but it's not valid.我知道这是不久前提出的一个幽灵,但它是无效的。 (I checked it with either Joe Duffy or Vance Morrison a while ago; I can't remember which.) (我不久前曾与 Joe Duffy 或 Vance Morrison 核对过;我不记得是哪个了。)

Without the volatile modifier it's possible that the local copy taken will be out of date, but that's all.如果没有 volatile 修饰符,获取的本地副本可能会过时,但仅此而已。 It won't cause a NullReferenceException .它不会导致NullReferenceException

And yes, there's certainly a race condition - but there always will be.是的,肯定有竞争条件——但总会有的。 Suppose we just change the code to:假设我们只是将代码更改为:

TheEvent(this, EventArgs.Empty);

Now suppose that the invocation list for that delegate has 1000 entries.现在假设该委托的调用列表有 1000 个条目。 It's perfectly possible that the action at the start of the list will have executed before another thread unsubscribes a handler near the end of the list.在另一个线程取消订阅列表末尾附近的处理程序之前,列表开头的操作很可能已经执行。 However, that handler will still be executed because it'll be a new list.但是,该处理程序仍将被执行,因为它将是一个新列表。 (Delegates are immutable.) As far as I can see this is unavoidable. (代表是不可变的。)据我所知,这是不可避免的。

Using an empty delegate certainly avoids the nullity check, but doesn't fix the race condition.使用空委托当然可以避免无效性检查,但不能修复竞争条件。 It also doesn't guarantee that you always "see" the latest value of the variable.它也不能保证您总是“看到”变量的最新值。

I see a lot of people going toward the extension method of doing this ...我看到很多人都在朝着这样做的扩展方法...

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

That gives you nicer syntax to raise the event ...这为您提供了更好的语法来引发事件......

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

And also does away with the local copy since it is captured at method call time.并且还取消了本地副本,因为它是在方法调用时捕获的。

"Why is explicit-null-check the 'standard pattern'?" “为什么显式空检查'标准模式'?”

I suspect the reason for this might be that the null-check is more performant.我怀疑其原因可能是空检查性能更高。

If you always subscribe an empty delegate to your events when they are created, there will be some overheads:如果您总是在创建事件时为事件订阅一个空委托,则会产生一些开销:

  • Cost of constructing the empty delegate.构建空委托的成本。
  • Cost of constructing a delegate chain to contain it.构建委托链以包含它的成本。
  • Cost of invoking the pointless delegate every single time the event is raised.每次引发事件时调用无意义委托的成本。

(Note that UI controls often have a large number of events, most of which are never subscribed to. Having to create a dummy subscriber to each event and then invoke it would likely be a significant performance hit.) (请注意,UI 控件通常有大量事件,其中大多数从未订阅过。必须为每个事件创建一个虚拟订阅者然后调用它可能会对性能造成重大影响。)

I did some cursory performance testing to see the impact of the subscribe-empty-delegate approach, and here are my results:我做了一些粗略的性能测试以查看 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

Note that for the case of zero or one subscribers (common for UI controls, where events are plentiful), the event pre-initialised with an empty delegate is notably slower (over 50 million iterations...)请注意,对于零个或一个订阅者(对于 UI 控件很常见,其中事件很多),使用空委托预初始化的事件明显较慢(超过 5000 万次迭代......)

For more information and source code, visit this blog post on .NET Event invocation thread safety that I published just the day before this question was asked (!)有关更多信息和源代码,请访问我在提出这个问题的前一天发布的关于.NET 事件调用线程安全的博文(!)

(My test set-up may be flawed so feel free to download the source code and inspect it yourself. Any feedback is much appreciated.) (我的测试设置可能存在缺陷,因此请随时下载源代码并自行检查。非常感谢任何反馈。)

I truly enjoyed this read - not!我真的很喜欢这篇文章——不! Even though I need it to work with the C# feature called events!即使我需要它来处理称为事件的 C# 功能!

Why not fix this in the compiler?为什么不在编译器中解决这个问题? I know there are MS people who read these posts, so please don't flame this!我知道有 MS 人读过这些帖子,所以请不要喷这个!

1 - the Null issue ) Why not make events be .Empty instead of null in the first place? 1 - Null 问题)为什么不首先将事件设为 .Empty 而不是 null ? How many lines of code would be saved for null check or having to stick a = delegate {} onto the declaration?将保存多少行代码用于空检查或必须将= delegate {}粘贴到声明上? Let the compiler handle the Empty case, IE do nothing!让编译器处理 Empty 情况,IE 什么也不做! If it all matters to the creator of the event, they can check for .Empty and do whatever they care with it!如果这对事件的创建者来说很重要,他们可以检查 .Empty 并做任何他们关心的事情! Otherwise all the null checks / delegate adds are hacks around the problem!否则,所有空检查/委托添加都是围绕问题的黑客攻击!

Honestly I'm tired of having to do this with every event - aka boilerplate code!老实说,我厌倦了必须对每个事件都这样做 - 也就是样板代码!

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 - the race condition issue ) I read Eric's blog post, I agree that the H (handler) should handle when it dereferences itself, but cannot the event be made immutable/thread safe? 2 - 竞争条件问题)我阅读了 Eric 的博客文章,我同意 H(处理程序)在取消引用自身时应该处理,但不能使事件不可变/线程安全吗? IE, set a lock flag on its creation, so that whenever it is called, it locks all subscribing and un-subscribing to it while its executing? IE,在其创建时设置一个锁定标志,以便每当它被调用时,它会在执行时锁定所有订阅和取消订阅它?

Conclusion ,结论

Are not modern day languages supposed to solve problems like these for us?现代语言不应该为我们解决这些问题吗?

With C# 6 and above, code could be simplified using new ?.使用C# 6及更高版本,可以使用 new ?. operator as in:运算符如:

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

Here is the MSDN documentation. 是 MSDN 文档。

According to Jeffrey Richter in the book CLR via C# , the correct method is:根据 Jeffrey Richter 在CLR via C#一书中的说法,正确的方法是:

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

Because it forces a reference copy.因为它强制引用副本。 For more information, see his Event section in the book.有关更多信息,请参阅书中的事件部分。

I've been using this design pattern to ensure that event handlers aren't executed after they're unsubscribed.我一直在使用这种设计模式来确保事件处理程序在取消订阅后不会被执行。 It's working pretty well so far, although I haven't tried any performance profiling.到目前为止,它运行得很好,尽管我还没有尝试过任何性能分析。

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

I'm mostly working with Mono for Android these days, and Android doesn't seem to like it when you try to update a View after its Activity has been sent to the background.这些天我主要使用 Mono for Android,当您在 Activity 发送到后台后尝试更新 View 时,Android 似乎不喜欢它。

This practice is not about enforcing a certain order of operations.这种做法并不是要强制执行特定的操作顺序。 It's actually about avoiding a null reference exception.它实际上是关于避免空引用异常。

The reasoning behind people caring about the null reference exception and not the race condition would require some deep psychological research.人们关心空引用异常而不是竞争条件的原因需要一些深入的心理学研究。 I think it has something to do with the fact that fixing the null reference problem is much easier.我认为这与修复空引用问题要容易得多这一事实有关。 Once that is fixed, they hang a big "Mission Accomplished" banner on their code and unzip their flight suit.一旦解决了这个问题,他们就会在代码上挂一个大大的“任务完成”横幅,然后解开飞行服的拉链。

Note: fixing the race condition probably involves using a synchronous flag track whether the handler should run注意:修复竞争条件可能涉及使用同步标志跟踪处理程序是否应该运行

So I'm a little late to the party here.所以我在这里聚会有点晚了。 :) :)

As for the use of null rather than the null object pattern to represent events with no subscribers, consider this scenario.至于使用 null 而不是 null 对象模式来表示没有订阅者的事件,请考虑这种情况。 You need to invoke an event, but constructing the object (EventArgs) is non-trivial, and in the common case your event has no subscribers.您需要调用一个事件,但构造对象 (EventArgs) 并非易事,而且在通常情况下,您的事件没有订阅者。 It would be beneficial to you if you could optimize your code to check to see if you had any subscribers at all before you committed processing effort to constructing the arguments and invoking the event.如果您可以优化您的代码以在您提交处理工作以构造参数和调用事件之前检查您是否有任何订阅者,这对您将是有益的。

With this in mind, a solution is to say "well, zero subscribers is represented by null."考虑到这一点,一个解决方案是说“嗯,零订阅者由 null 表示”。 Then simply perform the null check before performing your expensive operation.然后在执行昂贵的操作之前简单地执行空检查。 I suppose another way of doing this would have been to have a Count property on the Delegate type, so you'd only perform the expensive operation if myDelegate.Count > 0. Using a Count property is a somewhat nice pattern that solves the original problem of allowing optimization, and it also has the nice property of being able to be invoked without causing a NullReferenceException.我想另一种方法是在 Delegate 类型上使用 Count 属性,因此只有在 myDelegate.Count > 0 时才执行昂贵的操作。使用 Count 属性是一种很好的模式,可以解决原始问题允许优化,并且它还具有能够在不导致 NullReferenceException 的情况下被调用的良好属性。

Keep in mind, though, that since delegates are reference types, they are allowed to be null.但是请记住,由于委托是引用类型,因此它们可以为空。 Perhaps there was simply no good way of hiding this fact under the covers and supporting only the null object pattern for events, so the alternative may have been forcing developers to check both for null and for zero subscribers.也许根本没有什么好的方法可以隐藏这个事实并仅支持事件的空对象模式,因此替代方案可能会迫使开发人员同时检查空订阅者和零订阅者。 That would be even uglier than the current situation.那将比现在的情况更丑陋。

Note: This is pure speculation.注意:这是纯粹的猜测。 I'm not involved with the .NET languages or CLR.我不涉及 .NET 语言或 CLR。

Please take a look here: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety This is the correct solution and should always be used instead of all other workarounds.请看这里: http ://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety 这是正确的解决方案,应始终使用而不是所有其他解决方法。

“You can ensure that the internal invocation list always has at least one member by initializing it with a do-nothing anonymous method. “您可以通过使用无操作匿名方法对其进行初始化来确保内部调用列表始终具有至少一个成员。 Because no external party can have a reference to the anonymous method, no external party can remove the method, so the delegate will never be null” — Programming .NET Components, 2nd Edition, by Juval Löwy因为没有外部方可以引用匿名方法,所以没有外部方可以删除该方法,因此委托永远不会为空” — 编程 .NET 组件,第 2 版,作者:Juval Lö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);  
}  

I don't believe the question is constrained to the c# "event" type.我不相信这个问题仅限于 c#“事件”类型。 Removing that restriction, why not re-invent the wheel a bit and do something along these lines?消除这个限制,为什么不重新发明轮子并按照这些思路做点什么?

Raise event thread safely - best practice 安全地引发事件线程 - 最佳实践

  • Ability to sub/unsubscribe from any thread while within a raise (race condition removed)能够在提升期间从任何线程订阅/取消订阅(竞争条件已删除)
  • Operator overloads for += and -= at the class level. += 和 -= 在类级别的运算符重载。
  • Generic caller-defined delegate通用调用者定义的委托

Thanks for a useful discussion.感谢您的有用讨论。 I was working on this problem recently and made the following class which is a bit slower, but allows to avoid callings to disposed objects.我最近正在解决这个问题,并制作了以下课程,虽然速度有点慢,但可以避免调用已处置的对象。

The main point here is that invocation list can be modified even event is raised.这里的要点是即使引发事件也可以修改调用列表。

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

And the usage is:用法是:

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

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

Test测试

I tested it in the following manner.我以以下方式对其进行了测试。 I have a thread which creates and destroys objects like this:我有一个线程可以创建和销毁这样的对象:

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

In a Bar (a listener object) constructor I subscribe to SomeEvent (which is implemented as shown above) and unsubscribe in 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;
    }

Also I have couple of threads which raise event in a loop.我也有几个线程在循环中引发事件。

All these actions are performed simultaneously: many listeners are created and destroyed and event is being fired at the same time.所有这些操作都是同时执行的:创建和销毁许多侦听器,同时触发事件。

If there were a race conditions I should see a message in a console, but it is empty.如果有竞争条件,我应该在控制台中看到一条消息,但它是空的。 But if I use clr events as usual I see it full of warning messages.但是如果我像往常一样使用 clr 事件,我会看到它充满了警告消息。 So, I can conclude that it is possible to implement a thread safe events in c#.因此,我可以得出结论,可以在 c# 中实现线程安全事件。

What do you think?你怎么看?

for single threaded applicaitons, you are correc this is not an issue.对于单线程应用程序,您是正确的,这不是问题。

However, if you are making a component that exposes events, there is no guarantee that a consumer of your component is not going to go multithreading, in which case you need to prepare for the worst.但是,如果您正在制作一个公开事件的组件,则无法保证您的组件的使用者不会使用多线程,在这种情况下,您需要为最坏的情况做好准备。

Using the empty delegate does solve the problem, but also causes a performance hit on every call to the event, and could possibly have GC implications.使用空委托确实可以解决问题,但也会导致每次调用事件时性能受到影响,并且可能会产生 GC 影响。

You are right that the consumer trie dto unsubscribe in order for this to happen, but if they made it past the temp copy, then consider the message already in transit.您是对的,消费者尝试取消订阅以使这种情况发生,但如果他们通过临时副本,则考虑该消息已经在传输中。

If you don't use the temporary variable, and don't use the empty delegate, and someone unsubscribes, you get a null reference exception, which is fatal, so I think the cost is worth it.如果你不使用临时变量,也不使用空委托,并且有人取消订阅,你会得到一个空引用异常,这是致命的,所以我认为这个成本是值得的。

I've never really considered this to be much of an issue because I generally only protect against this sort of potential threading badness in static methods (etc) on my reusable components, and I don't make static events.我从来没有真正认为这是一个很大的问题,因为我通常只在我的可重用组件上的静态方法(等)中防止这种潜在的线程错误,并且我不制作静态事件。

Am I doing it wrong?我做错了吗?

Wire all your events at construction and leave them alone.在施工中连接您的所有活动,不要管它们。 The design of the Delegate class cannot possibly handle any other usage correctly, as I will explain in the final paragraph of this post. Delegate 类的设计不可能正确处理任何其他用法,我将在本文的最后一段中解释。

First of all, there's no point in trying to intercept an event notification when your event handlers must already make a synchronized decision about whether/how to respond to the notification .首先,当您的事件处理程序必须已经就是否/如何响应通知做出同步决定时,尝试拦截事件通知是没有意义的。

Anything that may be notified, should be notified.任何可能被通知的事情,都应该被通知。 If your event handlers are properly handling the notifications (ie they have access to an authoritative application state and respond only when appropriate), then it will be fine to notify them at any time and trust they will respond properly.如果您的事件处理程序正确处理通知(即他们可以访问权威的应用程序状态并仅在适当时响应),那么随时通知他们并相信他们会正确响应就可以了。

The only time a handler shouldn't be notified that an event has occurred, is if the event in fact hasn't occurred!唯一不应该通知处理程序事件发生的情况是事件实际上没有发生! So if you don't want a handler to be notified, stop generating the events (ie disable the control or whatever is responsible for detecting and bringing the event into existence in the first place).因此,如果您不希望通知处理程序,请停止生成事件(即禁用控件或首先负责检测和使事件存在的任何东西)。

Honestly, I think the Delegate class is unsalvageable.老实说,我认为 Delegate 类是无法挽救的。 The merger/transition to a MulticastDelegate was a huge mistake, because it effectively changed the (useful) definition of an event from something that happens at a single instant in time, to something that happens over a timespan.向 MulticastDelegate 的合并/转换是一个巨大的错误,因为它有效地将事件的(有用的)定义从发生在某个时刻的事情变成了在一段时间内发生的事情。 Such a change requires a synchronization mechanism that can logically collapse it back into a single instant, but the MulticastDelegate lacks any such mechanism.这种变化需要一种同步机制,可以在逻辑上将其折叠回单个瞬间,但 MulticastDelegate 缺少任何此类机制。 Synchronization should encompass the entire timespan or instant the event takes place, so that once an application makes the synchronized decision to begin handling an event, it finishes handling it completely (transactionally).同步应该包括事件发生的整个时间跨度或瞬间,以便一旦应用程序做出开始处理事件的同步决定,它就会完全(事务性地)完成处理。 With the black box that is the MulticastDelegate/Delegate hybrid class, this is near impossible, so adhere to using a single-subscriber and/or implement your own kind of MulticastDelegate that has a synchronization handle that can be taken out while the handler chain is being used/modified .使用 MulticastDelegate/Delegate 混合类的黑盒,这几乎是不可能的,因此请坚持使用单订阅者和/或实现您自己的 MulticastDelegate 类型,它具有可以在处理程序链运行时取出的同步句柄正在使用/修改 I'm recommending this, because the alternative would be to implement synchronization/transactional-integrity redundantly in all your handlers, which would be ridiculously/unnecessarily complex.我推荐这个,因为替代方案是在所有处理程序中冗余地实现同步/事务完整性,这将是荒谬/不必要的复杂。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM