发现问题

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

当然不会拉!

我们来看看代码世界的:

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. android 代码分析

    1.@Override注解 @Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素. 作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否 ...

  2. 从0到1搭建redis6.0.7续更~

    "心有所向,日复一日,必有精进" 前言: 想必大家看完我之前写的搭建redis服务器,大家都已经把redis搭建起来了吧如果没有搭建起来的小可爱请移步这里哦从0到1搭建redis6 ...

  3. Node.js的学习(三)node.js 开发web后台服务

    一.Express -- Web开发框架 1.Express是什么? Express 是一个简洁而灵活.目前最流行的基于Node.js的Web开发框架, 提供了一系列强大特性帮助你创建各种 Web 应 ...

  4. 喜欢用Map却从未遭遇内存泄露的Java程序员上辈子都是神仙

    前言 点进来这篇文章的大概有两种人,一种是喜欢用Map的想看看自己是不是有可能也会踩雷,一种是不喜欢用Map的想进来看看那些喜欢用的人是怎么踩雷的. 那你要失望了,我只是单纯把公司最近代码审查时一个关 ...

  5. 学生管理系统Python

    student1=[ {1:'lucy','age':17,'sex':'n','Pnum':1111111}, {2:'tom','age':17,'sex':'m','Pnum':2222222} ...

  6. 【每日一题】【DFS/回溯】2022年1月1日-113. 路径总和 II

    给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径. 叶子节点 是指没有子节点的节点. 来源:力扣(LeetCode)链接 ...

  7. CORS与CSRF在Spring Security中的使用

    背景 在项目使用了Spring Security之后,很多接口无法访问了,从浏览器的网络调试窗看到的是CORS的报错和403的报错 分析 我们先来看一下CORS是什么,和它很相似的CSRF是什么,在S ...

  8. 常用BOM操作 DOM操作 事件 jQuery类库

    目录 BOM操作 常用BOM操作 三种弹出框 alert confirm prompt 定时任务 setTimeout 循环定时 setInterval DOM操作 查找标签 直接查找 间接查找 操作 ...

  9. 一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中自定义控件制作和调用、TCP协议下文件的收发 、以及可执行文件的打包

    一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中ui的使用和TCP协议下文件的收发.以及可执行文件的打包 写在前面,Qt Designer是一个非常操蛋的页面编辑器,它非常的...怎么说呢 ...

  10. Mattermost 笔记

    目录 部署 配置 客户端 桌面程序 Android 使用 扩展 Jenkins Hubot 机器人 Jira GitHub Mattermost 是一个开源.可私有化部署的在线通讯平台,可以和Gith ...