C# 我是如何理解async await的

本文详细介绍了C#中async/await的关键字,阐述了其设计初衷是为了简化Task的连续操作,提高代码可读性。通过示例代码展示了如何使用async/await替代ContinueWith,使异步代码更加简洁。同时,文章还探讨了可能出现的死锁问题及其原因,并提到了在特定场景下如何避免死锁,强调在使用async/await时要考虑任务调用链和上下文恢复可能带来的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

写这篇文章的原因是网上关于C# async关键字和await关键字的文章很多,但是在我看来写的有些绕了,其实这俩关键字很容易理解,需要注意的是:这和ES7中的async、await有很大区别,如果你理所当然的用ES7中的理解,那么你已经走歪了。

理解设计初衷

async/await关键字设计的初衷

是为了让Task.ContinueWith之后,或嵌套Task之后,return新的Task的代码看起来更简洁
并不是为了单个Task的情况下省去Start/Run或task.Result的调用

下面写了2个小例子,非常简单的代码,当你理解这两个关键字出现的原因后,那么你就更容易理解为什么使用,在什么时候使用。

我们先看下面的代码

using System;
using System.Threading.Tasks;
namespace Test
{
    class Program
    {
        static Task<long> SquareTask(long i)
        {

            Task<long> task = Task.Run(() => {
                return i * i;
            });

            return task;
        }

        static Task<long> FourthPowerTask(long i)
        {
            Task<long> task = SquareTask(i);

            Task<long> continueTask = task.ContinueWith((preTask) =>
            {
                //假设我们保存之前计算的平方(preTask.Result) 进数据库(模拟会用到之前task的返回值)
                Console.WriteLine("存入数据库 {0}", preTask.Result);

                //此时我们返回再计算一次的平方
                return preTask.Result * preTask.Result;
            });

            return continueTask;
        }

        static void Main(string[] args)
        {
            Task<long> task = FourthPowerTask(10);
            Console.WriteLine("4次方结果 {0}", task.Result);
        }
    }
}

代码中FourthPowerTask方法创建了一个Task,并且让它先将平方值存入数据库,然后继续计算之前Task结果的平方,并继续返回新的Task

注意:代码中我们只嵌套了一层ContinueWith,如果Task里面嵌套Task变得越来越多,代码的可读性就变得更差。

使用async await让代码看起来简洁

  • await:表示等待一个异步方法的结果,但必须和async同时出现。
  • async:表示当前方法是异步方法,但必须和await同时出现。
  • 方法内await在Task对象之前,表示需要获取Task.Result,此时当前方法必须使用async关键字。

下面的代码使用只是为了让ContinueWith后返回新的Task的代码,变的简洁(设计初衷)。

在下面的代码中你会发现,我们之前看似通过一层一层回调处理的代码变成了最原始的,函数式代码。

using System;
using System.Threading.Tasks;
using System.Threading;
namespace Test
{
    class Program
    {
        static Task<long> SquareTask(long i)
        {
            Task<long> task = Task.Run(() => {
                return i * i;
            });

            return task;
        }

        static async Task<long> FourthPowerTask(long i)
        {
            var result = await SquareTask(i);

            //假设我们保存之前计算的平方 进数据库(模拟会用到之前task的返回值)
            Console.WriteLine("存入数据库 {0}", result);

            //此时我们返回再计算一次的平方
            //async关键字会将返回值封装为Task
            return result * result;
        }

        static void Main(string[] args)
        {
            Task<long> task = FourthPowerTask(10);
            Console.WriteLine("4次方结果 {0}", task.Result);
        }
    }
}

死锁可能性

探究死锁的原因我们先要分清楚线程是怎么处理asyn和await的。
我们将上面的代码改一改,看看线程ID

using System;
using System.Threading.Tasks;
using System.Threading;
namespace Test
{
    class Program
    {
        static Task<long> SquareTask(long i)
        {
            Console.WriteLine("SquareTask 进入线程:{0}", Thread.CurrentThread.ManagedThreadId);
            Task<long> task = Task.Run(() => {
                Console.WriteLine("SquareTask 执行线程:{0}", Thread.CurrentThread.ManagedThreadId);
                return i * i;
            });
            Console.WriteLine("SquareTask 返回线程:{0}", Thread.CurrentThread.ManagedThreadId);
            return task;
        }

        static async Task<long> FourthPowerTask(long i)
        {
            Console.WriteLine("FourthPowerTask 进入线程:{0}", Thread.CurrentThread.ManagedThreadId);
            var result = await SquareTask(i);

            
            //假设我们保存之前计算的平方 进数据库(模拟会用到之前task的返回值)
            Console.WriteLine("存入数据库 {0}", result);

            //此时我们返回再计算一次的平方
            //async关键字会将返回值封装为Task
            Console.WriteLine("FourthPowerTask 返回线程:{0}", Thread.CurrentThread.ManagedThreadId);
            return result * result;
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Main 执行线程:{0}", Thread.CurrentThread.ManagedThreadId);
            Task<long> task = FourthPowerTask(10);
            Console.WriteLine("4次方结果 {0}", task.Result);
        }
    }
}

输出

Main 执行线程:1
FourthPowerTask 进入线程:1
SquareTask 进入线程:1
SquareTask 返回线程:1
SquareTask 执行线程:4
存入数据库 100
FourthPowerTask 返回线程:4
4次方结果 10000
  • 发现在await之前的代码,都是在(调用者线程)主线程执行
  • await关键字之后的代码,是在另外一个线程执行的,这和ContinueWith是不是很像?
  • 但是有例外,看下面。

需要注意的是,在WPF/WinForm程序或asp.net中,某些情况下await之后的代码还是在主线程执行,它会还原context调用者的context,也就是说如果在await之后,你调用了WaitResult阻塞,调用者那边也在等Result,此时就会产生死锁,我们可以调用task.ConfigureAwait(false),让await之后的代码不要恢复到调用者的context执行。

死锁主要发生在

1、WPF/WinForm程序
2、asp.net (不包括asp.net core)程序

写在最后

当你确定使用async/await时,应该考虑的是否会形成一个Task的调用链(当前方法需要使用之前Task的值,并且要返回新的Task),而不是完全不思考的情况下使用这两个关键字。

代码是写给人看的,给机器看那是编译器和解释器的事,在你足够了解底层,对编程有自己的理解后,你写的代码才能兼顾性能和可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没事干写博客玩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值