1.什么是异步?同步和异步主要是用来修饰方法的。当一个方法被调用时,调用者需要等待方法完成并返回,然后才能继续。我们称这种方法为同步方法;当一个方法被调用时,它立即返回并获得一个线程来执行该方法的内部业务。Call或者不等待方法完成,我们称这个方法为异步方法。异步的好处是它是非阻塞的(调用线程不会暂停执行等待子线程完成),所以我们设置一些不需要立即使用结果的耗时任务立即执行异步的,可以提高程序的运行效率。Net4.0在ThreadPool的基础上推出了Task类。微软强烈推荐使用Task来执行异步任务。现在C#类库中的异步方法基本都是用Task;net5.0推出了async/await,让异步编程更简单。为了方便。本篇主要介绍Task和async/await相关的内容,其他异步操作方法将在下篇介绍。2、Task介绍Task是在ThreadPool的基础上发起的。让我们简单了解一下ThreadPool。ThreadPool中有一定数量的线程。如果有任务需要处理,就会从线程池中获取一个空闲线程来执行该任务。任务执行完毕后,线程不会被销毁,而会被线程池回收,用于后续任务。当线程池中的所有线程都处于忙碌状态,并且有新的任务需要处理时,线程池会创建一个新的线程来处理该任务。如果线程数达到设置的最大值,则任务会进入队列等待其他任务释放线程后执行。线程池可以减少线程的创建,节省开销。我们来看一个ThreadPool的栗子。(obj)=>{Console.WriteLine($"{obj}thexecutiontask");}),i);}Console.ReadKey();}以上代码通过ThreadPool执行了10个任务,执行结果为:与Thread相比,ThreadPool可以减少线程的创建,有效降低系统开销;但是ThreadPool无法控制线程的执行顺序,我们无法获得线程池中线程取消/异常/完成的通知,即我们无法有效监控和控制线程池中的Threads。1任务的创建和运行我们知道ThreadPool的缺点:我们无法控制线程池中线程的执行顺序,也无法获得线程池中线程取消/异常/完成的通知。Net4.0在ThreadPool的基础上引入了Task。Task既有线程池的优点,又解决了使用线程池不易控制的缺点。我们先来看看如何创建和运行一个Task。创建和执行Task有3种方式:staticvoidMain(string[]args){//1。新的Task实例化方式,需要通过Start方法task=newTask(()=>{Thread.Sleep(100);Console.WriteLine($"你好,task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");});task.Start();//2.任务。Factory.StartNew(Actionaction)创建并启动一个TaskTasktask2=Task.Factory.StartNew(()=>{Thread.Sleep(100);Console.WriteLine($"你好,task2的线程ID是{Thread.CurrentThread.ManagedThreadId}");});//3.Task.Run(Actionaction)将任务放入线程池队列,返回并启动一个TaskTasktask3=Task.Run(()=>{Thread.Sleep(100);Console.WriteLine($"你好,task3的线程ID是{Thread.CurrentThread.ManagedThreadId}");});Console.WriteLine("执行主线程!");Console.ReadKey();}执行结果如下如下:我们看到先打印“Executethemainthread”,然后打印每个task,说明Task不会阻塞主线程。上面的栗子都没有返回值。我们还可以创建一个有返回值的任务。用法和没有返回值基本一样。我们简单修改一下上面的栗子。代码如下:staticvoidMain(string[]args){////1。新的Task实例化方式,需要启动Tasktask=newTask(()=>{return$"你好,task1的ID是{Thread.CurrentThread.ManagedThreadId}";});task.Start();////2.Task.Factory.StartNew(Funcfunc)创建并启动一个TaskTasktask2=Task.Factory.StartNew(()=>{return$"hello,IDoftask2is{Thread.CurrentThread.ManagedThreadId}";});////3.Task.Run(Funcfunc)将任务放入线程池队列,返回并启动一个TaskTasktask3=Task.Run(()=>{return$"你好,task3的ID是{Thread.CurrentThread.ManagedThreadId}";});Console.WriteLine("执行主线程!");Console.WriteLine(task.Result);Console.WriteLine(task2.Result);Console.WriteLine(task3.Result);Console.ReadKey();}注意task.Resut在获取结果时会阻塞线程,即如果任务没有完成,它会等待任务完成并获取GotoResult,然后执行下面的代码。程序运行结果如下:上面所有栗子中Task的执行都是异步的,不会阻塞主线程。在某些场景下,我们想让Task同步执行怎么办?Task提供了task.RunSynchronously()来同步执行Task任务。代码如下:staticvoidMain(string[]args){Tasktask=newTask(()=>{Thread.Sleep(100);Console.WriteLine("EndofexecutingTask!");});//同步执行,task会阻塞主线程task.RunSynchronously();Console.WriteLine("Endofexecutingthemainthread!");Console.ReadKey();}执行结果如下:2Task阻塞方法(Wait/WaitAll/WaitAny)1Thread阻塞线程方法在使用Thread时,我们知道thread.Join()方法可以阻塞主线程。看一个例子:staticvoidMain(string[]args){Threadth1=newThread(()=>{Thread.Sleep(500);Console.WriteLine("Thread1isfinished!");});th1.Start();Threadth2=newThread(()=>{Thread.Sleep(1000);Console.WriteLine("线程2结束了!");});th2.Start();//阻塞主线程th1.Join();th2.Join();Console.WriteLine("主线程结束了!");Console.ReadKey();}如果把这两个Join注释掉,执行结果是:先打印[主??线程结束],加上两个Join方法后的执行结果如下,实现了线程阻塞:2.Task的Wait/WaitAny/WaitAll方法。调用一次Join方法;②如果我们想在所有线程都执行完(或者任意一个线程执行完)的时候立即解除阻塞,使用Join方法不太好。Task提供了Wait/WaitAny/WaitAll方法,可以更方便的控制线程阻塞。task.Wait()表示等待任务执行,功能类似于ad.Join();Task.WaitAll(Task[]tasks)表示所有任务都执行完才解除阻塞;Task.WaitAny(Task[]tasks)表示只要一个task执行完,就会解除阻塞。看个栗子:staticvoidMain(string[]args){Tasktask1=newTask(()=>{Thread.Sleep(500);Console.WriteLine("线程1结束!");});task1.Start();Tasktask2=newTask(()=>{Thread.Sleep(1000);Console.WriteLine("线程2结束了!");});task2.Start();//阻塞主线程。task1和task2执行完后,执行主线程 //执行[task1.Wait();task2.Wait();]实现同样的功能Task.WaitAll(newTask[]{task1,task2});安慰。WriteLine("主线程执行完毕!");Console.ReadKey();}执行结果如下:如果把栗子中的WaitAll换成WaitAny,那么任何任务执行完都会解阻塞线程,执行结果为:先打印[线程1执行完成],然后打印[主线程执行完成],最后打印[线程2执行完成]3Task(WhenAny/WhenAll/ContinueWith)继续操作上面的Wait/WaitAny/WaitAll方法返回值为void,这些方法简单实现阻塞线程。现在我们想在所有任务执行完(或者任意一个任务执行完)后开始执行后续操作,如何实现呢?这时候就可以使用WhenAny/WhenAll方法,这些方法执行后返回一个任务实例。task.WhenAll(Task[]tasks)表示所有task都执行完之后再进行后续操作,task.WhenAny(Task[]tasks)表示任意一个task执行完之后,再进行后续操作。看个栗子:staticvoidMain(string[]args){Tasktask1=newTask(()=>{Thread.Sleep(500);Console.WriteLine("线程1执行完成!");});task1.Start();Tasktask2=newTask(()=>{Thread.Sleep(1000);Console.WriteLine("Thread2isfinished!");});task2.Start();//task1,执行task2后执行后续操作Task.WhenAll(task1,task2).ContinueWith((t)=>{Thread.Sleep(100);Console.WriteLine("后续操作完成!");});Console.WriteLine("主线程完成了!");Console.ReadKey();}执行结果如下。我们可以看到WhenAll/WhenAny方法不会阻塞主线程。使用WhenAll方法时,所有的任务都会执行完,然后再执行后续操作;如果把栗子中的WhenAll换成WhenAny的话,只要一个线程执行完,就会开始执行后续操作,这里不做演示。上面的栗子也可以通过Task.Factory.ContinueWhenAll(Task[]tasks,ActioncontinuationAction)和Task.Factory.ContinueWhenAny(Task[]tasks,ActioncontinuationAction)实现,修改上面栗子的代码如下,执行结果不变。staticvoidMain(string[]args){Tasktask1=newTask(()=>{Thread.Sleep(500);Console.WriteLine("Thread1isfinished!");});task1.Start();Tasktask2=newTask(()=>{Thread.Sleep(1000);Console.WriteLine("线程2执行完毕!");});task2.Start();//Task.Factory.ContinueWhenAll(newTask[]{task1执行完毕通过TaskFactroy,task2},(t)=>{Thread.Sleep(100);Console.WriteLine("执行后续操作");});Console.WriteLine("主线程执行完毕!");Console.ReadKey();}4任务取消(CancellationTokenSource)1线程取消任务执行。在Task之前,我们使用Thread来执行任务。Thread如何取消任务?大致流程是:设置一个变量来控制任务是否停止,比如设置一个变量isStop,然后线程轮询检查isStop。如果isStop为真,则停止。代码如下:staticvoidMain(string[]args){boolisStop=false;intindex=0;//启动一个线程执行任务Threadth1=newThread(()=>{while(!isStop){Thread.Sleep(1000);Console.WriteLine($"{++index}execution,threadrunning...");}});th1.Start();//五秒后取消任务执行Thread.Sleep(5000);isStop=true;Console.ReadKey();}2Task取消任务执行Task有个专门的类CancellationTokenSource来取消任务执行,还是用上面的例子,我们修改代码如下,程序运行的效果不变.staticvoidMain(string[]args){CancellationTokenSourcesource=newCancellationTokenSource();intindex=0;//启动一个任务执行任务Tasktask1=newTask(()=>{while(!source.IsCancellationRequested){Thread.Sleep(1000);Console.WriteLine($"{++index}execution,threadrunning...");}});task1.Start();//五秒后取消任务执行Thread.Sleep(5000);//source.Cancel()方法请求取消任务,IsCancellationRequested会变为true(5000)实现5秒后自动取消任务,也可以通过source.Token.Register(Actionaction)注册取消任务触发的回调函数,即注册的action会在任务执行时执行取消。看一个栗子:staticvoidMain(string[]args){CancellationTokenSourcesource=newCancellationTokenSource();//注册任务取消的事件source.Token.Register(()=>{Console.WriteLine("执行任务后执行xx操作iscancelled!");});intindex=0;//开启一个任务执行任务Tasktask1=newTask(()=>{while(!source.IsCancellationRequested){Thread.Sleep(1000);Console.WriteLine($"第{++index}次执行,线程正在运行...");}});task1.Start();//延迟取消,效果等同于Thread.Sleep(5000);source.Cancel();source.CancelAfter(5000);Console.ReadKey();}执行结果如下。取消回调后打印第五次执行。这是因为第五个任务取消执行时已经通过了while()判断,任务已经执行完毕:最后看前面的跨线程栗子,点击按钮启动一个任务,赋值给tetxtbox,我们把Thread改成Task,代码如下:i)=>{myTxtbox.Text=i.ToString();};for(inti=0;i<1000000;i++){myTxtbox.Invoke(setValue,i);}});}}运行界面如下如下,分配的任务不会阻塞UI线程:回到顶部3.C#5.0中出现的异步方法async和await(async/await)让异步编程变得更简单。我们看一个获取文件内容的栗子:classProgram{staticvoidMain(string[]args){stringcontent=GetContentAsync(Environment.CurrentDirectory+@"/test.txt").Result;//调用同步方法//stringcontent=GetContent(Environment.CurrentDirectory+@"/test.txt");Console.WriteLine(content);Console.ReadKey();}//异步读取文件内容asyncstaticTaskGetContentAsync(stringfilename){FileStreamfs=newFileStream(filename,FileMode.Open);varbytes=newbyte[fs.Length];//ReadAsync方法异步读取内容,不阻塞线程Console.WriteLine("开始读取文件");intlen=awaitfs.ReadAsync(bytes,0,bytes.Length);stringresult=Encoding.UTF8.GetString(bytes);returnresult;}//同步读取文件内容staticstringGetContent(stringfilename){FileStreamfs=newFileStream(filename,FileMode.Open);varbytes=newbyte[fs.Length];//读取方法同步读取内容,阻塞线程intlen=fs.Read(bytes,0,bytes.Length);stringresult=Encoding.UTF8.GetString(bytes);returnresult;}}test.txt内容为【helloworld!】执行结果为:上面的栗子也写了同步读取的方法,去掉即可同步读取文件内容主函数中的注释。我们可以看到异步读取的代码和同步读取的代码基本是一样的。async/await使异步编码更容易,我们可以像编写同步代码一样编写异步代码。注意一个小问题:异步方法中方法签名的返回值为Task,代码中的返回值为T。上面栗子中GetContentAsync的签名返回值为Task,而在代码是字符串。在分析异步代码时,记住这个细节是有帮助的。异步方法签名的返回值有以下三种类型:①Task:如果调用方法想通过调用异步方法获取类型为T的返回值,那么签名必须是Task;②Task:如果调用方法不想通过异步方法获取值,只想跟踪异步方法的执行状态,那么我们可以将异步方法签名的返回值设置为Task;③void:如果调用方法只是调用异步方法,不与异步方法交互,我们可以设置异步方法签名返回值为void,这种形式也称为“callandforget”。总结:至此,Task和async/await的简单使用基本结束,工作中遇到一些高级特性再研究。通过上面的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool封装的改进,主要是为了更有效地控制线程池中的线程(ThreadPool中的线程,我们很难控制它的执行通过代码序列、任务继续和取消等);ThreadPool基于Thread,主要目的是减少Thread的创建数量和管理Thread的成本。async/awaitTask在C#中比较高级,也是微软大力推广的一个特性。我们在开发中可以尝试使用Task来代替Thread/ThreadPool。处理本地IO和网络IO任务,我们应该尽量使用async/await来提高任务执行效率。.本文转载自微信公众号《CSharp编程百科全书》,可通过以下二维码关注。转载本文请联系CSharp编程百科公众号。