在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁

栏目: ASP.NET · 发布时间: 6年前

内容简介:本文将说一个在同步上下文中非常常见的一种用法,换成异步上下文中会产生死锁的问题。先看看一段非常简单的代码:

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 之后,所以不会执行,于是死锁。

更多死锁问题

死锁问题:


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

互联网+秋叶课堂:我的网课创业

互联网+秋叶课堂:我的网课创业

秋叶 / 北京:机械工业出版社 / 2016-3-1 / 39.0

在线教育被很多人视为 “互联网+”创业热潮中的下一个风口,越来越多的老师和创业者选择在线教育创业。本书作者秋叶老师2013年双11正式带一帮小伙伴开始在线教育创业,在短短两年内“从0到1”,累计做到了超500万元的销售业绩,成为国内Office领域在线教育运营最成功的团队之一。在这本书中秋叶老师结合自己的实战经历,向各位完整剖析了两年创业的真实复盘思考,是一本值得在线教育创业同行入手的必读书。 ......一起来看看 《互联网+秋叶课堂:我的网课创业》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具