• C#中async的死锁分析和解决方案


    死锁示例

    如果你开发一个简单的Windows Form程序,点击Button去使用async异步获取一个数据,然后显示在Label上,类似这样的代码

    private void button1_Click(object sender, EventArgs e)
    {
        var task = GetContentAsync();
        var content = task.Result;
    
        this.label1.Text = content;
    }
    
    
    public async Task<string> GetContentAsync()
    {
        var http = new HttpClient();
        var result  = await http.GetStringAsync("http://www.imzjy.com");
    
        var first50 = result.Substring(0, 50);
        return first50;
    }
    

    当你点击Button的时候会发现程序直接卡死了。

    死锁原因分析

    C#中的async/await隐藏了很多的细节,一个简单的await其实让函数发生了一次重入,重入对于多线程代码来说其实很正常。但是C#将这些藏了起来。你看上去像一个函数,其实被分成了两段,而且执行这两段代码的线程还可能不一样。

    上面代码真正的执行过程是这样的:

    private void button1_Click(object sender, EventArgs e)
    {
        //1. calling GetContentAsync
        var task = GetContentAsync();
        Debug.WriteLine($"Continuation:{Environment.CurrentManagedThreadId}");
    
        //4. .Result(or GetAwait().GetResult()) which waiting for GetContentAsync to complete. 
        //OOPS:   DEADLOCK!!!
        //REASON: task.Result waiting the http.GetStringAsync complete and return;
        //REASON: GetContentAsync wait button1_Click release the synchronization context;
        var content = task.Result;
    
        this.label1.Text = content;
    }
    
    
    public async Task<string> GetContentAsync()
    {
        var http = new HttpClient();
        //2. automatic capture synchronization context   :: auto capture caused the issue.
        //3. due to await applied, yield thread to caller(button1_Click)
        var result  = await http.GetStringAsync("http://www.imzjy.com");
        
        //WHY AUTO CAPTURE? capture the synchronization context makes following accessing UI control became possible.
        //textBox1.Text = first50;    
    
        var first50 = result.Substring(0, 50);
        return first50;
    }
    

    对于GetContentAsync函数来说,在await之前其实是同步的代码,当await之后,线程直接返回给button1_Clickawait时候发生了两件事:

    • 在返回之前偷偷做了个动作,那就是将当前线程的同步上下文(SychronizationContext)给捕获了。
    • 返回了一个未完成的任务,这里面抽象为Task

    然后在第4步,当button1_Click中去获取上面这个Task返回值的时候出现了死锁,button1_ClickGetContentAsync相互等待:

    1. var content = task.Result; button1_Click等待任务await http.GetStringAsync("http://www.imzjy.com");完成
    2. await http.GetStringAsync("http://www.imzjy.com");等待当前线程(UI线程)的同步上下文SychronizationContext

    由于上面两个方法相互等待,所以产生了死锁。

    为什么自动捕获当前线程同步上下文

    GetContentAsync自动捕获的是当前UI线程的同步上下文,通过偷偷的捕获当前UI线程的同步上下文可以让你在GetContentAsync方法中await之后可以更新UI控件。如果你在GetContentAsync中不需要更新UI控件,那么我们就不必捕获同步上下文,那么也就不存在这个问题。

    解决方案 1

    修改GetContentAsync,让http.GetStringAsync("http://www.imzjy.com”);自动捕获上下文时候捕获不到。破坏了上面的死锁条件2。

    private void button1_Click(object sender, EventArgs e)
    {
        var task = GetContentAsync();
        var content = task.Result;
    
        this.label1.Text = content;
    }
    
    
    public async Task<string> GetContentAsync()
    {
        var syncContext = WindowsFormsSynchronizationContext.Current;  //save SynchronizationContext
        WindowsFormsSynchronizationContext.SetSynchronizationContext(null); //set SynchronizationContext to null
    
        var http = new HttpClient();
        var result  = await http.GetStringAsync("http://www.imzjy.com");
        
        WindowsFormsSynchronizationContext.SetSynchronizationContext(syncContext); //restore the SynchronizationContext
    
        var first50 = result.Substring(0, 50);
        return first50;
    }
    

    优点:

    1. 调用方代码不需要改变

    缺点:

    1. 调用者线程(UI线程)会在var content = task.Result;阻塞,直到GetContentAsync返回,导致界面在此期间无响应。
    2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI线程)会出现问题
    3. 改的代码比较多3行。
    4. WindowsFormsSynchronizationContext.SetSynchronizationContext(null);可能有副作用。

    解决方案 2

    修改调用方式,将调用放到Thread pool中,这样await http.GetStringAsync("http://www.imzjy.com");的auto capture就不会获取到当前UI线程的SynchronizationContext,破坏了上面的死锁条件2。

    private void button1_Click(object sender, EventArgs e)
    {
        //put the GetContentAsync into thread pool, so that 
        //http.GetStringAsync("http://www.imzjy.com"); 
        //will capture the SynchronizationContext from thread pool's excection environemnt
        var task = Task<string>.Run(GetContentAsync); 
    
    
        var content = task.Result;
        this.label1.Text = content;
    }
    
    
    public async Task<string> GetContentAsync()
    {
        var http = new HttpClient();
        var result  = await http.GetStringAsync("http://www.imzjy.com");
        
        var first50 = result.Substring(0, 50);
        return first50;
    }
    

    优点:

    1. async方法不需要改变。

    缺点:

    1. 调用者线程(UI线程)会在var content = task.Result;阻塞,直到GetContentAsync返回,导致界面在此期间无响应。
    2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI线程)会出现问题

    解决方案 3

    通过ConfigureAwait来改变自动捕获SynchronizationContext行为,破坏了上面的死锁条件2。

    private void button1_Click(object sender, EventArgs e)
    {
        var task = GetContentAsync();
    
        var content = task.Result;
        this.label1.Text = content;
    }
    
    
    public async Task<string> GetContentAsync()
    {
        var http = new HttpClient();
        //tell await not to capture SynchronizationContext
        var result  = await http.GetStringAsync("http://www.imzjy.com”)
    .ConfigureAwait(continueOnCapturedContext: false); var first50 = result.Substring(0, 50); return first50; }

    优点:

    1. 调用方(caller)不需要改变
    2. 避免了此处无用的自动捕获线程上下文。

    缺点:

    1. 调用者线程(UI线程)会在var content = task.Result;阻塞,直到GetContentAsync返回,导致界面在此期间无响应。
    2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI线程)会出现问题

    解决方案 4

    把当前的事件处理函数也改成async的,这样破坏了死锁条件的1。button1_Click不在死等,所以也释放了上下文。

    private async void button1_Click(object sender, EventArgs e)
    {
        var task = GetContentAsync();
        var content = await task;
    
        this.label1.Text = content;
    }
    
    public async Task<string> GetContentAsync()
    {
        var http = new HttpClient();
        var result  = await http.GetStringAsync("http://www.imzjy.com");
    
        var first50 = result.Substring(0, 50);
        textBox1.Text = first50;
    
        return first50;
    }
    

    优点:

    1. async方法不需要改变。
    2. 避免了UI无响应的问题。
    3. GetContentAsyncawait之后可以更新UI界面。

    缺点:

    1. button1_Click改为了异步,对原来的方法有侵入性,甚至会改变整个调用链的行为,我最讨厌这点了。

    适用

    上面的死锁通常会发生在下面两个地方

    1. Windows Forms的UI线程中调用了异步的方法。
    2. ASP.NET的User Request Context执行环境,比如Controller中的方法。 代码细节

    经验

    异步方法实现者

    1. 分开提供同步和异步方法
    2. 只是自己做一些事,不需要bind到调用线程上的需要尽量.ConfigAwait(continueOnCapturedContext:false)

    对于异步方法使用者

    1. 看看是否提供了同步方法
    2. 考虑是否有机会将自己的代码转为异步代码
    3. 实在不行放到threadpool中去执行

    async/await中的异常处理

    如果加上异常处理,那么async/await会变得更加复杂,因为异步方法在异步执行,所以可以放到不同的线程中,那么如果出现了异常会怎么样?简单来说:

    1. 异步代码中的异常如果存在TaskTask<T>被attach到了Task对象上
    2. 但是async void例外,由于没有Task对象可以attach,所以attach到了SynchronizationContext中活跃的线程中了。
    3. 异步方法调用(调用链)中的异常,会被Aggregate,然后生成一个AggregateException。你可以使用aggExp.Flatten()方法来方便查看这个调用链中所有异常--如果有多个的话。

    完整代码示例

    AsyncLockAndFixes

    首发:https://www.imzjy.com/blog/dotnet-async-locks-and-solutions

  • 相关阅读:
    TestNG教程网站
    BITE
    软件测试理论
    Java 学习笔记 (八) Java 变量
    封装
    把封装脚本做成jar包
    表现层(jsp)、持久层(类似dao)、业务层(逻辑层、service层)、模型(javabean)、控制层(action)
    IOException parsing XML document from class path resource [WebRoot/WEB-INF/applicationContext.xml];
    java 里面耦合和解耦
    JAVA中Action层, Service层 ,modle层 和 Dao层的功能区分
  • 原文地址:https://www.cnblogs.com/Jerry-Chou/p/dotnet-async-locks-and-solutions.html
Copyright © 2020-2023  润新知