C#深入理解异步编程async/await

async/await 本身是个语法糖

看我这篇文章: https://blog.csdn.net/weixin_46879188/article/details/120849575

C#的另一个语法糖:用yield实现IEnumerable接口也是采用这种技术。

使用async/await和.Result和.Wait()和.GetAwaiter().GetResult()的区别

用法:
await搭配 (返回Task/Task<T>)使用。

.Wait()和GetAwaiter().GetResult() 搭配(返回Task/Task<T>)使用。

.Result只能搭配 (返回Task<T>)使用。

功能:

await是阻塞当前方法继续调用,但不阻塞当前线程;

.Result和.Wait()和.GetAwaiter().GetResult() 会在阻塞当前线程同时仍然会启动另外一个后台线程去执行这个函数。

在非常特殊的情况下,如果非得阻塞当前线程去执行这个 Task,可以考虑使用 .GetAwaiter().GetResult()。两者效果基本一样,阻塞当前线程同时会启动另外一个后台线程。但是如果使用 .Result 或 .Wait() 发生错误时会抛出 AggregateExecption,如果用Exception去也是可以捕捉到异常的 。而使用 .GetAwaiter().GetResult()则是会抛出实际的 Excepiton ,Excepiton 中的堆栈也符合我们所期望的。

主要是区分两个概念:
1.阻塞当前方法调用;
2.阻塞当前线程;

Async

Async 方法有三种可能的返回类型: Task、Task<T> 和 void。 当从同步转换为异步代码时,任何返回类型 T 的方法都会成为返回 Task<T> 的 async 方法,任何返回 void 的方法都会成为返回 Task 的 async 方法。返回 void 的 async 方法具有特定用途: 用于支持异步事件处理程序,async, await 底层是状态机, 而如果返回值是void的话,调度方是不会有等待行为的,因为没有awaiter(可以看看下面demo的static async void Test()方法)

如下Demo:

      static void Main(string[] args)
        {

            Console.WriteLine("①我是主线程,线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
			
			//同步方法调用异步方法,遇到异步方法内部的第一个await之后,不会等待await之后的代码执行完毕,会马上返回到Main()方法中,执行var testResult = TestAsync();之后的代码
            var testResult = TestAsync();

//GetAwaiter().OnCompleted() 方法允许你将一个回调函数绑定到一个异步任务的完成事件上。当异步任务完成时,这个回调函数会被调用。这种方法的好处是它不会阻塞当前线程,因为回调函数会在另一个线程中执行。
            testResult.GetAwaiter().OnCompleted(() => {
                var id = testResult.Id;
                Console.WriteLine("My name is: " + id);
            });

            Console.WriteLine("主线程执行完毕");

            Console.ReadKey();
        }
        static async Task TestAsync()
        {
            Console.WriteLine("②调用GetReturnResult()之前,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
                DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
            var name = GetReturnResult();
            Console.WriteLine("④调用GetReturnResult()之后,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
                DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
                
	//遇到await name后,  因为没有使用await方法,不会等待之后的代码执行完毕,会马上返回到Main()方法中,执行var testResult = TestAsync(); 之后的代码
            Console.WriteLine("⑥得到GetReturnResult()方法的结果一:{0}。当前时间:{1}", await name,
                DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
        }

        static async Task<string> GetReturnResult()
        {
            Console.WriteLine("③执行Task.Run之前, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
			
			//遇到await Task.Run后, 因为没有使用await方法,不会等待之后的代码执行完毕,会马上返回到TestAsync()方法中,执行 var name = GetReturnResult(); 之后的代码
			
            return await Task.Run(() =>
            {
                Thread.Sleep(5000);
                Console.WriteLine("⑤GetReturnResult()方法里面线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
                return "我是返回值";
            });
        }

输出结果如下:

在这里插入图片描述

Execution Context 执行上下文

它在多线程的好比空气:你可以不知道它,但它非常重要。ExecutionContext是为了解决线程本地存储在多线程中无法传递的问题:总得有一种机制能够传递全局信息。否则只能通过函数调用参数传递了。
当一个线程发起异步调用的时候,ExecutionContext会自动的在线程之间传递以下信息:
线程安全设置
Host设置(与web服务有关)
Logical Call Context, 可以在其中保存和传递对象。
线程的Culture(从.NET 4.6以后)。

Synchronization Context 同步上下文

它是为了描述异步调用返回时的行为所创建的抽象。它有两个基本接口方法:
Send 同步地等待任务执行完毕。
Post 把任务发出去就不管了。
那么异步调用返回时的行为是什么意思?既然是抽象,那就会有具体的实现。后面我们会看到几种实现。
当开始异步调用时,C#会捕获(capture)当前线程的同步上下文,并保存到Task中。在异步调用返回时,需要恢复(resume)同步上下文。此时就会调用同步上下文的Send或者Post。
下面是几种典型的同步上下文实现:
1.UI同步上下文。由于UI界面操作必须在UI线程中进行,因此这个上下文做的事情就是把需要恢复的工作Marshal起来交给UI线程。(可能有人会好奇如何交给UI线程去做。简单来说, UI线程有个Windows消息循环,同步上下文将任务封装在一个特定消息中,UI线程得到这个消息后,就去执行其中的任务)。
2.
ASP.NET同步上下文。它有以下特点:
3.
①不会切换线程,因为后台线程没什么区别。
②会把线程的Principle和Culture传递过去。(因为ASP.NET依赖于此)
③在异步页面中记录尚未完成的IO数量。
4.默认同步上下文。就是线程池的调度器,基本上没有特别的操作。
最后,调用ConfigureAwait(false)时,就会跳过恢复同步上下文这一过程。所以,有时候必要(当没必要传递任何信息时,使用它可以提高效率),有时候又会出错。例如,UI程序的异步调用本来没问题,你加了这个语句,反而会造成修改界面的操作可能不在UI线程中执行,从而出错。但是注意,无论如何,执行上下文都是会传递的。
结合以上,第一段程序的更精确的编译后版本是这样的:

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(
                task => SynchronizationContext.Current.Post(
                 state => btnDoStuff_Click(task.Step), 
                 task)
            );
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

到底是谁在执行异步调用?
这个问题曾经困扰我很久。如果我当前的线程调用一个异步调用后返回了,那到底是谁在完成真正调用的工作呢?答案是一个(或几个)共享的线程:线程池中的IO线程。
如下是一段代码:

async void GetButton_OnClick(object o, EventArgs e)
{
    Task<Image> task = GetFaviconAsync(_url);

    Image image = await task;

    AddAFavicon(image);
}

async Task<Image> GetFaviconAsync(string url)
{
    var task = _webClient.DownloadDataTaskAsync(url);

    byte[] bytes = await task;

    return MakeImage(bytes);
}

线程的执行情况如下图:

在这里插入图片描述

大部分的时间都在用户线程中。只有调用到非常底层,IO完成之后,才有IO线程被唤醒(见11),然后它调用Task的同步上下文的Post,将剩下的任务再交给用户线程去执行(Winform是委托UI线程。ASP.NET是可能是发起请求的线程,但更有可能是操作完成时空闲的任何线程。)。
下面是一个动态的解释:
在这里插入图片描述

<think>好的,我现在需要帮助用户了解C#中的async/await的用法和原理。首先,我得整理一下自己知道的信息,确保准确无误。根据用户提供的引用资料,async/awaitC#中处理异步编程的关键字,async修饰方法表示异步,await用于等待异步操作完成。引用[1]和[2]提到了基本用法,引用[3]讲到了异常处理和避免阻塞,引用[4]讨论了原理,特别是关于线程的问题。 首先,我需要解释async/await的基本使用方法。比如如何定义一个异步方法,使用async关键字,返回类型通常是Task或Task<T>。然后,await的使用场景,比如在调用异步方法时使用await,这样可以让后续代码在异步操作完成后继续执行,而不会阻塞当前线程。 接下来是示例代码部分。用户可能需要一个简单的例子,比如一个模拟的异步下载方法,然后在主方法中调用它。这里要注意代码的正确性,比如方法的返回类型,使用Task.Delay来模拟耗时操作,以及Main方法标记为async,这在C# 7.1之后是允许的。 然后是原理部分。根据引用[4],async/await本身不会创建新线程,但底层的异步操作可能会。需要解释状态机的工作机制,编译器如何将async方法转换为状态机,await如何让出控制权,以及如何回调继续执行剩余代码。这部分可能比较复杂,需要用通俗易懂的语言解释,避免过于技术化。 还要提到异常处理,像引用[3]里说的,使用try-catch来捕获异步方法中的异常。这里可以举一个例子,在await调用周围包裹try块,捕获可能发生的异常。 最后是注意事项,比如避免阻塞异步代码,不要使用.Result或.Wait(),而是始终使用await。配置上下文,比如在UI线程中保持同步上下文,而在ASP.NET Core中默认没有,需要特别注意。 现在要生成相关问题。根据用户提供的示例,问题应该围绕使用方法、原理、异常处理、性能优化等展开。比如如何正确处理异常,async方法是否会阻塞线程,状态机的作用,与多线程的区别,以及性能优化的方法。 需要确保回答结构清晰,分点说明,代码示例正确,并且引用正确的来源。检查是否有遗漏的信息,比如线程池的使用或者异步IO操作的解释,确保覆盖用户可能关心的点。 最后,通读整个回答,确认是否符合用户的要求,特别是使用中文,正确的LaTeX格式(虽然这里可能用不上),以及引用标记的正确添加。确保没有技术错误,比如关于线程创建的部分是否准确,根据引用[4],异步操作本身可能在其他线程执行,但async方法本身不直接创建线程。</think>### C#async/await的用法与原理 #### 1. 基本用法 在C#中,`async`关键字用于声明异步方法,其返回类型通常为`Task`或`Task<T>`(无返回值或有返回值)。`await`关键字用于等待异步操作完成,期间线程可释放以执行其他任务,避免阻塞[^1]。例如: ```csharp public async Task DownloadDataAsync() { // 模拟耗时操作(如网络请求) await Task.Delay(1000); Console.WriteLine("数据下载完成"); } ``` #### 2. 代码执行流程 调用异步方法时,使用`await`会暂停当前方法,将控制权交回调用方。待异步操作完成后,剩余代码继续执行: ```csharp public static async Task Main() { Console.WriteLine("开始"); await DownloadDataAsync(); Console.WriteLine("结束"); } ``` 输出顺序为:开始 →(等待1秒)→ 数据下载完成 → 结束[^2]。 --- #### 3. 实现原理 - **状态机机制**:编译器将`async`方法转换为状态机类,通过`MoveNext`方法管理代码执行阶段。`await`是状态机的“断点”,标记异步操作前后的代码分割[^4]。 - **线程模型**:`async`方法本身不创建新线程,但底层异步操作(如文件I/O或网络请求)可能使用线程池或操作系统级异步I/O。例如: ```csharp await File.WriteAllTextAsync("test.txt", "内容"); // 异步I/O,不占用线程 ``` - **同步上下文**:在UI线程中,`await`后的代码默认回到原线程执行;在ASP.NET Core等无同步上下文的环境中,代码可能在任意线程池线程恢复。 --- #### 4. 异常处理 使用`try-catch`捕获异步操作中的异常: ```csharp try { await GetDataAsync(); } catch (Exception ex) { Console.WriteLine($"错误:{ex.Message}"); } ``` 异步方法抛出的异常会封装到`Task`中,通过`await`重新抛出[^3]。 --- #### 5. 注意事项 1. **避免阻塞**:禁止使用`.Result`或`.Wait()`,否则可能导致死锁(尤其在UI线程中)。 2. **返回值**:无返回值用`Task`,有返回值用`Task<T>`;`void`仅用于事件处理。 3. **性能优化**:对CPU密集型任务,可结合`Task.Run`卸载到线程池: ```csharp var result = await Task.Run(() => Calculate(1000000)); ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值