突然想到有关C#中使用event特性时关于线程安全的问题,以前虽然有遵从“复制引用+null判断”的模式(盲目地),但没有深入了解和思考。

为之查询了资料和实验,对此有了进一步的理解。

一般event使用模式

定义(field-like event):

public event EventHandler Done;

类内raise:

protected void OnDone()
{
var done = Done;
if (done != null)
{
done(this, new EventArgs());
}
}

不禁要问,为何要复制引用?多线程下表现如何?

关于C#3.0和C#4.0中编译器对event实现的整理

为了解决上面哪些疑惑,我查了一些资料,其中有来自当时C#编译器开发组成员的一篇博文 Field-like Events Considered Harmful

这篇博文介绍了C#3.0中编译器对于field-like event(也是最常见的使用方式)的实现。

对于如此的代码,

class EventInCS3
{
public event EventHandler Done;
}

编译器会将其转换成:

class EventInCS3
{
private EventHandler __Done; //
public event EventHandler Done
{
add
{
lock (this) //
{
__Done = __Done + value; //
}
}
remove
{
lock (this) { __Done = __Done - value; }
}
}
}

有以下几点值得注意(同注释编号):

1.event下隐藏的真正delegate链。实际上我们使用的是子类MulticastDelegate(可以参考 开源的coreclr实现)。

3.正如+、-操作符对于string类型是起字符串组合作用,其对于delegate类型也同样是起到两条链的组合作用(参考 MSDN),实际上是调用了Delegate.Combine和Delegate.Remove。同时也引入了经典的线程问题(修改丢失)。

2.为了解决多线程问题,使用了lock。

(就先不管这个lock(this)了。当然上面提到的 博文 里提到了,编译器并不是通过lock,继而通过Monitor的静态方法来同步,而是通过IL即MethodImplAttribute(MethodImplOptions.Synchronized)实现。这些都是C#本身不推荐的方法。)

而在C#4.0中,同步的实现有了变化,同样参见同一作者两年后的 这一篇博文

编译器默认的add、remove实现,改为使用compare and swap来实现lock-free同步。值得注意的是,delegate是不可更改的类型,即+=、-=之后,会指向一个新的对象,而不再是原对象(类似string)。

通过IL查看程序集里生成的add_Done、remove_Done,可以发现端倪,大致会生成如下的代码:

static void add_Done(EventHandler value)
{
EventHandler V_0 = __Done;
EventHandler V_1, V_2;
do
{
V_1 = V_0;
V_2 = (EventHandler)Delegate.Combine(V_1, value);
V_0 = Interlocked.CompareExchange<EventHandler>(ref __Done, V_2, V_1);
} while (V_0 != V_1);
}

C#4.0中event相关的语义变化整理

在同一作者的 另一篇博文 中,介绍了C#4.0中event相关的语义变化,主要是+=、-=操作符的语义变化

在C#3.0中,对于一个event,如果在该类之外访问这个event,则会被认为是访问这个event本身,如我们熟知的只能通过+=、-=这两个操作符来访问(即是调用对应的add、remove访问器);而在类的内部,所有对这个event的访问,都会被认为是访问作为event实现的delegate本身(即访问Done,实际上访问到的是__Done)。

这么处理的话,我们就能在OnDone方法里复制引用,判断null,进行调用。因为此时Done这个标识符,代表的是一个EventHandler对象的引用。

C#3.0的问题也在于此,这种情况下,我们写下

Done += SomeHandlerMethod;

时,+=实际是调用了:

EventHandler EventHandler.operator +(EventHandler left, EventHandler right)

在Visual Studio 2015里写一个普通的、非event的EventHandler的+=运算,鼠标放在+=上时,显示的也是这个函数签名。C#3.0时即使对event也是这么处理的。

导致我们失去了默认add访问器提供的同步功能

而这一现象在C#4.0中得到了改善。在类内部访问event的标识符时,+=、-=操作符就会被认为是add、remove的调用了。

可知在C#4.0写下同样的代码时,+=调用的签名为:

void EventInCS4.Done.add 

自定义event访问器

自定义event时(非field-like event),我们自己编写的add、remove访问器就没有默认的同步了。如果要考虑线程安全,需要手动加上同步(比如lock(someLockObject))。

此时,在类内部访问event标识符,只会被当成是访问event本身。要引发事件(Done)的话,需访问对应delegate(__Done(this, new EventArgs()))。

操作event的正确方式

一般情况下无需自己实现event,用field-like就好了。

因为不管是通过event标识符访问delegate(field-like event),还是直接访问delegate(自定义event),我们得到的都是delegate对象的引用,而且delegate对象是不可更改的。引用的复制是原子的。所以我们可以随意地复制该delegate的引用,然后判断null并invoke。

一些code snippet如:

  1. 通过扩展方法来引发事件:

    public static class EventExtension
    {
    public static void Raise<T>(this EventHandler<T> handler, object sender, T args)
    {
    if (handler != null)
    {
    handler(sender, args);
    }
    }
    public static void Raise(this EventHandler handler, object sender, EventArgs args); // 重载版
    }

    delegate的引用会以pass-by-value形式得到复制,所以直接

    Done.Raise(this, new EventArgs());
  2. 通过C#6.0提供的Null-conditional操作符
    Done?.Invoke(this, new EventArgs());

    null-conditional操作符也会进行引用的复制,所以是线程安全的。(没有Done?(...)这种写法)

对于编译器是否会将复制引用作为重复的局部变量优化掉,以至于在一些情况下需要使用诸如以下的方式的问题,我没有深入了解。

Interlocked.CompareExchange(ref Done, null, null);

简单查询一下之后,得知对于微软自家的CLR无需关心这个问题,盖其遵循较严格的内存模型(memory model),不会引入新的读取操作。但其他情况下有可能存在这样的问题。相关文章和讨论链接如下:

  1. http://stackoverflow.com/questions/11159176/thread-safe-event-calls
  2. http://code.logos.com/blog/2008/11/events_and_threads_part_4.html
  3. Understand the Impact of Low-Lock Techniques in Multithreaded Apps MSDN Magazine Oct 2005 (需要下载chm看)

C# event线程安全的更多相关文章

  1. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  2. python基础-12 多线程queue 线程交互event 线程锁 自定义线程池 进程 进程锁 进程池 进程交互数据资源共享

    Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就比别人NB. 我们先了解一下什么是进程和线程. 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CP ...

  3. 死锁与递归锁 信号量 event 线程queue

    1.死锁现象与递归锁 死锁:是指两个或两个以上的进程或线程在执行过程中,因争抢资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相 ...

  4. Python并发编-用Event,线程检测数据库连接的例子

    尝试3次连接数据库 import time import random from threading import Thread,Event def connect_db(e): count = 0 ...

  5. python第五十一天----线程,Event,队列

    进程与线程的区别: 线程==指令集,进程==资源集  (线程集) 1.同一个进程中的线程共享内存空间,进程与进程之间是独立的 2.同一个进程中的线程是可以直接通讯交流的,进程与间通讯必需通过一个中间的 ...

  6. 11 并发编程-(线程)-信号量&Event&定时器

    1.信号量(本质也是一把锁)Semaphore模块 信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行, 信号量同一时间可以有5个任务拿到锁去执行, 如果说互斥锁是合租 ...

  7. Python之网路编程之死锁,递归锁,信号量,Event事件,线程Queue

    一.死锁现象与递归锁 进程也是有死锁的 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用, 它们都将无法推进下去.此时称系统处于死锁状态或系统 ...

  8. [并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁]

    [并发编程 - 多线程:信号量.死锁与递归锁.时间Event.定时器Timer.线程队列.GIL锁] 信号量 信号量Semaphore:管理一个内置的计数器 每当调用acquire()时内置计数器-1 ...

  9. Python_Day10_进程、线程、协程

    本节内容    操作系统发展史介绍    进程.与线程区别    python GIL全局解释器锁    线程        语法        join        线程锁之Lock\Rlock\ ...

随机推荐

  1. ubuntu 搭建samba共享方案

    1.samba服务安装搭建 sudo apt-get install samba sudo vim /etc/samba/smb.conf workgroup = szsoft 设置用户密码登陆方式s ...

  2. ES6 箭头函数下的this指向

    在javscript中,this 是在函数运行时自动生成的一个内部指针,它指向函数的调用者. 箭头函数有些不同,它的this是继承而来, 默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象. ...

  3. 脚本设置IP bat 命令行设置自动获取IP和固定IP

    由于办公室网络需要固定IP和DNS才能上网, 在连接公共网络或者家里又需要自动获取IP和DNS才能上网. 频繁手动切换很麻烦,就搞了两个脚本一键设置. 1.新建文本文件, 命名为固定IP.bat 复制 ...

  4. SQL Server 合并复制的Article可以指定单个对象的更新方向

    如下所示,这是SQL Server中一个合并复制发布端的Article: 我们可以在Article中选择一个对象,比如这里我们选择MD.Car表,点击鼠标右键,选择"Set Properti ...

  5. CPU IO MEM NETWork 监控命令

    性能优化中CPU.内存.磁盘IO.网络性能的依赖(上) 性能优化中CPU.内存.磁盘IO.网络性能的依赖(下)

  6. C# 添加日志 log4net

    1.首先在项目中添加Nuget程序包... 2.然后在NuGet窗体中搜索Log4Net,然后点击安装<安装过程可能会持续几分钟,请耐心等待> 3.在项目中添加一个Config文件,如已有 ...

  7. pt-heartbeat --update --daemonize 只执行一次秒退的问题

    使用pt-heartbeat帮助测试MySQL从库delay时长- 第一次运行测试成功,运行顺畅. 过几天再次用--update测试时,发现pt-heartbeat只update一次就自己退出了,-- ...

  8. 中断标志位 IRQF_ONESHOT

    one shot本身的意思的只有一次的,结合到中断这个场景,则表示中断是一次性触发的,不能嵌套.对于primary handler,当然是不会嵌套,但是对于threaded interrupt han ...

  9. 菜鸟对APP界面设计的一些心得小结

    1. 前言 当我看着我以前做的一些app界面,我意识到我应该把我的界面设计能力水平再提升一个,因为实在是丑啊!贴一些以前的设计: 现在看来,是不能看的了.我主要是做需求设计,后面也有一些美工的工作,我 ...

  10. Python3编写网络爬虫03-正则表达式的使用

    一.正则表达式(处理字符串强大的工具,有特定的语法结构)功能:实现字符串的检索,替换,匹配验证实例: 地址:http://tool.oschina.net/regex/ #测试文本Hello,my p ...