浅谈C#中的 async await 以及对线程相关知识的复习
C#5.0以后新增了一个语法糖,那就是异步方法async await,之前对线程,进程方面的知识有过较为深入的学习,大概知道这个概念,我的项目中实际用到C#异步编程的场景比较少,就算要用到一般也感觉Task类也基本够用了,所以没有稍微仔细的去研究过这个语法,今天借工作闲暇来梳理一下这个知识点,顺便复习一下线程相关方面的知识,要搞懂这个知识点,需要有一定的基础知识,首先要知道,什么是线程,什么是同步,什么是异步
1. 线程?异步?同步?
什么是线程?规范定义说线程是程序执行流的最小单元,同一时间内只能做一件事情,请说人话,啥意思,举个栗子,假如把人比作一个多线程的软件(此处举例只是为了便于理解,实际中的人脑应该是多进程的),现在这个软件需要做一件叫做吃饭的任务,那么在同一时间内就开启了一个负责吃饭的线程,那这个线程就只能吃饭,不能做其他的事情,如果他想在吃饭的时候“边吃饭边看电视”,就要启用另一个线程,一个线程负责吃饭,一个线程负责看电视。在这里我为啥要把边吃饭边看电视打上引号?因为实际的多线程执行机制中,“边吃饭边看电视”这个两个任务并不是两个任务各在同一时间各自执行,而是快速且交替执行的,也就是两个任务在同一时间只能执行其中一个任务,用一张图来便于理解两个线程如何快速且交替执行,这种线程的执行方式叫做并发执行,而多线程同时执行只是系统带来的一个假像,它在多个单位时间内进行多个线程的切换。因为切换频密而且单位时间非常短暂,所以多线程可被视作同时运行。

下面看一段代码
static void Main(string[] args)
{
//创建 th1 th2两个线程,分别输出字母e和字母w,模拟执行吃饭和看电视两个任务
Thread th1 = new Thread(() => {
for (int i = ; i < ; i++)
{
Console.Write("e");//输出eat
}
});
Thread th2 = new Thread(() => {
for (int i = ; i < ; i++)
{
Console.Write("w");//输出watch
}
});
th1.Start();//启动th1线程
th2.Start();//启动th2线程
Console.ReadKey();
}
我们用for循环输出不同的字母来模拟把两个线程执行的任务碎片化最后看如下执行结果

可以看到输出结果字母e和w是不规则交替输出的,当然你可以使将for循环的i的最大设置的更大一些,来使执行效果更明显,如果你在两个线程内部打上断点,最后运行发现,两个线程在不断的交替执行。
能理解线程之间在同一时间内是并发执行的,那么也就不难理解什么是同步,什么是异步了,同步执行可以理解为代码一行一行按照顺序执行,即完成吃饭的任务后才可以执行看电视,所以一般情况下同步任务,一个线程就可以搞定,异步执行即两个任务的代码代码是交替执行,即吃饭和看电视两个任务是交替运行的,一个线程执行吃饭,一个线程执行看电视,所以不难理解一般涉及到同步异步的东西基本都会跟多线程挂钩。
异步编程的优点:合理的运用异步编程能极大的提升我们的代码运行效率,举一个简单的运用场景,比如界面加载数据渲染时,加载某个模块比较耗时,如果运用同步执行可能会出现加载耗时模块时界面卡死的情况,我们就可以把耗时的模块放在异步任务中,这样就不会影响其他模块的正常加载。ajax应该就是最常见的一个异步编程场景
异步编程是否一定就比同步编程好?
首先代码异步执行CPU需要花费不少的时间在线程的切换上,线程切换也有细微的性能损耗,所以过多地使用多线程反而会导致程序性能的下降。
而且不合理使用线程会造成线程冲突,下面代码我们分别用两个线程去对变量a进行十次++和五次--操作,然后多次运行这个程序,发现每次输出a的结果可能是不一样的,这就是两个线程代码执行顺序的不确定性导致每次执行算出的a的结果都不一样,也就是所谓的异步执行导致线程之间共享数据的冲突
static void Main(string[] args)
{
int a = ;
Thread th1 = new Thread(() => {
for (int i = ; i < ; i++)
{
a++;
}
});
Thread th2 = new Thread(() => {
for (int i = ; i < ; i++)
{
a--;
}
});
th1.Start();
th2.Start();
Console.Write(a);
Console.ReadKey();
}
2. Thread vs Task
上面讲了这么多关于线程,同步,异步的东西,似乎不用 async await语法我们也能实现异步编程,那么 async await语法与传统的ThreadPool.QueueUserWorkItem启动线程(也就是上面的Thread)有什么区别呢? async await的好处又在哪呢?
上面的用代码传统的Thread方法启动线程虽然感觉很简单方便,但是我们我们仔细查看Thread类的构造函数的参数,发现Thread构造函数中所传递的委托是无参数,无返回值的,所以Thread所执行的异步函数的是没有参数,也没有返回结果的,这无疑是个很大的限制

而且对比Thread(线程)和async中Task(任务),二者之间有如下区别
1、任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制,所以Task的控制性和灵活性也是比Thread要好的。
那么在还没有async await语法之前,我们如何解决Thread没有返回值这个问题呢?我们可以运用下面方法,通过委托中的BeginInvoke执行异步代码,然后通过EndInvoke获取异步代码的返回结果,这种异步执行代码的方式可以说是比较灵活了,我们执行异步方法的参数和返回值都是可以变的
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
Func<string, string> fuc = new Func<string, string>(Hello);
//异步调用委托,获取计算结果
IAsyncResult result = fuc.BeginInvoke("AsyncWork", null, null);
//完成主线程其他工作
Console.WriteLine("主线程执行常规任务.....");
//等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果
string data = fuc.EndInvoke(result);
Console.WriteLine(data); Console.ReadKey();
} static string Hello(string name)
{
ThreadMessage("Async Thread After Two Seconds");
Thread.Sleep(); //模拟异步耗时工作
return "Hello " + name;
} //显示当前线程,输出当前线程的线程ID
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data, Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
3. async await
前面已经讲了相关知识做铺垫。现在来看看async await 关键字,我们来写一个简单的例子,单纯只使用async关键字不使用 await
static void Main(string[] args)
{
DoSomethingAsync();
DoSomethingSync();
Console.ReadKey();
} static void DoSomethingSync()
{
//循环模拟耗时运算
for (int i = ; i < ; i++)
{
Console.Write("*");
}
Console.WriteLine("\nResult: *,我是主线程代码,线程ID:" + Thread.CurrentThread.ManagedThreadId);
} static async void DoSomethingAsync()
{
//循环模拟耗时运算
for (int i = ; i < ; i++)
{
Console.Write("/");
}
Console.WriteLine("\nResult: +,我是async await关键字执行的代码, 线程ID: " + Thread.CurrentThread.ManagedThreadId);
}
运行结果如下,可以看到,DoSomethingAsync()和DoSomethingSync()两个函数在同一个线程上同步执行,所以我们得到一个结论,那就是如果一个异步方法只使用async而不使用await,那么这个所谓的异步方法其实和一个普通的方法没有什么区别,并不会异步执行代码

那我们修改上面的代码,在函数里面加上一个await关键字,会有怎样的结果呢,在使用await之前我们有几个需要注意的点
1:await无法等待void,也就是await后面所等待的函数必须要有返回值
2:所有异步函数返回值必须是void,Task和Task<T>类型,有泛型的存在,(自 C# 7后,貌似还可以指定其他任何返回类型,前提是返回类型包含 GetAwaiter 方法。),所以使用async await时我们也不用担心异步代码无法获取返回值的问题
可以看下面代码分别执行普通函数,Thread启动的函数,和用async await的异步函数
static void Main(string[] args)
{
//async await执行的异步函数
DoSomethingAsync();
//Thread启动的异步函数
DoSomethingAsync2();
//常规同步函数
DoSomethingSync();
Console.ReadKey();
} public static void DoSomethingSync()
{
//循环模拟耗时运算
for (int i = ; i < ; i++)
{
Console.Write("*");
}
Console.WriteLine("\nResult: *,我是主线程代码,线程ID:" + Thread.CurrentThread.ManagedThreadId);
} private static async void DoSomethingAsync()
{
//此处输出执行的任然是主线程代码
Console.WriteLine("\nResult: +,我是只使用async关键字执行的代码, 线程ID: " + Thread.CurrentThread.ManagedThreadId);
//通过await来告诉编译器此处要执行异步代码,所以await后面执行的DoAsync方法是另一线程的异步代码
string msg = await DoAsync();
Console.WriteLine("\nResult: +," + msg);
} private static async Task<string> DoAsync()
{
await Task.Run(() => {
//循环模拟耗时运算
for (int i = ; i < ; i++)
{
Console.Write("+");
}
});
return "我是async await关键字执行的异步代码,线程ID:" + Thread.CurrentThread.ManagedThreadId;
} private static void DoSomethingAsync2()
{
//常规Thread启动新的线程
string msg = string.Empty;
Thread th = new Thread(() => {
//循环模拟耗时运算
for (int i = ; i < ; i++)
{
Console.Write("-");
}
msg = "我是Thread启动的异步代码,线程ID:" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("\nResult: -," + msg);
});
th.Start();
}
其实运行代码我们不难发现,所谓的async await异步函数真正实现异步效果的其实还是Task类,async 和 await 关键字本身并不会创建其他线程,而为什么要用async await,而不是单纯的只使用Task,我的个人看法是如果在异步编程中,用async await配合 Task.Run 将占用大量 CPU 的工作移到后台线程,这样会使代码执行效率相对变高,标记的异步方法使用await来指定暂停点这样写也会使代码相对简洁清晰,我们可以清晰的看的哪里是异步执行的代码,哪里是同步执行的代码,也无需防止争用条件,可以理解为async await是微软对我们异步编程的一种规范。
最后再来实战一个async配合使用委托的代码实战,我们来写一段简化版的代码,来模拟类似asp core框架中间件的调用和注册过程,虽然实际的asp core框架的中间件的调用和注册过程是很复杂的,但是大概代码原理也就和下面差不多,多个中间件管道顺序调用并共同维护同一个HttpContext上下文类
delegate Task RequestDelegate(string context);
public class Program
{
private static readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
public static void Main()
{
//注册中间件
_middlewares.Add(MiddlewareHello);
_middlewares.Add(MiddlewareGoodbye);
_middlewares.Add(MiddlewareOK);
RequestDelegate del = async context =>
{
await Task.CompletedTask;
};
_middlewares.Reverse();
//遍历并调用中间件
foreach (var item in _middlewares)
{
del = item(del);
}
del("kangkang");
System.Console.ReadKey();
}
static RequestDelegate MiddlewareHello(RequestDelegate del)
{
return async context =>
{
System.Console.WriteLine(" Hello, {0}!", context);
await del(context);
};
}
static RequestDelegate MiddlewareGoodbye(RequestDelegate del)
{
return async context =>
{
System.Console.WriteLine(" Goodbye, {0}!", context);
await del(context);
};
}
static RequestDelegate MiddlewareOK(RequestDelegate del)
{
return async context =>
{
System.Console.WriteLine(" SayOK, {0}!", context);
};
}
}
浅谈C#中的 async await 以及对线程相关知识的复习的更多相关文章
- 浅谈ES6中的Async函数
转载地址:https://www.cnblogs.com/sghy/p/7987640.html 定义:Async函数是一个异步操作函数,本质上,Async函数是Generator函数的语法糖.asy ...
- 理解C#中的 async await
前言 一个老掉牙的话题,园子里的相关优秀文章已经有很多了,我写这篇文章完全是想以自己的思维方式来谈一谈自己的理解.(PS:文中涉及到了大量反编译源码,需要静下心来细细品味) 从简单开始 为了更容易理解 ...
- 浅谈Java中的equals和==(转)
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...
- [C#] .NET4.0中使用4.5中的 async/await 功能实现异
好东西需要分享 原文出自:http://www.itnose.net/detail/6091186.html 在.NET Framework 4.5中添加了新的异步操作库,但是在.NET Framew ...
- 浅谈Linux中的信号处理机制(二)
首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...
- 浅谈Java中的对象和引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- 浅谈Java中的equals和==
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...
- 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。
[TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...
- 转【】浅谈sql中的in与not in,exists与not exists的区别_
浅谈sql中的in与not in,exists与not exists的区别 1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...
随机推荐
- h5实现输入框fixed定位在屏幕最底部兼容性
1.问题由来 做h5 已经有很长一段时间了,现在做的工作h5比pc上的更多,曾经解决pc端IE各个版本的兼容性也是伤透脑筋,原以为h5的会更好,殊不知,还有更头疼的问题,当设计师要设计一个聊天窗口,把 ...
- 如何恢复windows的exe文件的默认打开方式
事情由来: 有一次在用一个播放器选择打开视频文件的时候,意外的手贱点击打来了 exe 文件,之后戏剧性的恶心开始了,首先当然是打开失败,接着整个桌面的 exe 文件全部被替换成那个播放器的图标,然后所 ...
- guava EventBus 消息总线的运用
public class Test { public static void main(String[] args) { final EventBus eventBus = new EventBus( ...
- 技术笔记5 MINA 和事务
Java NIO框架MINA用netty性能和链接数.并发等压力测试参数好于mina. 特点:1.NIO弥补了原来的I/O的不足,它再标准java代码中提供了高速和面向块的I/O原力的I/O库与NIO ...
- 【Leetcode】292. Nim游戏
题目链接:https://leetcode-cn.com/problems/nim-game/description/ 您和您的朋友,两个人一起玩 Nim游戏:桌子上有一堆石头,每次你们轮流拿掉 1 ...
- C#中HttpWebRequest的用法详解
原文链接:http://www.cnblogs.com/love201314/p/5029312.html 1.HttpWebRequest和HttpWebResponse类是用于发送和接收HTTP数 ...
- C++ 小知识点 WINAPI
int WINAPI WINMain 中,WINAPI含义 网友给出回答:在windef.h头文件中有如下定义#define WINAPI __stdcall#define APIENTRY ...
- 常用的7个SQl优化技巧
作为程序员经常和数据库打交道的时候还是非常频繁的,掌握住一些Sql的优化技巧还是非常有必要的.下面列出一些常用的SQl优化技巧,感兴趣的朋友可以了解一下. 1.注意通配符中Like的使用 以下写法会造 ...
- MyBatis动态添加—trim标签
做添加时,部分字段有值,没值的字段不添加,这就是动态添加,使用 trim 标签就可以实现. <insert id="insertSysUser" parameterType= ...
- Mybatis的自动映射autoMappingBehavior与mapUnderscoreToCamelCase
autoMappingBehavior 在Mybatis的配置文件中添加settings属性的autoMappingBehavior <settings> <setting name ...