为什么我们要使用Async、Await关键字

本文深入探讨了C#中Async/Await关键字的使用方法及其背后的原理。通过对比同步和异步操作,展示了如何利用这些关键字提高应用程序的响应性和性能。

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

 

前不久,在工作中由于默认(xihuan)使用Async、Await关键字受到了很多质问,所以由此引发这篇博文“为什么我们要用Async/Await关键字”,请听下面分解:

 

Async/Await关键字

Visual Studio(.net framework 4.5)提供了异步编程模型,相比之前实现方式,新的异步编程模型降低了使用的复杂度并且更容易维护和调试,编译器代替用户做了很多复杂的工作来实现异步编程模型[^4]。

 

 

 

  1. 调用异步方法AccesstheWebAsync
  2. 创建HttpClient实例,并使用HttpClient获取异步数据。
  3. 利用Task执行获取数据方法(假设获取数据需要很长时间),不阻塞当前线程,getStringTask代表进行中的任务。
  4. 因为getStringTask还没有使用await 关键字,使之可以继续执行不依赖于其返回结果的其他任务,同步执行DoIndependentWork。
  5. 当同步任务DoIndependentWork执行完毕之后,返回调用给AccessTheWebAsync线程。
  6. 使用await强制等待getStringTask完成,并获取基于Task<String>类型的返回值。(如果getStringTask在同步方法DoIndependentWork执行之前完成,调用会返回给AccessTheWebAsync线程,调用await将会执行不必要的挂起操作)
  7. 当获取web数据之后,返回结果记录在Task中并返回给await调用处(当然,返回值并没有在第二行返回)。
  8. 获取数据并返回计算结果。 

 

刨根问底

 

以上是官方给的说明文档,例子详尽表达清楚,但是有一个问题没有解决(被证明):

 

1. 当线程在await处返回给线程池之后,该线程是否“真的”被其他请求所消费?

2. 服务器线程资源是一定的,是谁在真正执行Await所等待的操作,或者说异步IO操作?

3. 如果使用IO线程执行异步IO操作,相比线程池的线程有什么优势?或者说异步比同步操作优势在哪里?

 

前提条件:

 

1. 相对Console应用程序来说,可以使用ThreadPool的SetMaxThread来模拟当前进程所支持的最大工作线程和IO线程数。

2. 通过ThreadPool的GetAvailableThreads可以获得当前进程工作线程和IO线程的可用数量。

3. ThreadPool是基于进程的,每一个进程有一个线程池,IIS Host的进程可以单独管理线程池。

4. 如果要真正意义上的模拟异步IO线程操作文件需要设置FileOptions.Asynchronous,而不是仅仅是使用BeginXXX一类的方法,详情请参考[^1]的异步IO线程。

5. 在验证同步和异步调用时,执行的任务数量要大于当前最大工作线程的2倍,这样才可以测出当Await释放工作线程后,其他请求可继续利用该线程。

 

 

结论:

 

1.  Await使用异步IO线程来执行,异步操作的任务,释放工作线程回线程池。

2.  线程池分为工作线程和异步IO线程,分别执行不同级别的任务。

3.  使用Await来执行异步操作效率并不总是高于同步操作,需要根据异步执行长短来判断。

4.  当工作线程和IO线程相互切换时,会有一定性能消耗。

 


各位可以Clone代码,并根据Commit去Review代码,相信大家能理解代码意图,如果不能,请留言,我改进:)

 

 

[GitHubRepo](https://github.com/Cuiyansong/Why-To-Use-Async-Await-In-DotNet.git)

 

 

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
 
namespace AsyncAwaitConsole
{
    class Program
    {
        static int maxWorkerThreads;
        static int maxAsyncIoThreadNum;
        const string UserDirectory = @"files\";
        const int BufferSize = 1024 * 4;
 
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) =>
            {
                Directory.Delete("files", true);
            };
 
            maxWorkerThreads = Environment.ProcessorCount;
            maxAsyncIoThreadNum = Environment.ProcessorCount;
            ThreadPool.SetMaxThreads(maxWorkerThreads, maxAsyncIoThreadNum);
 
            LogRunningTime(() =>
            {
                for (int i = 0; i < Environment.ProcessorCount * 2; i++)
                {
                   Task.Factory.StartNew(SyncJob, new {Id = i});
                }
            });
 
            Console.WriteLine("===========================================");
 
            LogRunningTime(() =>
            {
                for (int i = 0; i < Environment.ProcessorCount * 2; i++)
                {
                    Task.Factory.StartNew(AsyncJob, new { Id = i });
                }
            });
 
            Console.ReadKey();
        }
 
        static void SyncJob(dynamic stateInfo)
        {
            var id = (long)stateInfo.Id;
            Console.WriteLine("Job Id: {0}, sync starting...", id);
 
            using (FileStream sourceReader = new FileStream(UserDirectory + "BigFile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize))
            {
                using (FileStream destinationWriter = new FileStream(UserDirectory + $"CopiedFile-{id}.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, BufferSize))
                {
                    CopyFileSync(sourceReader, destinationWriter);
                }
            }
            Console.WriteLine("Job Id: {0}, completed...", id);
        }
 
        static async Task AsyncJob(dynamic stateInfo)
        {
            var id = (long)stateInfo.Id;
            Console.WriteLine("Job Id: {0}, async starting...", id);
 
            using (FileStream sourceReader = new FileStream(UserDirectory + "BigFile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.Asynchronous))
            {
                using (FileStream destinationWriter = new FileStream(UserDirectory + $"CopiedFile-{id}.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, BufferSize, FileOptions.Asynchronous))
                {
                    await CopyFilesAsync(sourceReader, destinationWriter);
                }
            }
            Console.WriteLine("Job Id: {0}, async completed...", id);
        }
 
        static async Task CopyFilesAsync(FileStream source, FileStream destination)
        {
            var buffer = new byte[BufferSize + 1];
            int numRead;
            while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
            {
                await destination.WriteAsync(buffer, 0, numRead);
            }
        }
 
        static void CopyFileSync(FileStream source, FileStream destination)
        {
            var buffer = new byte[BufferSize + 1];
            int numRead;
            while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
            {
                destination.Write(buffer, 0, numRead);
            }
        }
 
        static void LogRunningTime(Action callback)
        {
            var awailableWorkingThreadCount = 0;
            var awailableAsyncIoThreadCount = 0;
 
            var watch = Stopwatch.StartNew();
            watch.Start();
 
            callback();
 
            while (awailableWorkingThreadCount != maxWorkerThreads)
            {
                Thread.Sleep(500);
                ThreadPool.GetAvailableThreads(out awailableWorkingThreadCount, out awailableAsyncIoThreadCount);
 
                Console.WriteLine("[Alive] working thread: {0}, async IO thread: {1}", awailableWorkingThreadCount, awailableAsyncIoThreadCount);
            }
 
            watch.Stop();
            Console.WriteLine("[Finsih] current awailible working thread is {0} and used {1}ms", awailableWorkingThreadCount, watch.ElapsedMilliseconds);
        }
    }
}
View Code

 

 

注:Async/Await并没有创建新的线程,而是基于当前同步上线文的线程,相比Thread/Task或者是基于线程的BackgroundWorker使用起来更方便。Async关键字的作用是标识在Await处需要等待方法执行完成,过多的await不会导致编译器错误,但如果没有await时,方法将转换为同步方法. 

 

基于IIS Host的应用程序 

 

 

 

 1. IIS 可以托管ThreadPool,通过在IIS Application Pool中增加,并且可以设置Working Thread 和 Async IO Thread 数目。

2. 服务端接受请求并从线程池中获取当前闲置的线程进行处理,如果是同步处理请求,当前线程等待处理完成然后返回给线程池. 服务器线程数量有限,当超过IIS所能处理的最大请求时,将返回503错误。

3. 服务端接受请求并异步处理请求时,当遇到异步IO类型操作时,当前线程返回给线程池。当异步操作完成时,从线程池中拿到新的线程并继续执行任务,直至完成后续任务[^7]。

 

例如,在MVC Controller中加入awaitable方法,证明当遇到阻塞任务时,当前线程立即返回线程池。当阻塞任务完成时,将从线程池中获取新的线程执行后续任务:

 

     var availableWorkingThreadCount = 0;

                var availableAsyncIoThreadCount = 0;

                ThreadPool.GetAvailableThreads(out availableWorkingThreadCount, out availableAsyncIoThreadCount);

                AddErrors(new IdentityResult(string.Format("[IIS Host] Thread Id {0}, ThreadPool Thread: {1}",

                    Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread)));

                AddErrors(new IdentityResult(string.Format("[IIS Host] current working thread: {0}, current async thread: {1}", availableWorkingThreadCount, availableAsyncIoThreadCount)));

 

                HttpClient httpClient = new HttpClient();

                var response = httpClient.GetStringAsync("https://msdn.microsoft.com/en-us/library/system.threading.thread.isthreadpoolthread(v=vs.110).aspx");

                await response;

 

                AddErrors(new IdentityResult(string.Format("[IIS Host] Thread Id {0}, ThreadPool Thread: {1}",

                Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread)));

 

 

[IIS Host] Thread Id 4, ThreadPool Thread: True

[IIS Host] current working thread: 4094, current async thread: 1000

[IIS Host] Thread Id 9, ThreadPool Thread: True

 

 

结论:

  • 同步方法应用场景:
    • 请求处理非常快
    • 代码简洁大于代码效率
    • 主要是基于CPU耗时操作

 

  • 异步方法应用场景:
    • 基于Network或者I/O类型操作,而非CPU耗时操作
    • 当阻塞操作成为瓶颈时,通过异步方法能使IIS处理更多的请求
    • 并行化处理比代码简洁更重要
    • 提供一种机制可以让用户取消长时间运行的请求 

 

 更多线程优化

Stephen Cleary 介绍了三种异步编程模型的规范[^5]:

1. Avoid Async Void, void和task<T>将产生不同的异常类型

2. 总是使用Async关键字

3. 使用Task.WaitXXX 代替Task.WhenXXX

4. Configure context 尽量不要捕捉线程上下文,使用Task.ConfigureAwait(false)

 

引用

[^1] 《CLR via C# Edition3》 25章线程基础

[^2]百科-蜜蜂舞:http://baike.baidu.com/link?url=ixwDjgocRIg4MJGTQyR3mUC1fspHZtfPYEtADfJAJdC6X0xIVU4lJUe2iVvCNHEj3JeE1JalBCNyyPcVMdhaoyBFz_xXcLPMEJ_2iUcHjithF8_F8A9yI61EAzpmpYR4

[^3] 异步编程模型:https://msdn.microsoft.com/en-us/library/mt674882.aspx

[^4] C# Async、Await关键字:https://msdn.microsoft.com/library/hh191443(vs.110).aspx

[^5] Task Best Practice[Stephen Cleary]: https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

[^6] 异步编程模型最佳实践中文翻译版:http://www.cnblogs.com/farb/p/4842920.html

[^7] 同步vs异步Controller:https://msdn.microsoft.com/en-us/library/ee728598%28v=vs.100%29.aspx

[^8] IIS 优化: https://docs.microsoft.com/en-us/aspnet/mvc/overview/performance/using-asynchronous-methods-in-aspnet-mvc-4

 

转载于:https://www.cnblogs.com/cuiyansong/p/7424997.html

<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、付费专栏及课程。

余额充值