C# 如果我将对事件对象的引用复制到另一个对象,然后更改事件对象,会怎么样?
我正在读一本关于C#和CLR的书,a一点也不懂。全文如下: 为了修复这种竞争条件,许多开发人员编写了OnNewMail方法,如下所示:C# 如果我将对事件对象的引用复制到另一个对象,然后更改事件对象,会怎么样?,c#,.net,events,delegates,clr,C#,.net,Events,Delegates,Clr,我正在读一本关于C#和CLR的书,a一点也不懂。全文如下: 为了修复这种竞争条件,许多开发人员编写了OnNewMail方法,如下所示: //版本2 受保护的虚拟void OnNewMail(NewMailEventArgs e){ EventHandler temp=NewMail; 如果(temp!=null)temp(this,e); } 这里的想法是将对NewMail的引用复制到临时变量temp中,temp在执行赋值时引用委托链。现在,这个方法比较temp和null并调用temp,所以在
//版本2
受保护的虚拟void OnNewMail(NewMailEventArgs e){
EventHandler temp=NewMail;
如果(temp!=null)temp(this,e);
}
这里的想法是将对NewMail的引用复制到临时变量temp中,temp在执行赋值时引用委托链。现在,这个方法比较temp和null并调用temp,所以在分配给temp之后,如果另一个线程更改了NewMail,这并不重要。记住,委托是不可变的,这就是为什么这种技术在理论上是有效的
因此,问题是:为什么它会起作用?我认为,temp和NewMail对象是指同一个对象,不需要更改任何事项——结果将对两者都产生影响。谢谢 CLR中的委托类型尤其是
多播委托
类型,尽管它们是引用类型,但属于罕见的一组类型,称为“不可变”类型。这意味着此类类型的引用赋值操作创建实例的副本,而常规引用类型的赋值只复制引用的值。所以当你写作时:
EventHandler<NewMailEventArgs> temp = NewMail;
EventHandler temp=NewMail;
创建由
NewMail
引用的代理的新副本,并将对该副本的引用分配给temp
变量,因此,在执行这一行之后,EventHandler
委托将有两个实例:由NewMail
引用的实例和由temp
引用的另一个实例(不是您可能认为的由两个变量引用的单个实例)。这就是为什么现在可以安全地调用由temp
指向的委托,因为在调用委托的一段时间内,它不能被另一个线程设置为null 避免并发问题的可靠方法是使用System.Threading.Volatile.Read方法。可以通过未记录的编译器优化删除临时变量。目前它运行良好,但将来可能会改变
using System;
using System.Threading;
namespace EventHandling
{
class Program
{
static void Main(string[] args)
{
var eventProvider = new EventProvider();
eventProvider.Event += (sender, e) => Console.WriteLine("Event fired");
eventProvider.FireEvent();
}
}
class EventProvider
{
public event EventHandler Event;
protected void OnEvent(EventArgs e) => Volatile.Read(ref Event)?.Invoke(this, e);
public void FireEvent() => OnEvent(EventArgs.Empty);
}
}
要探索并发如何影响事件处理,可以尝试以下代码:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace EventHandling
{
class Program
{
static void Main(string[] args)
{
while(true)
new Program().Run();
}
private void Run()
{
var eventProvider = new EventProvider();
eventProvider.Event += HandleEvent;
Console.WriteLine("subscribed");
var unsubscribe = new Task(() =>
{
eventProvider.Event -= HandleEvent;
Console.WriteLine("unsubscribed");
});
var fireEvent = new Task(() => eventProvider.FireEvent());
fireEvent.Start();
unsubscribe.Start();
Task.WaitAll(fireEvent, unsubscribe);
Console.ReadLine();
}
private void HandleEvent(object sender, EventArgs e) => Console.WriteLine("Event fired");
}
class EventProvider
{
public event EventHandler Event;
protected void OnEvent(EventArgs e)
{
var temp = Volatile.Read(ref Event);
Console.WriteLine("temp delegate created");
Thread.Sleep(25); // time to unsubscribe concurrently
if (temp != null)
{
Console.WriteLine("temp delegate invoking");
temp.Invoke(this, e);
Console.WriteLine("temp delegate invoked");
}
else
Console.WriteLine("temp delegate is empty");
}
public void FireEvent() => OnEvent(EventArgs.Empty);
}
}
有时输出为:
subscribed
unsubscribed
temp delegate created
temp delegate is empty
有时:
subscribed
temp delegate created
unsubscribed
temp delegate invoking
Event fired
temp delegate invoked
如果你能提供一个。另外,请明确书的名称和页码。它们指的是同一个对象,但它们是两个独立的变量(它们只是内存中的两个位置,其值在某个时间点恰好相同,并且两个值都引用原始对象占用的内存)。如果将一个变量的值更改为不同的值,则另一个变量仍然具有对象的值(引用)
temp和NewMail对象引用同一对象
No,它们是指向同一对象的两个变量。如果NewMail
被分配给一个新对象,那么您将有两个变量指向两个不同的对象。分配给一个变量不会影响另一个变量。好的,我想我应该阅读更多关于分配额的内容。谢谢。您需要更好地理解1)Volatile
不是防弹的。领先的并发专家。2) 示例中使用的将生成包含临时变量的代码。换句话说,它使用相同的方法来修复比赛条件。@TheodorZoulias,谢谢你的文章。很有趣。临时变量本身不是问题所在。该点是不稳定的。读取可防止编译器将其删除。杰弗里·里克特(Jeffrey Richter)在其著作《CLR通过C#》中提出了解决方案。
subscribed
temp delegate created
unsubscribed
temp delegate invoking
Event fired
temp delegate invoked