C#中的线程安全事件

目录

检查空值和引发事件的三种最常见方法

分析方法A

分析方法B

分析方法C

事件与代理相同吗?

三种改进方法中的竞争线程攻击

结论

参考


检查空值和引发事件的三种最常见方法

在网上的文章中,你会发现很多关于c#中检查null值和触发Event的最佳线程安全方法的讨论。通常,提到和讨论了三种方法:

public static event EventHandler<EventArgs> MyEvent;

Object obj1 = new Object();
EventArgs args1 = new EventArgs();

//Method A
if (MyEvent != null)            //(A1)
{
    MyEvent(obj1, args1);       //(A2)
}

//Method B
var TmpEvent = MyEvent;         //(B1)
if (TmpEvent != null)           //(B2)
{
    TmpEvent(obj1, args1);      //(B3)
}

//Method C
MyEvent?.Invoke(obj1, args1);   //(C1)

让我们立即给出一个答案:方法A不是线程安全的,而方法BC是检查null值和触发Event的线程安全方法。让我们对它们中的每一个进行分析。

分析方法A

为了避免NullReferenceException,在(A1)中我们检查null,然后在(A2)中我们触发Event。问题是在(A1)(A2)之间的时间里,其他一些线程可以访问Event MyEvent并更改其状态。因此,这种方法不是线程安全的。我们在我们的代码(如下)中演示了这一点,我们成功地对这种方法发起了竞态线程攻击。

分析方法B

理解这种方法的关键是真正理解(B1)中发生的事情。在那里,我们在它们之间有对象和分配。

一开始,有人可能会想,我们有两个C#对象引用和它们之间的赋值,所以,它们应该指向同一个C#对象。这不是这里的情况,因为那样就没有分配的意义了。事件是C#对象(您可以分配Object obj=MyEvent,这是合法的),但(B1)中的分配在此处有所不同。

编译器生成的TmpEvent真实类型是EventHandler<EventArgs>。因此,我们基本上将一个Event分配给了代理。如果我们假设EventDelegates是不同的类型(见下文),从概念上讲编译器正在执行隐式转换,这与我们编写的相同:

//not needed, just a concept of what compiler it is implicitly doing
EventHandler<EventArgs> TmpEvent = EventA as EventHandler<EventArgs>;  //(**)

正如[1]中所解释的,委托是不可变的引用类型。这意味着此类类型的引用分配操作会创建实例的副本,这与仅复制引用值的常规引用类型的分配不同。这里的关键是InvocationList(类型为Delegate[])真正发生了什么,其中包含所有添加的委托的列表。似乎该列表在该作业中被克隆。这就是方法B可以工作的关键原因,因为没有其他人可以访问新创建的变量TmpEvent及其内部类型Delegate[]InvocationList

我们在我们的代码(如下)中演示了这种方法是线程安全的,我们对这种方法发起了线程竞争攻击。

分析方法C

此方法基于可从C#6获得的null-条件运算符。为了线程安全,我们需要信任Microsoft及其文档。在[2]中,他们说:

“ '?.'运算符对其左侧操作数的评估不超过一次,保证在验证为非空后不能更改为null……。使用?.运算符检查委托是否为非空,并以线程安全的方式调用它(例如,当您引发事件时)。

我们在我们的代码(如下)中演示了这种方法是线程安全的,我们对这种方法发起了线程竞争攻击。

事件与代理相同吗?

在上述(**)处的文本中,我们认为在(B1)中,我们有隐式转换从EventDelegate。但是,EventDelegateC#中是相同的还是不同的类型?

如果您查看[3],您会发现作者Jon Skeet强烈认为EventDelegate是不一样的。去引用:

事件不是委托实例。不幸的是,C#允许您在某些情况下以相同的方式使用它们,但了解它们的区别非常重要。我发现理解事件的最简单方法是将它们想象成属性。虽然属性看起来像是字段,但它们绝对不是……事件是成对的方法,在IL中适当地装饰以将它们联系在一起…… ”

所以,根据上面Jon Skeet的文字,我们可以接受事件就像一种特殊的属性的解释。按照这个类比,我们可以在下面的演示程序中替换:

public static event EventHandler<EventArgs> EventA;
public static event EventHandler<EventArgs> EventB;
public static event EventHandler<EventArgs> EventC;

和:

public static EventHandler<EventArgs> EventA { get; set; } = null;
public static EventHandler<EventArgs> EventB { get; set; } = null;
public static EventHandler<EventArgs> EventC { get; set; } = null;

一切仍然有效。此外,尝试以下代码很有趣:

public static event EventHandler<EventArgs> EventD1;
public static EventHandler<EventArgs> EventD2 { get; set; } = null;
public static EventHandler<EventArgs> EventD3;

EventD1 = EventD2 = EventD3 = delegate { };
Console.WriteLine("Type of EventD1: {0}", EventD1.GetType().Name);
Console.WriteLine("Type of EventD2: {0}", EventD2.GetType().Name);
Console.WriteLine("Type of EventD3: {0}", EventD3.GetType().Name);

你会得到回应:

Type of EventD1: EventHandler`1
Type of EventD2: EventHandler`1
Type of EventD3: EventHandler`1

但是,回到现实,事件是由event关键字创建的,因此它们是C#语言中的单独构造,然后是属性或委托。我们可以解释它们是相似的属性或委托,但它们并不相同。事实是,事件是编译器使用该关键字event所做的任何事情,似乎它使它们看起来像C#委托。

我倾向于这样想:EventsDelegates严格来说是不一样的,但在C#语言中,它们似乎以非常相似的方式被互换处理,业界已经习惯把它们当作是相同的、可以互换的东西来谈论。即使在Microsoft文档[2]中,作者在讨论空条件运算符“?.”时也可以互换使用术语EventDelegate。有一刻,作者谈到“..raise an event”,然后下一句说“...delegate instances are immutable...”等。

三种改进方法中的竞争线程攻击

为了验证三种提议方法的线程安全性,我们创建了一个小型演示程序。这个程序并不是对所有情况都有明确的答案,也不能被认为是证明,但仍然可以展示/演示一些有趣的观点。为了设置竞争情况,我们通过一些Thread.Sleep()调用来减慢线程。

这是演示代码:

internal class Client
{
    public static event EventHandler<EventArgs> EventA;
    public static event EventHandler<EventArgs> EventB;
    public static event EventHandler<EventArgs> EventC;
    public static void HandlerA1(object obj, EventArgs args1)
    {
        Console.WriteLine("ThreadId:{0}, HandlerA1 invoked",
            Thread.CurrentThread.ManagedThreadId);
    }
    public static void HandlerB1(object obj, EventArgs args1)
    {
        Console.WriteLine("ThreadId:{0}, HandlerB1 invoked",
            Thread.CurrentThread.ManagedThreadId);
    }

    public static void HandlerC1(object obj, EventArgs args1)
    {
        Console.WriteLine("ThreadId:{0}, HandlerC1 - Start",
            Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        Console.WriteLine("ThreadId:{0}, HandlerC1 - End",
            Thread.CurrentThread.ManagedThreadId);
    }
    public static void HandlerC2(object obj, EventArgs args1)
    {
        Console.WriteLine("ThreadId:{0}, HandlerC2 invoked",
            Thread.CurrentThread.ManagedThreadId);
    }

    static void Main(string[] args)
    {
        // Demo Method A for firing of Event-------------------------------
        Console.WriteLine("Demo A =========================");

        EventA += HandlerA1;

        Task.Factory.StartNew(() =>  //(A11)
        {
            Thread.Sleep(1000);
            Console.WriteLine("ThreadId:{0}, About to remove handler HandlerA1",
                Thread.CurrentThread.ManagedThreadId);
            EventA -= HandlerA1;
            Console.WriteLine("ThreadId:{0}, Removed handler HandlerA1",
                Thread.CurrentThread.ManagedThreadId);
        });

        if (EventA != null)
        {
            Console.WriteLine("ThreadId:{0}, EventA is null:{1}",
                Thread.CurrentThread.ManagedThreadId, EventA == null);
            Thread.Sleep(2000);
            Console.WriteLine("ThreadId:{0}, EventA is null:{1}",
                Thread.CurrentThread.ManagedThreadId, EventA == null);

            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();

            try
            {
                EventA(obj1, args1);  //(A12)
            }
            catch (Exception ex)
            {
                Console.WriteLine("ThreadId:{0}, Exception:{1}",
                    Thread.CurrentThread.ManagedThreadId, ex.Message);
            }
        }

        // Demo Method B for firing of Event-------------------------------
        Console.WriteLine("Demo B =========================");

        EventB += HandlerB1;

        Task.Factory.StartNew(() =>  //(B11)
        {
            Thread.Sleep(1000);
            Console.WriteLine("ThreadId:{0}, About to remove handler HandlerB1",
                Thread.CurrentThread.ManagedThreadId);
            EventB -= HandlerB1;
            Console.WriteLine("ThreadId:{0}, Removed handler HandlerB1",
                Thread.CurrentThread.ManagedThreadId);
        });

        var TmpEvent = EventB;
        if (TmpEvent != null)
        {
            Console.WriteLine("ThreadId:{0}, EventB is null:{1}",
                Thread.CurrentThread.ManagedThreadId, EventB == null);
            Console.WriteLine("ThreadId:{0}, TmpEvent is null:{1}",
                Thread.CurrentThread.ManagedThreadId, TmpEvent == null);
            Thread.Sleep(2000);
            Console.WriteLine("ThreadId:{0}, EventB is null:{1}",   //(B13)
                Thread.CurrentThread.ManagedThreadId, EventB == null);
            Console.WriteLine("ThreadId:{0}, TmpEvent is null:{1}",   //(B14)
                Thread.CurrentThread.ManagedThreadId, TmpEvent == null);

            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();

            try
            {
                TmpEvent(obj1, args1);  //(B12)
            }
            catch (Exception ex)
            {
                Console.WriteLine("ThreadId:{0}, Exception:{1}",
                    Thread.CurrentThread.ManagedThreadId, ex.Message);
            }
        }

        // Demo Method C for firing of Event-------------------------------
        Console.WriteLine("Demo C =========================");

        EventC += HandlerC1;
        EventC += HandlerC2;  //(C11)

        Task.Factory.StartNew(() =>   //(C12)
        {
            Thread.Sleep(1000);
            Console.WriteLine("ThreadId:{0}, About to remove handler HandlerC2",
                Thread.CurrentThread.ManagedThreadId);
            EventC -= HandlerC2;
            Console.WriteLine("ThreadId:{0}, Removed handler HandlerC2",
                Thread.CurrentThread.ManagedThreadId);
        });

        Console.WriteLine("ThreadId:{0}, EventC has EventHandlers:{1}",
            Thread.CurrentThread.ManagedThreadId, EventC?.GetInvocationList().Length);

        try
        {
            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();

            EventC?.Invoke(obj1, args1);

            Console.WriteLine("ThreadId:{0}, EventC has EventHandlers:{1}",
            Thread.CurrentThread.ManagedThreadId, EventC?.GetInvocationList().Length);  //(C13)
        }
        catch (Exception ex)
        {
            Console.WriteLine("ThreadId:{0}, Exception:{1}",
                Thread.CurrentThread.ManagedThreadId, ex.Message);
        }

        Console.WriteLine("End =========================");
        Console.ReadLine();
    }
}

这是执行结果:

A)为了攻击方法A,我们在(A11)推出了新的触发线程,它会造成一些伤害。我们会看到它在(A12)处创建NullReferenceException成功

B为了攻击方法B,我们在(B11)推出了新的触发线程,这将造成一些伤害。我们将看到在(B12)处不会发生任何事情,这种方法将在这次攻击中幸存下来。关键是在(B13)(B14)处的打印输出,这将表明TmpEvent不受更改为EventB的影响。

C)我们将以不同的方式攻击方法C。我们知道EventHandler是同步调用的。我们将创建EventHandler(C11)并将在第一个执行期间,使用竞争线程(C12)进行攻击并尝试删除第二个处理程序。我们将从打印输出中看到攻击失败并且两个EventHandler都被执行了。查看(C13)处的输出很有趣,该输出显示EventC之后,报告减少了EventHandler的数量。

结论

最好的解决方案是避免线程竞争情况,并从单个线程访问事件。但是,如果您需要,基于null -条件运算符的方法C是检查null值并触发Event的完美方式。

参考

https://www.codeproject.com/Articles/5327025/Thread-safe-Events-in-Csharp

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值