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 以及对线程相关知识的复习的更多相关文章

  1. 浅谈ES6中的Async函数

    转载地址:https://www.cnblogs.com/sghy/p/7987640.html 定义:Async函数是一个异步操作函数,本质上,Async函数是Generator函数的语法糖.asy ...

  2. 理解C#中的 async await

    前言 一个老掉牙的话题,园子里的相关优秀文章已经有很多了,我写这篇文章完全是想以自己的思维方式来谈一谈自己的理解.(PS:文中涉及到了大量反编译源码,需要静下心来细细品味) 从简单开始 为了更容易理解 ...

  3. 浅谈Java中的equals和==(转)

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...

  4. [C#] .NET4.0中使用4.5中的 async/await 功能实现异

    好东西需要分享 原文出自:http://www.itnose.net/detail/6091186.html 在.NET Framework 4.5中添加了新的异步操作库,但是在.NET Framew ...

  5. 浅谈Linux中的信号处理机制(二)

    首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...

  6. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  7. 浅谈Java中的equals和==

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...

  8. 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。

    [TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...

  9. 转【】浅谈sql中的in与not in,exists与not exists的区别_

    浅谈sql中的in与not in,exists与not exists的区别   1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...

随机推荐

  1. Vue笔记:VS Code 常用快捷键

    VS Code 常用快捷键 1.注释: 单行注释:ctrl+/, 注释后再按取消 取消单行注释:alt+shift+A 注释后再按取消 2.移动行 向上移动一行:alt+up 向下移动一行:alt+d ...

  2. 详解Java中的final关键字

    本文原文地址:https://jiang-hao.com/articles/2019/coding-java-final-keyword.html1 final 简介2 final关键字可用于多个场景 ...

  3. Application Metrics With Spring Boot Actuator

    转自:https://bartcode.co.uk/2015/01/application-metrics-with-spring-boot-actuator Update 12/2017: It w ...

  4. Python模块: 文件和目录os+shutil

    一 常用函数 os模块 os.sep 表示默认的文件路径分隔符,windows为\, linux为/os.walk(spath): 用来遍历目录下的文件和子目录os.listdir(dirname): ...

  5. 版本管理(二)之Git和GitHub的连接和使用

    首先需要注册登录GitHub:https://github.com 然后 ①:下载Git 先从Git官网,由于我的系统是64位的所以选择64-bit Git for Windows Setup htt ...

  6. 阿里巴巴java手册示例

    package com.led.daorumysql; /** * @Description:alibaba java development manual * @author 86157 * */ ...

  7. [转]一分钟告诉你究竟DevOps是什么鬼?

    本文转自:https://www.cnblogs.com/jetzhang/p/6068773.html 一分钟告诉你究竟DevOps是什么鬼?   历史回顾 为了能够更好的理解什么是DevOps,我 ...

  8. RDLC报表带搜索与传参数功能演示(ASP.NET MVC)

    昨晚有演示了<ASP.NET MVC应用程序展示RDLC报表>http://www.cnblogs.com/insus/p/3665295.html RDLC报表.在实现过程中,有遇上了诸 ...

  9. 解决MVC应用程序数据重复加载问题

    先来看看这个动画: 这是使用jQuery来实现数据加载,每点击一次,数据就加载一次.这源程序与实现来自<MVC应用程序JsonResult()的练习>http://www.cnblogs. ...

  10. 分分钟弄明白UML中泛化 , 实现 , 关联, 聚合, 组合, 依赖

    在UML类图中,常见的有以下几种关系: 泛化(Generalization),  实现(Realization), 关联(Association), 聚合(Aggregation), 组合(Compo ...