【问题标题】:Can events be used in a multi-threaded application safely事件可以安全地在多线程应用程序中使用吗
【发布时间】:2012-07-25 22:23:32
【问题描述】:

在下面的代码中,我有两个类,一个在单独的线程中运行并触发事件,另一个订阅此事件并从事件接收数据。我基于 Jon Skeet 的文章 http://csharpindepth.com/Articles/Chapter2/Events.aspx 的事件代码

在这篇文章http://www.codeproject.com/Articles/37474/Threadsafe-Events 中它说...

出于这个原因,我推荐 Jon Skeet 在 Delegates and Events 最后推荐的相同方法:“不要那样做”,即不要以多线程方式使用事件。如果对象上存在事件,那么只有一个线程应该能够订阅或取消订阅该事件,并且将引发该事件的线程是同一线程。

现在显然我的设计打破了这一点,因为它在与它订阅的线程不同的线程上触发事件。我怎样才能修改我的设计,使其符合不以多线程方式使用事件的原则,或者这是不可能的?

我想到的另一种方法是将我的回调方法作为委托传递给 B 类并调用它而不是调用事件?

我可能有完全错误的一端,所以任何澄清将不胜感激。

注意:我知道 .Net 4.0 显然已经解决了这个问题,但是如果有办法在 .Net 4 之前解决这个问题,我仍然很感兴趣

public delegate void MyDelegate(int a);

class A
{
    void main()
    {
        B bObject = new B();
        bObject.MyEvent += new MyDelegate(NiceMethod);
        bObject.Run();
    }   

    void NiceMethod(int a)
    {   
        Console.Writeline({0}, a);  
    }
}

class B
{
    readonly object eventLock = new object();

    MyDelegate myDel;

    public event MyDelegate MyEvent
    {   
        add
        {
            lock (eventLock)
            {
                myDel += value;
            }
        }
        remove
        {
            lock (eventLock)
            {
                myDel -= value;
            }
        }
    }   

    //Assume this runs in a new thread and calls back data using MyEvent
    //Have ommited thread code for simplicity
    public void Run()
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(1000);
            MyDelegate handler;
            lock (someEventLock)
            {
                handler = myDel;
            }
            if (handler != null)
            {
                handler (i);
            }
        }   
    }
}

【问题讨论】:

  • c# 编译器总是为类似字段的事件(即编译器为您编写添加/删除的地方)添加了线程安全性 - 对此的实现已得到改进在 c# 4 中。此外,这不是 .net 4 的东西 - 它是 ac# 4 的东西 :)
  • @MarcGravell 感谢您提供信息!

标签: c# multithreading events


【解决方案1】:

引发事件或监听来自不同线程的事件没有任何问题。侦听器负责处理从另一个线程调用。正如 Marc Gravell 在他的评论中指出的那样,编译器生成的 addremove 实现支持(并且一直)支持从不同线程的事件中添加和删除监听器。唯一的问题是以线程安全的方式引发事件,这可以通过生成的addremove 使用的同一种自旋锁同步对事件的访问来完成:

class B 
{  
    public event MyDelegate MyEvent;

    protected OnMyEvent(int p_Arg)
    {
        // Delegates are immutable and add/remove default
        // implementations always generate a new instance of the 
        // delegate. Therefore, tTmp (if not null) can be safely invoked
        var tTmp = 
            System.Threading.Interlocked
            .CompareExchange(ref MyEvent, null, null);
        if (tTmp != null) {
            tTmp(p_Arg);
        }
    }

    //Assume this runs in a new thread and calls back data using MyEvent 
    //Have ommited thread code for simplicity 
    public void Run() 
    { 
        for (int i = 0; i < 100; i++) 
        { 
            OnMyEvent(i);
        }    
    } 
} 

唯一可能发生的情况是侦听器在从事件列表中删除之后被调用。恕我直言,侦听器必须能够处理这种情况,因为它处理从不同线程调用的beeing...

【讨论】:

  • C# 编译器并不总是正确支持从不同线程添加和删除处理程序。旧的 C# 编译器生成的代码对于订阅自己的事件的类来说根本不是线程安全的,并且在添加/删除其他对象的事件时会锁定 this
  • 我认为变化发生在 C# 3 和 C# 4 之间。使旧编译器的情况真正可怕的是,如果一个类有一个名为 Foo 的事件,那么在类之外,thing.Foo += bar将调用AddHandler 方法(这将锁定thing),但在类中,Foo += bar 会将Foo 视为私有委托的名称,并将该语句解释为Foo = Foo + bar;,没有锁定。最终结果是代码假装是线程安全的,但如果一个类在内部使用它的事件,那么代码实际上根本就不是线程安全的。
  • 就个人而言,我认为定义一个名为Foo 的事件永远不应该定义一个名为Foo 的委托;相反,委托应该有一些其他名称(例如Foo__event),并且尝试“调用”事件应该被转换为对具有不同名称的方法的调用(例如Foo__raiseevent),然后可以酌情对委托采取行动。
猜你喜欢
  • 2022-01-10
  • 2011-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多