zoukankan      html  css  js  c++  java
  • 以线程安全方式引发事件(修正)

      《CLR via C#》3rd中提到,应该以线程安全的方式引发事件,不禁冒冷汗,一直以来还真没注意到这个问题,以前写的不少代码得重新审查修正了。下面是引用原文说明:

    .Net Framework最初发布时,是建议开发者用以下方式引发事件:

    protected virtual void OnNewMail(NewMailEventArgs e)
    {
    if (NewMail != null) NewMail(this, e);
    }

    这个OnNewMail方法的问题在于,线程可能发现NewMail不为null,然后,就在调用NewMail之前,另一个线程从委托链中移除了一个委托,是NewMail变成了null。这会造成抛出一个NullReferenceException异常。

      于是我写了以下代码测试重现这种线程竞态的情况: 

    代码
    using System;
    using System.Threading;
    using System.Diagnostics;

    namespace Neutra.Utils
    {
    class EventTest
    {
    public event EventHandler MyEvent;

    static void Main(string[] args)
    {
    Console.WriteLine(
    "Test1 start");
    Test1();
    Console.WriteLine(
    "Test1 end");

    Console.WriteLine(
    "Test2 start");
    Test2();
    Console.WriteLine(
    "Test2 end");
    }

    static void AddAndRemoveEventHandler(object obj)
    {
    var instance
    = obj as EventTest;
    for (int i = 0; i < 100000; i++)
    {
    instance.MyEvent
    += HandleEvent;
    Thread.Sleep(
    0);
    instance.MyEvent
    -= HandleEvent;
    Thread.Sleep(
    0);
    }
    }

    static void HandleEvent(object sender, EventArgs e)
    {
    Console.Write(
    '>');
    }

    static void Test1()
    {
    var sw
    = Stopwatch.StartNew();
    var instance
    = new EventTest();
    Thread thread
    = new Thread(AddAndRemoveEventHandler);
    thread.Start(instance);
    int i = 0;
    try
    {
    for (i = 0; i < 2000; i++)
    {
    if (instance.MyEvent != null)
    {
    instance.MyEvent(instance, EventArgs.Empty);
    }
    Thread.Sleep(
    0);
    }
    Console.WriteLine();
    }
    catch (Exception exception)
    {
    sw.Stop();
    Console.WriteLine();
    Console.WriteLine(
    "index = {0}, time: {1}", i, sw.Elapsed);
    Console.WriteLine(exception);
    }
    finally
    {
    thread.Abort();
    thread.Join();
    }
    }

    static void Test2()
    {
    var sw
    = Stopwatch.StartNew();
    var instance
    = new EventTest();
    Thread thread
    = new Thread(AddAndRemoveEventHandler);
    thread.Start(instance);
    int i = 0;
    try
    {
    for (i = 0; i < 2000; i++)
    {
    var handler
    =Interlocked.CompareExchange(ref instance.MyEvent, null, null);
    if (handler != null)
    {
    handler(instance, EventArgs.Empty);
    }
    Thread.Sleep(
    0);
    }
    Console.WriteLine();
    }
    catch (Exception exception)
    {
    sw.Stop();
    Console.WriteLine();
    Console.WriteLine(
    "index = {0}, time: {1}", i, sw.Elapsed);
    Console.WriteLine(exception);
    }
    finally
    {
    thread.Abort();
    thread.Join();
    }
    }
    }
    }

      我测试了好几次,index最小的一次是60多,最大的1000多,并发问题还是比较明显的。下面是其中一次测试结果:

      有些人倾向于使用EventHandler handler = instance.MyEvent;代替使用Interlocked.CompareExchange方法,书中也提到了,这种方式也是可行的,因为MS的JIT编译器不会将这里的handler优化掉。书中最后说道“另外由于事件主要在单线程的情形中使用(WinForm/WPF/SilverLight),所以线程安全并不是一个问题。”

      我认为,这个问题还是有必要注意一下的。这种问题一般都很难重现,而且还是该死的NullReferenceException异常,一看上下文代码,霎时间还真是“莫名其妙”,最后归于人品问题倒是相当无奈了。

    ===============================================================

    今天发现代码中有误,Interlocked.Exchange会交换两引用,应该使用Interlocked.CompareExchange方法。(上面代码已修正)

  • 相关阅读:
    HTML 块标签的学习(样式/节)
    HTML 链接标签的学习
    HTML 列表标签的学习
    (译)快速指南:用UIViewPropertyAnimator做动画
    RunLoop 总结:RunLoop的应用场景(二)
    RunLoop 总结:RunLoop的应用场景(一)
    Android Studio精彩案例(二)《仿微信动态点击底部tab切换Fragment》
    Android Studio精彩案例(一)《ActionBar和 ViewPager版仿网易新闻客户端》
    Android简易实战教程--第四十五话《几种对话框》
    Android中Sqlite数据库进行增删改查
  • 原文地址:https://www.cnblogs.com/neutra/p/1849721.html
Copyright © 2011-2022 走看看