浅谈async、await关键字 => 深谈async、await关键字

本文详细探讨了C#中async和await关键字的使用方法及原理,包括如何正确使用这两个关键字来实现异步编程,以及它们背后的编译器行为。文章还讨论了异步方法的条件限制、lambda表达式中的异步应用等高级主题。

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

前言

之前写过有关异步的文章,对这方面一直比较弱,感觉还是不太理解,于是会花点时间去好好学习这一块,我们由浅入深,文中若有叙述不稳妥之处,还请批评指正。

话题

(1)是不是将方法用async关键字标识就是异步方法了呢?

(2)是不是没有await关键字的存在async就没有存在的意义了呢?

(3)用异步方法的条件是什么呢,为什么会有这个条件限制?

(4)只能调用.NET Framework内置的用await标识的Task,能否自定义实现呢?

(5)在lambda表达式中是否可以用async和await关键字来实现异步呢(即异步lambda表达式)?

上述抛出这几个话题,明白本文主要讲述的话题以及需要深入了解的知识。

注意:这里我将参照园友【反骨仔的文章进行进一步解析

async关键字

例如异步方法是这样的:

        public static async Task<int> asyncMethod()
        {
            return await Task.Run(() => Calculate());
        }

        static int Calculate()
        {
            return 1 + 1;
        }

那要是如下这样写呢?

        public static async Task<int> asyncMethod()
        {
            var task = Task.Run(() => Calculate());
            return task.Result;
        }

那上述这种写法是不是也是异步方法呢?答案是【NO】,既然不是异步方法为什么要用async关键字来进行标识呢?不是很容易被我们所误解呢?好了疑问这么多我们一一来解惑。

当方法用async标识时,编译器主要做了什么呢?

(1)告诉编译器这个方法里面可能会用到await关键字来标识该方法是异步的,如此之后,编译器将会在状态机中编译此方法。接着该方法执行到await关键字时会处于挂起的状态直到该异步动作完成后才恢复继续执行方法后面的动作。

(2)告诉编译器解析出方法的结果到返回类型中,比如说Task或者Task<TResult>,也就是说将返回值存储到Task中,如果返回值为void那么此时应该会将可能出现的异常存储到上下文中。

当方法用async标识时,是不是所有调用者都将是异步的呢?

当将方法用async标识时且返回值为void或者Task或者Task<TReuslt>,此时该方法会在当前线程中一直同步执行。用async标识方法并不会影响方法运行完成是否是同步或者异步,相反,它能够将方法划分成多块,有可能有些在异步中运行,以至于这些方法是异步完成的,而划分异步和同步方法的边界就是使用await关键字。也就是说如果在方法中未用到await关键字时则该方法就是一整块没有所谓的划分,会在同步中运行,在同步中完成。

当方法用async标识时,是否会引起方法的调用会被添加到线程池队列中或者是创建一个新的线程呢?

显然不是这样,当用async标识方法时只是显示告诉编译器在该方法中await关键字可能会被用到,当执行到await关键字开始处于挂起的状态知道异步动作执行完成才恢复(异步操作是在状态机中【有关状态机请看这里:Async和Await异步编程的原理】完成,完成后此时才会创建一个线程),这也就是为什么在方法中方法用async标识如果没有用到await关键字IDE会发出警告的原因。

—————————————————————————————————————————————————————————————————

到了这里我们可以得出结论:无论方法是同步还是异步都可以用async关键字来进行标识,因为用async标识只是显示表明在该方法内可能会用到await关键字使其变为异步方法,而且将该异步方法进行了明确的划分,只有用了await关键字时才是异步操作,其余一并为同步操作。

参数为什么不能使用ref和out关键字

返回类型必须为void或者Task或者Task<TResult>和关键字的标识以及其他就不再叙述,其中有一条是不能使用ref和out关键字,你是背书似的铭记了这一条,还是略加思索了呢?你想过没有为何不可呢?

我们知道用ref和out关键字不过是为了在方法里面改变其值,也就是是当同步完成时我们期望被ref或者out关键字修饰的值会被设置,但是它们可能在异步完成时或者之后才会被设置达不到我们预期,所以在异步方法中不能用ref和out关键字。

lambda表达式是否可以异步呢?

返回类型Task参数可以为lambda表达式或者匿名方法对象,那直接对lambda表达式异步是否可行?下面我们来看看

        public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, T2> func)
        {
            return func.Invoke(t);
        }

        public static async Task<string> GetStringAsync(int value)
        {
            return await Task.Run(() => "xpy0928");
        }

        public static async Task MainAsync()
        {
            string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s));
        }

编译后生成如下错误:

由上知异步lambda表达式是不行的,猜测是异步lambda表达式不能转换为表达式树,同时我们看看上述代码,CallFunAsync此时并未是异步方法,上述我们已经叙述过,此时是同步运行,既然上述错误,并且代码也有不可取之处我们接下来一一进行修改。

string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s));

修改为:

string value = await CallFuncAsync<int, string>(30, s => GetStringAsync(s).Result);

解决了编译错误,但是未解决CallFuncAsync为异步运行,我们将其修改为异步运行。既然await是针对于Task而操作,我们将CallFuncAsync中的返回参数设置为Task即可。

        public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, Task<T2>> func)
        {
            return await func.Invoke(t);
        }

则最终调用时我们直接调用即可。

   string value = await CallFuncAsync(30, GetStringAsync);

此时CallFuncAsync才算是异步运行。

补充(2016-10-21 23:11)

对于异步表达式有一点其实表述不太正确,其实我一直还是有点怀疑异步lambda表达式真的不行吗,此刻我居然发现这样是可以的:

            var task = Task.Run(async () =>
            {
                await Task.Delay(TimeSpan.FromMilliseconds(5000));
            });

如上不正是异步表达式的影子吗,于是我将上述代码进行了改写,如下:

        public static async Task<Action> CallFuncAsync(Action action)
        {
            return action;
        }

        public static async Task<Action> GetStringAsync()
        {
            return () => Console.WriteLine("xpy0928");
        }

        public static async Task MainAsync()
        {
            await CallFuncAsync(async () => await GetStringAsync());
        }

此时编译通过,说明表述异步表达式并非一定不可以,只是对于无参数的lambda表达式才可以,而对于有参数的lambda表达式如fun则不能执行异步lambda表达式。

至此可以基本下结论:

异步lambda表达式只对于无参数的lambda表达式 才可以(当然这也就没有了什么意义),而对于有参数的lambda表达式则产生编译错误则不能执行异步(猜测是无法转换成对应的表达式树)。(不知是否严谨或者不妥,若有错误之处,还望对此理解的更透彻的园友给出批评性意见)。

为了验证这一点,我们来看看无参数的func委托例子,如下:

        static async Task<string> GetTaskAsync()
        {
            await Task.Delay(TimeSpan.FromMilliseconds(5000));
            return "xpy0928";
        }
var task = Task.Run(async () => await GetTaskAsync());

此时无参数的func委托则编译通过,应该是验证了上述观点(还是有点怀疑我所下的结论)。

await关键字

await关键字是这样用的

await Task.Run(() => "xpy0928");

此时背后究竟发生了什么呢?我们上述也说过异步动作时在状态机中完成,当执行到这里时,编译器会自动生成代码来检测该动作是否已经完成,如果已经完成则继续同步执行await关键字后面的代码,通过判断其状态机状态若未完成则会挂起一个继续的委托为await关键字的对象直到完成为止,调用这个继续动作的委托重新进入未完成的这样一个方法。

比如说: await someObject; 编译器则会生成如下代码:

private class FooAsyncStateMachine : IAsyncStateMachine 
{ 
    // Member fields for preserving “locals” and other necessary state 
    int $state; 
    TaskAwaiter $awaiter; 
    … 
    public void MoveNext() 
    { 
        // Jump table to get back to the right statement upon resumption 
        switch (this.$state) 
        { 
            … 
            case 2: goto Label2; 
            … 
        } 
        … 
        // Expansion of “await someObject;” 
        this.$awaiter = someObject.GetAwaiter(); 
        if (!this.$awaiter.IsCompleted) 
        { 
            this.$state = 2; 
            this.$awaiter.OnCompleted(MoveNext); 
            return; 
            Label2: 
        } 
        this.$awaiter.GetResult(); 
        … 
    } 
}

此时讲到这里就要涉及到await背后具体的实现,在Task或者Task<TResult>类里面有这样一个返回类型为 TaskAwaiter 的 GetAwaiter 属性,而TaskAwaiter中有如下属性:

    public struct TaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
    {

        public bool IsCompleted { get; }
        public void GetResult();
        public void OnCompleted(Action continuation);
        public void UnsafeOnCompleted(Action continuation);
    }

通过IsComplete来判断是否已经完成。这个有什么作用呢?通过看到背后具体实现,我们可以自己简单实现异步扩展方法,当我们在Task中查看其方法会有这样的提示:

下面我们就来实现这样的效果,给TimeSpan添加异步方法:

    public static class Extend
    {
        public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
        {

            return Task.Delay(timeSpan).GetAwaiter();
        }
    }

此时异步方法则是这样的:

总结 

本节我们详细讲述了async和await关键字的使用和一些基本原理以及解释其原因,希望通过对本文的学习,对大家能够更好的去理解异步,我也在学习中,Over。

<think>好的,我现在需要帮助用户了解C#中asyncawait关键字的用法,并提供示例。首先,我要回顾一下用户提供的引用内容,确保信息准确。根据引用[1],当使用await时,挂起的是直接包含await的异步方法,而不是外部方法。引用[2]提到asyncawait必须搭配使用,await后面接的是正在运行的Task。引用[3]指出asyncawait让异步代码更易读和维护。引用[4]中的示例展示了异步方法的执行流程。 首先,我需要解释asyncawait的基本概念。用户可能想知道为什么需要这两个关键字,以及它们如何简化异步编程。根据引用[3],异步编程传统上使用回调、事件等,但async/await让代码看起来像同步,更容易理解。所以应该先介绍这一点。 接下来是使用方法。需要说明如何声明异步方法,即用async修饰符,并返回Task或Task<T>。根据引用[2],async Task<TResult>是常见的返回类型。这里要强调方法签名中的async和返回类型的重要性。 然后,解释await的作用。根据引用[1],当遇到await时,当前异步方法会被挂起,控制权返回给调用者,直到等待的任务完成。这意味着不会阻塞线程,这在UI或服务应用中很重要。需要举例说明,比如在UI线程中等待网络请求时,不会冻结界面。 示例部分,用户需要看到具体的代码。引用[4]中的示例展示了多个步骤和await的使用。可以创建一个简单的示例,比如模拟下载数据,使用HttpClient或者Task.Delay来演示异步操作。例如,一个DownloadDataAsync方法,内部使用await等待网络请求,然后在按钮点击事件中调用它,并同样使用await,确保UI响应。 还要注意异常处理和配置任务,比如ConfigureAwait(false)的作用,避免死锁。这部分可能在高级用法中提到,但用户可能也需要知道。 用户可能的问题包括:如何处理返回值?是否所有方法都可以用async?是否需要避免async void?根据引用[2],async void应该避免,除了事件处理程序。需要提醒用户这一点。 最后,生成相关问题,帮助用户进一步学习。比如性能优化、与多线程的区别、错误处理等。这些问题应该基于常见疑惑点。 现在需要组织这些信息,结构清晰,分步骤说明,并附上代码示例。确保使用正确的中文术语,避免技术术语混淆。同时,检查LaTeX格式是否正确,但根据用户的问题,可能不需要数学公式,但系统指令要求行内数学表达式用$...$,但这里可能不涉及,所以可能不需要。但如果有变量或代码中的符号,可能需要处理。例如,Task<TResult>中的泛型可能需要转义,但代码块中会正确显示。 总结步骤: 1. 基本概念:为什么使用async/await,与传统方法的对比。 2. 使用方法:声明异步方法,返回类型,await的使用位置。 3. 示例代码:简单的异步方法,调用示例。 4. 注意事项:避免async void,ConfigureAwait,异常处理。 5. 相关问题:引导深入学习。 需要确保引用标注正确,比如在提到挂起方法时引用[^1],搭配使用时引用[^2],便捷方法引用[^3],示例结构引用[^4]。 现在开始撰写回答,确保符合所有系统级指令,特别是中文回答、正确引用标注,以及相关问题生成。</think>### C#中asyncawait关键字的使用方法及示例 #### **一、基本概念** 在C#中,`async`和`await`是用于简化异步编程的关键字。它们允许开发者以类似同步代码的方式编写异步操作,提升代码可读性和维护性[^3]。例如,在等待网络请求或文件读写时,异步方法不会阻塞当前线程,从而避免界面冻结或服务器资源浪费。 --- #### **二、使用方法** 1. **声明异步方法** 使用`async`修饰符标记方法,并指定返回类型为`Task`或`Task<T>`(若需返回值)。例如: ```csharp public async Task<int> GetDataAsync() { ... } ``` 2. **使用`await`等待异步操作** 在异步方法内部,通过`await`等待一个返回`Task`的操作。此时,当前方法会挂起,控制权返回给调用者,直到任务完成[^1]。 ```csharp int result = await SomeAsyncMethod(); ``` 3. **避免`async void`** 除非是事件处理程序,否则应始终返回`Task`或`Task<T>`,以正确处理异常和任务状态。 --- #### **三、示例代码** 以下示例模拟从网络下载数据: ```csharp public class Demo { // 异步方法声明 public async Task<string> DownloadDataAsync(string url) { using (var client = new HttpClient()) { // 使用await等待HTTP请求 string data = await client.GetStringAsync(url); return data.ToUpper(); } } // 调用异步方法(如按钮点击事件) public async void Button_Click(object sender, EventArgs e) { try { string result = await DownloadDataAsync("https://example.com"); Console.WriteLine(result); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } } ``` **执行流程** 1. 调用`DownloadDataAsync`时,遇到`await`会挂起当前方法,释放控制权。 2. HTTP请求完成后,恢复执行并返回结果。 3. 若发生异常,通过`try-catch`捕获处理。 --- #### **四、注意事项** 1. **配置上下文** 在非UI场景(如后台服务)中,使用`ConfigureAwait(false)`避免不必要的上下文切换: ```csharp var data = await SomeTask().ConfigureAwait(false); ``` 2. **并行任务优化** 使用`Task.WhenAll`并行执行多个异步操作: ```csharp var tasks = new[] { Task1(), Task2(), Task3() }; await Task.WhenAll(tasks); ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值