zoukankan      html  css  js  c++  java
  • C# event线程安全

    突然想到有关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; // 1
        public event EventHandler Done
        {
            add
            {
                lock (this) // 2
                {
                    __Done = __Done + value; // 3
                }
            }
            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看)
  • 相关阅读:
    UIScrollView 截图
    cocoapods import 第三方 自动补全
    UIWebView内存泄露问题解决方法
    iOS常用小控件集合
    UIViewController视图控制器视图的生命周期
    UIView
    UITextField
    UITabBar UITabBarController
    iOS图片相似度比较
    iOS获取已安装的app列表(私有库)+ 通过包名打开应用
  • 原文地址:https://www.cnblogs.com/wwwbdabc/p/11652909.html
Copyright © 2011-2022 走看看