发现问题

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

当然不会拉!

我们来看看代码世界的:

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

假设在一个请求响应中:

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

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

而很多Web框架,收到一个请求,就会创建一个线程来处理,

如果片刻间内有100个用户请求这个方法,那么就得安排100个线程,

有没有方法让第1个线程在等待数据返回时,先去接待第N+1个用户(校验请求参数什么的)

这样就能大大减少线程数量~

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

概念与理论

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

复习一下线程的阻塞睡眠挂起

主要是弄明白阻塞的定义,和什么时候会发生阻塞

线程阻塞

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

线程睡眠

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

线程挂起

// 伪代码,C# 的 ThreadPool 没有这些方法

// 主动叫线程去休息
ThreadPool.Recycle(t) // 等到有工作了,再叫线程处理执行
t = ThreadPool.GetThread();
t.Run(fun);

Synchronous(同步)

本人对 同步 给出比较容易理解的定义是:按顺序步骤,一个步骤只做一件事情。

本人以前看到 同步 这个词,错误地顾名思义,以为是同一刻时间做几件事,错错错!!!

// 线程会一步一步执行以下代码,这个过程叫 同步

// 先发完短信
SMS.Send(msg); // 2秒 // 再发邮件
Email.Send(smg); // 1秒 // 总耗时 3秒

Parallel(并行)

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

// 分别创建两个线程并行去执行,谁也不用等待谁~
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# 提供 asyncawait 这两个关键字,

轻松创建异步方法(几乎与创建同步方法一样轻松) —— 微软官方文档原话

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

async 修饰符

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

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

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

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

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}");
}

运行后的控制台输出:

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

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

await 运算符

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

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

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

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

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}");
}

运行后的控制台输出:

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 那些方法,并且必须实现不同的机制来报告成功完成或错误条件。
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}");
}

上面代码可以简化为:

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 的例子:

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}");
}

运行后的控制台输出:

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

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

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}"); }

运行后的控制台输出:

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

测试

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 = 0; 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 = 0; 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);
}
Main 开始执行前线程 Id:1
Get 开始执行前线程 Id:1
一共耗时:604 毫秒 # 1个线程干了10个线程的活,时间还差不多,美滋滋~
Get 执行结束后线程 Id:1
Main 执行结束后线程 Id:1

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

在写例子的过程中,

发现 HttpClient 这个类很多方法都是异步方法了,

依稀记得以前还有同步方法和异步方法提供选择的,

看来微软是在逼大家进步啊~

如果文章能帮到你,点个赞吧,十分感谢~

参考资料

异步编程:

https://docs.microsoft.com/zh-cn/dotnet/csharp/async

使用 Async 和 Await 的异步编程:

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async

异步编程模型:

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

深入了解异步:

https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth

async 关键字:

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async

await 运算符:

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/await

Async/Await 异步编程中的最佳做法:

https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

Future 与 promise:

https://zh.wikipedia.org/wiki/Future与promise

如何避免让线程摸鱼,请用异步技术 async await 拿捏他~的更多相关文章

  1. python爬虫14 | 就这么说吧,如果你不懂python多线程和线程池,那就去河边摸鱼!

    你知道吗? 在我的心里 你是多么的重要 就像 恩 请允许我来一段 freestyle 你们准备好了妹油 你看 这个碗 它又大又圆 就像 这条面 它又长又宽 你们 在这里 看文章 觉得 很开心 就像 我 ...

  2. 【转】让Chrome化身成为摸鱼神器,利用Chorme运行布卡漫画以及其他安卓APK应用教程

    下周就是十一了,无论是学生党还是工作党,大家的大概都会有点心不在焉,为了让大家更好的心不在焉,更好的在十一前最后一周愉快的摸鱼,今天就写一个如何让Chrome(google浏览器)运行安卓APK应用的 ...

  3. 菜鸡学C语言之摸鱼村村长

    题目描述 摸鱼村要选村长了! 选村长的规则是村里每个人进行一次投票,票数大于人数一半的成为村长. 然鹅摸鱼村的人都比较懒,你能帮他们写一个程序来找出谁当选村长吗? (每名村民的编号都是一个int范围内 ...

  4. Thief-Book 上班摸鱼神器

    Thief-Book 上班摸鱼神器 介绍 Thief-Book 是一款真正的摸鱼神器,可以更加隐秘性大胆的看小说. 隐蔽性 自定义透明背景,随意调整大小,完美融入各种软件界面 快捷性 三个快捷键,实现 ...

  5. 春节前“摸鱼”指南——SCA命令行工具助你快速构建FaaS服务

    春节将至,身在公司的你是不是已经完全丧失了工作的斗志? 但俗话说得好:"只要心中有沙,办公室也能是马尔代夫." 职场人如何才能做到最大效能地带薪"摸鱼",成为了 ...

  6. 删库吧,Bug浪——我们在同一家摸鱼的公司

    那些口口声声, Bug越来越难写人的,应该盯着你们: 像我一样,我盯着你们,满眼恨意. IT积攒了几十年的漏洞, 所有的死机.溢出.404和超时, 像是专门为你们准备的礼物. 圈复杂度.魔鬼变量.内存 ...

  7. 寒武纪加速平台(MLU200系列) 摸鱼指南(二)--- 模型移植-环境搭建

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  8. 寒武纪加速平台(MLU200系列) 摸鱼指南(四)--- 边缘端实例程序分析

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  9. [摸鱼]cdq分治 && 学习笔记

    待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...

  10. HNOI2018 摸鱼记

    HNOI2018 摸鱼记 今天我又来记流水账啦 Day 0 颓废的一天. 我,球爷和杜教在颓膜膜.io ych看起来在搓碧蓝 鬼知道哥达鸭干了什么 学习氛围只局限在机房的一角 后来全体Oier开会,5 ...

随机推荐

  1. excel公式与快捷操作

    将首行的公式,运用到这一整列 1.选中要输入公式的第一个单元格,SHIFT+CTRL+方向键下,在编辑栏中输入公式,按下CTRL+回车: 2.先输入要填充的公式,按下SHIFT+CTRL+方向键下,再 ...

  2. 深入学习SpringBoot

    1. 快速上手SpringBoot 1.1 SpringBoot入门程序开发 SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程 1. ...

  3. 【安装文档】TRex流量分析仪保姆级安装指南--基于VMware虚拟机(ubantu18.04@Intel 82545EM)

    前言 既然你已经知道TRex并尝试搜索它的安装教程,这意味着你有一定的基础知识(至少知道自己需要什么).因此本文对于TRex的介绍部分会偏少 本次主要为TRex安装过程的一次记录(版本为v3.0.0) ...

  4. 2022年rhce最新认证—(满分通过)

    RHCE认证 重要配置信息 在考试期间,除了您就坐位置的台式机之外,还将使用多个虚拟系统.您不具有台式机系统的 root 访问权,但具有对虚拟系统的完整 root 访问权. 系统信息 在本考试期间,您 ...

  5. HDLBits答案——Circuits

    1 Combinational Logic 1.1 Basic Gates 1.1.1 Exams/m2014 q4h module top_module ( input in, output out ...

  6. CodeGeeX:vscode中全新的智能代码补全插件

    大家好我是费老师,代码智能补全是近几年非常热门的话题,有前不久宣告项目终结的kite,反响平平的tabnine,以及最近吃了一堆官司的copilot. 而广大从事编程工作的用户只关心市面上的代码智能补 ...

  7. 关于python3调用matplotlib中文乱码问题

    问题描述 我用来绘制柱形图,横坐标上面的数据, 但是网上大部分说的都是更改横纵坐标标签的乱码问题,而不是横坐标数据乱码问题 解决办法 更改横纵坐标上标签的中文不乱码 import matplotlib ...

  8. 将 Vue.js 项目部署至静态网站托管,并开启 Gzip 压缩

    摘要:关于使用 Nginx 开启静态网站 Gzip 压缩的教程已经有很多了,但是好像没几个讲怎么在对象存储的静态网站中开启 Gzip 压缩.其实也不复杂,我们一起来看下~ 本文分享自华为云社区< ...

  9. day31-JQuery04

    JQuery04 6.jQuery的DOM操作02 6.9常用遍历节点方法 取得匹配元素的所有子元素组成的集合:children(),该方法只考虑子元素而不考虑任何后代元素 取得匹配元素后面的同辈元素 ...

  10. 【每日一题】【动态规划&二分】2022年2月9日-NC91 最长上升子序列(三)

    描述给定数组 arr ,设长度为 n ,输出 arr 的最长上升子序列.(如果有多个答案,请输出其中 按数值(注:区别于按单个字符的ASCII码值)进行比较的 字典序最小的那个) 方法1:双层循环实现 ...