zoukankan      html  css  js  c++  java
  • 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁

    AutoResetEventManualResetEventMonitorlock 等等这些用来做同步的类,如果在异步上下文(await)中使用,需要非常谨慎。

    本文将说一个在同步上下文中非常常见的一种用法,换成异步上下文中会产生死锁的问题。


    一段正常的同步上下文的代码

    先看看一段非常简单的代码:

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        ThreadPool.SetMinThreads(100, 100);
    
        // 全部在后台线程,不会死锁。
        for (var i = 0; i < 100; i++)
        {
            Task.Run(() => Do());
        }
    
        // 主线程执行与后台线程并发竞争,也不会死锁。
        for (var i = 0; i < 100; i++)
        {
            Do();
        }
    }
    
    private void Do()
    {
        _resetEvent.WaitOne();
    
        try
        {
            // 这个 ++ 在安全的线程上下文中,所以不需要使用 Interlocked.Increment(ref _count);
            _count++;
            DoCore();
        }
        finally
        {
            _resetEvent.Set();
        }
    }
    
    private void DoCore()
    {
        Console.WriteLine($"[{_count.ToString().PadLeft(3, ' ')}] walterlv is a 逗比");
    }
    

    以上代码运行会输出 200 个 “walterlv is a 逗比”:

    [  1] walterlv is a 逗比
    [  2] walterlv is a 逗比
    [  3] walterlv is a 逗比
    [  4] walterlv is a 逗比
    [  5] walterlv is a 逗比
    [  6] walterlv is a 逗比
    [  7] walterlv is a 逗比
    [  8] walterlv is a 逗比
    [  9] walterlv is a 逗比
    [ 10] walterlv is a 逗比
    // 有 200 个,但是不需要再在这里占用行数了。[197] walterlv is a 逗比
    [200] walterlv is a 逗比
    

    以上代码最关键的使用锁进行同步的地方是 Do 函数,采用了非常典型的防止方法重入的措施:

    // 获得锁
    try
    {
        // 执行某个需要线程安全的操作。
    }
    finally
    {
        // 释放锁
    }
    

    我们设置了线程池最小线程数为 100,这样在使用 Task.Run 进行并发的时候,一次能够开启 100 个线程来执行 Do 方法。同时 UI 线程也执行 100 次,与后台线程竞争输出。

    一个微调即会死锁

    现在我们微调一下刚刚的代码:

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        ThreadPool.SetMinThreads(100, 100);
    
        // 全部在后台线程,不会死锁。
        for (var i = 0; i < 100; i++)
        {
            Task.Run(() => DoAsync());
        }
    
        // 主线程执行与后台线程并发竞争,也不会死锁。
        for (var i = 0; i < 100; i++)
        {
            DoAsync();
        }
    }
    
    private async Task DoAsync()
    {
        _resetEvent.WaitOne();
    
        try
        {
            _count++;
            await DoCoreAsync();
        }
        finally
        {
            _resetEvent.Set();
        }
    }
    
    private async Task DoCoreAsync()
    {
        await Task.Run(async () =>
        {
            Console.WriteLine($"[{_count.ToString().PadLeft(3, ' ')}] walterlv is a 逗比");
        });
    }
    

    为了直观看出差别,我只贴出不同之处:

            {
    --          Task.Run(() => Do());
    ++          Task.Run(() => DoAsync());
            }
        ...
            {
    --          Do();
    ++          DoAsync();
            }
    
    --  private void Do()
    ++  private async Task DoAsync()
        {
        ...
                _count++;
    --          await DoCore();
    ++          await DoCoreAsync();
            }
        ...
        }
    
    --  private void DoCore()
    ++  private async Task DoCoreAsync()
        {
    --      Console.WriteLine($"[{_count.ToString().PadLeft(3, ' ')}] walterlv is a 逗比");
    ++      await Task.Run(async () =>
    ++      {
    ++          Console.WriteLine($"[{_count.ToString().PadLeft(3, ' ')}] walterlv is a 逗比");
    ++      });
        }
    

    现在再运行代码,只输出几次程序就停下来了:

    [  0] walterlv is a 逗比
    [  1] walterlv is a 逗比
    [  2] walterlv is a 逗比
    [  3] walterlv is a 逗比
    [  4] walterlv is a 逗比
    [  5] walterlv is a 逗比
    

    每次运行时,停下来的次数都不相同,这也正符合多线程坑的特点。

    此死锁的触发条件

    实际上,以上这段代码如果没有 WPF / UWP 的 UI 线程的参与,是 不会出现死锁 的。

    但是,如果有 UI 线程参与,即便只有 UI 线程调用,也会直接死锁。例如:

    DoAsync();
    DoAsync();
    

    只是这样的调用,你会看到值输出一次 —— 这就已经死锁了!

    此死锁的原因

    WPF / UWP 等 UI 线程会使用 DispatcherSynchronizationContext 作为线程同步上下文,我在 出让执行权:Task.Yield, Dispatcher.Yield - walterlv 一问中有说到它的原理。

    await 等待完成之后,会调用 BeginInvoke 回到 UI 线程。然而,此时 UI 线程正卡死在 _resetEvent.WaitOne();,于是根本没有办法执行 BeginInvoke 中的操作,也就是 await 之后的代码。然而释放锁的代码 _resetEvent.Set(); 就在 await 之后,所以不会执行,于是死锁。

    更多死锁问题

    死锁问题:

    解决方法:

  • 相关阅读:
    javascript的严格模式:use strict
    Ionic在线打包IOS平台应用
    安装nodejs6.9x以后,原来在nodejs4.2.x中运行正常的ionic项目出现问题的解决
    cordova插件分类
    ionic 启用sass
    ngCordova
    为Asp.net WebApi 添加跨域支持
    使用ionic framework创建一个简单的APP
    研究主题
    近两天让我羞愧难当的遭遇
  • 原文地址:https://www.cnblogs.com/walterlv/p/10236388.html
Copyright © 2011-2022 走看看