绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
如何避免让线程摸鱼,请用异步技术 async await 拿捏他~
2023-02-09 17:22:36

发现问题

你点了外卖后,会一直不做其它事情,一直等外卖的到来么?
当然不会拉!

我们来看看代码世界的:

Copy
public void Query(){ // 当前线程 向 数据库服务器 发起查询命令 // 在 数据库服务器 返回数据之前,当前线程 一直等待,不干活了!!! var data = Database.Query(); }

假设在一个请求响应中:

  1. 线程用 5ms 来验证用户的输入的参数;
  2. 线程用 50ms 来等待数据库返回;
  3. 线程用 5ms 序列化数据响应返回给用户;

可以看到在 60ms 中,线程摸鱼 50ms。

而很多Web框架,收到一个请求,就会创建一个线程来处理,
如果片刻间内有100个用户请求这个方法,那么就得安排100个线程,
有没有方法让第1个线程在等待数据返回时,先去接待第N+1个用户(校验请求参数什么的)
这样就能大大减少线程数量~

通过上面的例子,我相信你已有所悟:异步就是避免让线程摸鱼。

概念与理论

接下来为了更有效地沟通和提示逼格,我们还是使用专业的术语。

复习一下线程的阻塞睡眠挂起
主要是弄明白阻塞的定义,和什么时候会发生阻塞

线程阻塞

Copy
Thread t = new Thread(()=>{ // 阻塞:线程 被动 地等待外部返回,才能继续执行 var resp = Http.Get(url); // 需要等待网络传输文档 });

线程睡眠

Copy
Thread t = new Thread(()=>{ // 睡眠:线程 主动 停止执行片刻,然后继续执行 Thread.Sleep(1000); });

线程挂起

Copy
// 伪代码,C# 的 ThreadPool 没有这些方法 // 主动叫线程去休息 ThreadPool.Recycle(t) // 等到有工作了,再叫线程处理执行 t = ThreadPool.GetThread(); t.Run(fun);

Synchronous(同步)
本人对 同步 给出比较容易理解的定义是:按顺序步骤,一个步骤只做一件事情。
本人以前看到 同步 这个词,错误地顾名思义,以为是同一刻时间做几件事,错错错!!!

Copy
// 线程会一步一步执行以下代码,这个过程叫 同步 // 先发完短信 SMS.Send(msg); // 2秒 // 再发邮件 Email.Send(smg); // 1秒 // 总耗时 3秒

Parallel(并行)
指两个或两个以上事件(或线程)在同一时刻发生。

Copy
// 分别创建两个线程并行去执行,谁也不用等待谁~ Thread t1 = new Thread(()=>{ SMS.Send(msg); // 2秒 }); // t2 线程不需要等待 t1 线程 Thread t2 = new Thread(()=>{ Email.Send(smg); // 1秒 }); // 总耗时 2秒

微软官方文档-使用 Async 和 Await 的异步编程

微软用的做早餐的例子:

  1. 倒一杯咖啡。
  2. 加热平底锅,然后煎两个鸡蛋。
  3. 煎三片培根。
  4. 烤两片面包。
  5. 在烤面包上加黄油和果酱。
  6. 倒一杯橙汁。

同步则是单人(单线程)从 1 到 6 一步一步地做 —— 效率低。
并行则是多人(多线程),一人倒咖啡;一人煎鸡蛋;一个...同时进行 —— 效率高,人力成本高。
异步则是单人(单线程),点火热平底锅,平底锅要等待变热,那么先把面包放进烤面包机...

Asynchronous(异步)
指的是,当线程遇到阻塞时,让线程先去执行其它工作~

我们应该体验过,当一个人要在很多事情上来回切换的时候,很容易出错。

做早餐,我们点火热平底锅后就去烤面包,但平底锅什么时候好,我们什么时候切换回来煎鸡蛋,还是去倒橙汁。

要将代码的执行过程写成异步的,也不是容易的事情。

好在 C# 提供 async 和 await 这两个关键字,
轻松创建异步方法(几乎与创建同步方法一样轻松) —— 微软官方文档原话

理论讲解完毕,是时候来实践了~

async 修饰符

Copy
public void Get() { // 这是一个 同步方法 // 如果这个内部有会发生阻塞的功能代码,比如读取网络资源, // 那么一个线程运行这个方法遇到阻塞,这个线程就会摸鱼~ }

要将一个同步方法声明为异步方法,首先需要将用 async 修饰符标记一下,

Copy
public async void Get() { // 这是一个 异步方法 // 如果这个内部有会发生阻塞的功能代码 // 那么一个线程运行这个方法遇到阻塞时,这个线程就会去做其它事情~ }
Copy
public async void Get() { HttpClient httpClient = new HttpClient(); httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/"); }

加入一些我们需要观察的代码后,得:

Copy
public static void Main() { Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Get(); Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadKey(); } // 这代码是有问题的,我有意为之,用来和接下来的更完善的代码做比较~ public static async void Get() { Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/"); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

运行后的控制台输出:

Copy
Main 开始执行前线程 Id:1 Get 开始执行前线程 Id:1 Get 执行结束后线程 Id:1 Main 执行结束后线程 Id:1

注意!!!这个时候方法虽然被声明为异步的,但现在执行过程还是同步的!!!!

await 运算符

微软官方文档:async(C# 参考) 中:

异步方法同步运行,直至到达其个 await 表达式,此时会将方法挂起,直到等待的任务完成。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行。 编译器警告将通知你不包含 await 语句的任何异步方法,因为该情况可能表示存在错误。 请参阅编译器警告(等级 1)CS4014。

所以完善的代码,应该是这样子的:

Copy
public static void Main() { Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Get(); // Get 方法虽然是声明为异步的,但依旧时同步执行 Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadKey(); } public static async void Get() { Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); // 加上 await 运算符,才是真正的异步执行!!! await httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/"); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

运行后的控制台输出:

Copy
Main 开始执行前线程 Id:1 # 线程1,进入 main 函数 Get 开始执行前线程 Id:1 # 线程1,执行 Get 函数,遇到阻塞,但线程1被要求不能摸鱼, Main 执行结束后线程 Id:1 # 于是看看有没有其它工作做,发现需要打印... Get 执行结束后线程 Id:9 # 阻塞结束后,谁来执行剩下的代码呢?              # 如果线程1有空,可以回来执行,如果线程1忙,则有其它线程接管              # 由调度分配决定

我们自己定义的异步方法 Get() 和调用异步方法 httpClient.GetAsync
只有 httpClient.GetAsync 是异步执行的。
也就是说单单使用 async 还不够,还得必须同时使用 await

Task 类

通常来说,我们使用 httpClient.GetAsync,都是希望能处理返回的数据。

微软官方文档:异步方法的返回类型

  • Task 表示不返回值且通常异步执行的单个操作。
  • Task<TResult> 表示返回值且通常异步执行的单个操作。
  • void 对于除事件处理程序以外的代码,通常不鼓励使用 async void 方法,因为调用方不能 await 那些方法,并且必须实现不同的机制来报告成功完成或错误条件。
Copy
public static async void Get() { const string url = "https://learn.microsoft.com/zh-cn/docs/"; Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); // 用 Task 来 = 一个异步操作 Task<HttpResponseMessage> taskResp = httpClient.GetAsync(url); HttpResponseMessage resp = await taskResp;// 等待异步操作完成返回 // 可以对 resp 进行一些处理 Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

上面代码可以简化为:

Copy
public static async void Get() { const string url = "https://learn.microsoft.com/zh-cn/docs/"; Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); HttpResponseMessage resp = await httpClient.GetAsync(url); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

多个Task 的例子:

Copy
public static async void Get() { Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); var t1 = httpClient.GetAsync("https://learn.microsoft.com/"); var t2 = httpClient.GetAsync("https://cn.bing.com/"); var t3 = httpClient.GetAsync("https://www.cnblogs.com/"); Console.WriteLine($"Get await 之前的线程 Id:{Thread.CurrentThread.ManagedThreadId}"); await Task.WhenAll(t1, t2, t3); // 等待多个异步任务完成 //Task.WaitAll(t1, t2, t3); //await Task.Yield(); //await Task.Delay(0); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

运行后的控制台输出:

Copy
Main 开始执行前线程 Id:1 Get 开始执行前线程 Id:1 Get await 之前的线程 Id:1 Main 执行结束后线程 Id:1 Get 执行结束后线程 Id:14

按微软官方文档的建议和规范的终版本:

Copy
public static void Main() { Console.WriteLine($"Main 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); GetAsync().Wait(); Console.WriteLine($"Main 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadKey(); } // 通常不鼓励使用 async void 方法 // 异步方法名约定以 Async 结尾 public static async Task GetAsync() { Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); HttpClient httpClient = new HttpClient(); var t1 = httpClient.GetAsync("https://learn.microsoft.com/"); var t2 = httpClient.GetAsync("https://cn.bing.com/"); var t3 = httpClient.GetAsync("https://www.cnblogs.com/"); Console.WriteLine($"Get await 之前的线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Task.WaitAll(t1, t2, t3); // 等待多个异步任务完成 await Task.Yield(); //await Task.Delay(0); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); }

运行后的控制台输出:

Copy
Main 开始执行前线程 Id:1 Get 开始执行前线程 Id:1 Get await 之前的线程 Id:1 Get 执行结束后线程 Id:5 Main 执行结束后线程 Id:1

测试

Copy
public static async Task GetAsync() { Console.WriteLine($"Get 开始执行前线程 Id:{Thread.CurrentThread.ManagedThreadId}"); Stopwatch sw = new Stopwatch(); sw.Start(); TestHttp(); // http 网络不稳定,不好观察时间,可以试试 TestIdle() sw.Stop(); Console.WriteLine($"一共耗时:{sw.ElapsedMilliseconds} 毫秒"); Console.WriteLine($"Get 执行结束后线程 Id:{Thread.CurrentThread.ManagedThreadId}"); await Task.Yield(); } public static void TestHttp() { HttpClient httpClient = new HttpClient(); List<Task<HttpResponseMessage>> tasks = new List<Task<HttpResponseMessage>>(); for (int i = ; i < 10; i++) { var t = httpClient.GetAsync("https://learn.microsoft.com/"); tasks.Add(t); } Task.WaitAll(tasks.ToArray()); foreach (var item in tasks) { var html = item.Result.Content.ReadAsStringAsync().Result; } } public static void TestIdle() { List<Task> tasks = new List<Task>(); for (int i = ; i < 10; i++) { var t = Idle(); tasks.Add(t); } Task.WaitAll(tasks.ToArray()); } public static async Task Idle() { // 可以用于模拟阻塞效果 await Task.Delay(1000); // 不能用 Sleep 来模拟阻塞,Sleep 不是阻塞,是睡眠 // Thread.Sleep(1000); }
Copy
Main 开始执行前线程 Id:1 Get 开始执行前线程 Id:1 一共耗时:604 毫秒 # 1个线程干了10个线程的活,时间还差不多,美滋滋~ Get 执行结束后线程 Id:1 Main 执行结束后线程 Id:1

至此,关于 C# 中异步编程的三个知识点 asyncawaitTask 讲解完毕。

在写例子的过程中,
发现 HttpClient 这个类很多方法都是异步方法了,
依稀记得以前还有同步方法和异步方法提供选择的,
看来微软是在逼大家进步啊~

分享好友

分享这个小栈给你的朋友们,一起进步吧。

数据库开发
创建时间:2020-06-17 14:33:07
数据库开发是数据库管理系统(DBMS)和数据库应用软件设计研发的总称,主要是数据运维、参与数据库生产环境的问题优化和解决等方面的事宜
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 小雨滴
    专家
戳我,来吐槽~