目录
检查空值和引发事件的三种最常见方法
在网上的文章中,你会发现很多关于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不是线程安全的,而方法B和C是检查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分配给了代理。如果我们假设Event和Delegates是不同的类型(见下文),从概念上讲编译器正在执行隐式转换,这与我们编写的相同:
//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)中,我们有隐式转换从Event到Delegate。但是,Event和Delegate在C#中是相同的还是不同的类型?
如果您查看[3],您会发现作者Jon Skeet强烈认为Event和Delegate是不一样的。去引用:
”事件不是委托实例。不幸的是,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#委托。
我倾向于这样想:Events和Delegates严格来说是不一样的,但在C#语言中,它们似乎以非常相似的方式被互换处理,业界已经习惯把它们当作是相同的、可以互换的东西来谈论。即使在Microsoft文档[2]中,作者在讨论空条件运算符“?.”时也可以互换使用术语Event和Delegate。有一刻,作者谈到“..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是同步调用的。我们将创建2 EventHandler(C11)并将在第一个执行期间,使用竞争线程(C12)进行攻击并尝试删除第二个处理程序。我们将从打印输出中看到攻击失败并且两个EventHandler都被执行了。查看(C13)处的输出很有趣,该输出显示EventC之后,报告减少了EventHandler的数量。
结论
最好的解决方案是避免线程竞争情况,并从单个线程访问事件。但是,如果您需要,基于null -条件运算符的方法C是检查null值并触发Event的完美方式。
参考
- [1] c# - what if i will copy a reference to an event object to another object and will change an event object afterwards? - Stack Overflow
- [2] Member access operators and expressions - C# reference | Microsoft Docs
- [3] https://jonskeet.uk/csharp/events.html
https://www.codeproject.com/Articles/5327025/Thread-safe-Events-in-Csharp