• 异步陷阱之死锁篇


    提倡异步编程旨在给用户更好的前端体验,但异步编程也让学习成本和犯错几率大大升高,其中最常见且最难处理的就是死锁。

    何谓“死锁”,英文术语称“Deadlock”,当两个以上的运算单元,双方都在等待对方停止运行,以取得系统资源,但是没有一方提前退出时,这种状况,就称为死锁。​

    举个例子吧,这里是一段经典的死锁示例代码:

    int sharedResource1 = 1, sharedResource2 = 2;
    var lockResource1 = newobject();
    var lockResource2 = newobject();
    
    var t1 = newThread(() =>
    {
        Console.WriteLine("thead 1 begin");
    
        lock (lockResource1)
        {
            Thread.Sleep(10);
    
            lock (lockResource2)
            {
                sharedResource1++;
                sharedResource2++;
            }
        }
        Console.WriteLine("thead 1 end");
    });
    
    var t2 = newThread(() =>
    {
        Console.WriteLine("thead 2 begin");
    
        lock (lockResource2)
        {
            Thread.Sleep(10);​
    
            lock (lockResource1)
            {
                sharedResource1++;
                sharedResource2++;
            }
        }
    
        Console.WriteLine("thead 2 end");
    });
    
    t1.Start();
    t2.Start();

    运行结果如下,永远也不会看到“thread x end”:
    Deadlock-Run
    这是一个不同次序请求加锁导致死锁,归功于我们的教材对此类死锁的解释非常详细,这里我一笔带过,接下来看看日常开发中经常遇到的一些更具体的死锁情况——线程死锁。

    场景1—Task之间互相等待导致死锁:

    Task t1 = null, t2 = null;
    t1 = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("task 1 begin");
        Task.Delay(10);
        Task.WaitAll(t2);
        Console.WriteLine("task 1 end");
    });
    
    t2 = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("task 2 begin");
        Task.Delay(10);
        Task.WaitAll(t1);
        Console.WriteLine("task 2 end");
    });
    
    Task.WaitAll(t1, t2);
    Console.WriteLine("Done");

    场景2—​WinForm Invoke抢夺UI线程死锁:

    privatevoid button1_Click(object sender, EventArgs e)
    {
        var t = Task.Factory.StartNew<string>(() =>
            {
    
                Thread.Sleep(0);
                var text = Invoke(newFunc<string>(() =>
    
                {
    
                    // do some ui-dependent works
                    return Text;
    
                }));​
    
                return text + " - new title";
    
            });
        Text = t.Result;
    }

    场景3—WPF Dispatcher切换死锁

    privatevoid Button_Click(object sender, RoutedEventArgs e)
    {
        var t = Task.Factory.StartNew<Brush>((state) =>
        {
            Task.Delay(10);
            var clr = (Color)newColorConverter()
                .ConvertFromInvariantString(state asstring);
            var brush = Dispatcher.Invoke<SolidColorBrush>(() =>
                {
                    // do some works
                    returnnewSolidColorBrush() { Color = clr };
                });
            return brush;
        }, "red");
        theButton.Background = t.Result;
    }

    这里将各种无关代码精简筛除,基本上很快就可以发现这些情况中的问题,是的,实际上以上几种场景均是同一个原因——wait线程锁:主执行线程调用子线程后挂起等待子线程结果,子线程又需要切换到主线程或者等待主线程返回,从而导致两个线程均处在阻塞状态(死锁),如下图所示:

     deadlock

    解决方案很简单,去除所有的同步等待,至少确保在主线程上一定不要使用同步等待,如何操作呢?你可以到多种选择,这里我提几点,抛砖引玉,希望大家可以在实际应用中或者更多灵感和解决方法。

    1、去除所有wait,使用async和await关键字重写,推荐使用。
    这里或许你会有些迷惑,为什么async和await就能保证不会线程死锁呢?​如下图示意代码片段,当前线程执行完(1)之后,接着执行(2),注意这里执行(2)会切换线程,但是不是阻塞当前线程,.NET在这里耍了个“花招”,实际编译器发现async和await关键字的时候会自动插入一些代码,利用状态机在(3)的位置做了个标记,让当前线程“飞”了一会,等到await所处的子线程结束的时候,修改状态机状态,让当前线程恢复到(3)这里,接着就可以跑(4),从开发者的角度来看,好像这一段代码是顺序执行的。重要的是,这里没有wait锁。

    async-await

    2、去除所有wait,使用Task.ContinueWith来实现代码顺序。
    var ta = new Task(()=>{ doSome(); });
    ta.ContinueWith((tc)=>{ doAnother(tc.Result); });

    3、去除所有wait,将wait之后的代码移到单独的调用中,使用事件或者回调函数的方式,在子线程结束的时候,激活主线程。以WinForm为例,如下图所示:
    winform%20async

    附上文中所提到测试的代码工程:下载地址

  • 相关阅读:
    php笔记之数组
    php笔记之函数
    vue笔记之路由
    <QT>:使用QUdpSocket进行UDP通信
    Linux下用ssh在远程Xserver上显示图形界面
    <学习QT>在QListWidget中实现每个单元项显示文字在上图片在下
    const限定符以及顶层const和底层const的理解
    了解X window以及相关知识
    Linux下使用Eclipse C/C++生成创建并调用动态库(.so)
    Linux下搭载Xwindow(Xlib)开发环境
  • 原文地址:https://www.cnblogs.com/powertoolsteam/p/SpreadJs_Deadlock.html
Copyright © 2020-2023  润新知