目录
检查空值和引发事件的三种最常见方法
分析方法A
分析方法B
分析方法C
事件与代理相同吗?
三种改进方法中的竞争线程攻击
结论
参考
- 下载源代码 - 81.5 KB
在网上的文章中,你会发现很多关于c#中检查null值和触发Event的最佳线程安全方法的讨论。通常,提到和讨论了三种方法:
public static event EventHandler 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。因此,我们基本上将一个Event分配给了代理。如果我们假设Event和Delegates是不同的类型(见下文),从概念上讲编译器正在执行隐式转换,这与我们编写的相同:
//not needed, just a concept of what compiler it is implicitly doing
EventHandler TmpEvent = EventA as EventHandler; //(**)
正如[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 EventA;
public static event EventHandler EventB;
public static event EventHandler EventC;
和:
public static EventHandler EventA { get; set; } = null;
public static EventHandler EventB { get; set; } = null;
public static EventHandler EventC { get; set; } = null;
一切仍然有效。此外,尝试以下代码很有趣:
public static event EventHandler EventD1;
public static EventHandler EventD2 { get; set; } = null;
public static EventHandler 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 EventA;
public static event EventHandler EventB;
public static event EventHandler 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