C#多线程和异步之Task和async/await详解

开发 后端
同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

 [[381489]]

一、什么是异步

同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。

二、Task介绍

Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一个ThreadPool的栗子吧

  1. static void Main(string[] args) 
  2.         { 
  3.             for (int i = 1; i <=10; i++) 
  4.             { 
  5.                 //ThreadPool执行任务 
  6.                 ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { 
  7.                     Console.WriteLine($"第{obj}个执行任务"); 
  8.                 }),i); 
  9.             } 
  10.             Console.ReadKey(); 
  11.         } 

上边的代码通过ThreadPool执行了10个任务,执行结果为:

ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。

1 Task创建和运行

我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。

首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:

  1. static void Main(string[] args) 
  2.         { 
  3.             //1.new方式实例化一个Task,需要通过Start方法启动 
  4.             Task task = new Task(() => 
  5.             { 
  6.                 Thread.Sleep(100); 
  7.                 Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}"); 
  8.             }); 
  9.             task.Start(); 
  10.  
  11.             //2.Task.Factory.StartNew(Action action)创建和启动一个Task 
  12.             Task task2 = Task.Factory.StartNew(() => 
  13.               { 
  14.                   Thread.Sleep(100); 
  15.                   Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); 
  16.               }); 
  17.  
  18.             //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task 
  19.             Task task3 = Task.Run(() => 
  20.               { 
  21.                   Thread.Sleep(100); 
  22.                   Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); 
  23.               }); 
  24.             Console.WriteLine("执行主线程!"); 
  25.             Console.ReadKey(); 
  26.         } 

执行结果如下:

我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的栗子Task都没有返回值,我们也可以创建有返回值的Task ,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:

  1. static void Main(string[] args) 
  2.         { 
  3.             ////1.new方式实例化一个Task,需要通过Start方法启动 
  4.             Task<string> task = new Task<string>(() => 
  5.             { 
  6.                 return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}"
  7.             }); 
  8.             task.Start(); 
  9.  
  10.             ////2.Task.Factory.StartNew(Func func)创建和启动一个Task 
  11.            Task<string> task2 =Task.Factory.StartNew<string>(() => 
  12.             { 
  13.                 return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}"
  14.             }); 
  15.  
  16.             ////3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task 
  17.            Task<string> task3= Task.Run<string>(() => 
  18.             { 
  19.                 return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}"
  20.             }); 
  21.  
  22.             Console.WriteLine("执行主线程!"); 
  23.             Console.WriteLine(task.Result); 
  24.             Console.WriteLine(task2.Result); 
  25.             Console.WriteLine(task3.Result); 
  26.             Console.ReadKey(); 
  27.         } 

注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:

上边的所有栗子中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?Task提供了 task.RunSynchronously()用于同步执行Task任务,代码如下:

  1. static void Main(string[] args) 
  2.         { 
  3.             Task task = new Task(() => 
  4.             { 
  5.                 Thread.Sleep(100); 
  6.                 Console.WriteLine("执行Task结束!"); 
  7.             }); 
  8.             //同步执行,task会阻塞主线程 
  9.             task.RunSynchronously(); 
  10.             Console.WriteLine("执行主线程结束!"); 
  11.             Console.ReadKey(); 
  12.         } 

执行结果如下:

2 Task的阻塞方法(Wait/WaitAll/WaitAny)

1 Thread阻塞线程的方法

使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:

  1. static void Main(string[] args) 
  2.         { 
  3.             Thread th1 = new Thread(() => { 
  4.                 Thread.Sleep(500); 
  5.                 Console.WriteLine("线程1执行完毕!"); 
  6.             }); 
  7.             th1.Start(); 
  8.             Thread th2 = new Thread(() => { 
  9.                 Thread.Sleep(1000); 
  10.                 Console.WriteLine("线程2执行完毕!"); 
  11.             }); 
  12.             th2.Start(); 
  13.             //阻塞主线程 
  14.             th1.Join(); 
  15.             th2.Join(); 
  16.             Console.WriteLine("主线程执行完毕!"); 
  17.             Console.ReadKey(); 
  18.         } 

如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:

2 Task的Wait/WaitAny/WaitAll方法

Thread的Join方法可以阻塞调用线程,但是有一些弊端:①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。

task.Wait() 表示等待task执行完毕,功能类似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看一个栗子:

  1. static void Main(string[] args) 
  2.         { 
  3.             Task task1 = new Task(() => { 
  4.                 Thread.Sleep(500); 
  5.                 Console.WriteLine("线程1执行完毕!"); 
  6.             }); 
  7.             task1.Start(); 
  8.             Task task2 = new Task(() => { 
  9.                 Thread.Sleep(1000); 
  10.                 Console.WriteLine("线程2执行完毕!"); 
  11.             }); 
  12.             task2.Start(); 
  13.             //阻塞主线程。task1,task2都执行完毕再执行主线程 
  14.        //执行【task1.Wait();task2.Wait();】可以实现相同功能 
  15.             Task.WaitAll(new Task[]{ task1,task2}); 
  16.             Console.WriteLine("主线程执行完毕!"); 
  17.             Console.ReadKey(); 
  18.         } 

执行结果如下:

如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】

3 Task的延续操作(WhenAny/WhenAll/ContinueWith)

上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。看一个栗子:

 

  1. static void Main(string[] args) 
  2.         { 
  3.             Task task1 = new Task(() => { 
  4.                 Thread.Sleep(500); 
  5.                 Console.WriteLine("线程1执行完毕!"); 
  6.             }); 
  7.             task1.Start(); 
  8.             Task task2 = new Task(() => { 
  9.                 Thread.Sleep(1000); 
  10.                 Console.WriteLine("线程2执行完毕!"); 
  11.             }); 
  12.             task2.Start(); 
  13.             //task1,task2执行完了后执行后续操作 
  14.             Task.WhenAll(task1, task2).ContinueWith((t) => { 
  15.                 Thread.Sleep(100); 
  16.                 Console.WriteLine("执行后续操作完毕!"); 
  17.             }); 
  18.  
  19.             Console.WriteLine("主线程执行完毕!"); 
  20.             Console.ReadKey(); 
  21.         } 

执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。

上边的栗子也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边栗子代码如下,执行结果不变。

  1. static void Main(string[] args) 
  2.         { 
  3.             Task task1 = new Task(() => { 
  4.                 Thread.Sleep(500); 
  5.                 Console.WriteLine("线程1执行完毕!"); 
  6.             }); 
  7.             task1.Start(); 
  8.             Task task2 = new Task(() => { 
  9.                 Thread.Sleep(1000); 
  10.                 Console.WriteLine("线程2执行完毕!"); 
  11.             }); 
  12.             task2.Start(); 
  13.             //通过TaskFactroy实现 
  14.             Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => 
  15.             { 
  16.                 Thread.Sleep(100); 
  17.                 Console.WriteLine("执行后续操作"); 
  18.             }); 
  19.  
  20.             Console.WriteLine("主线程执行完毕!"); 
  21.             Console.ReadKey(); 
  22.         } 

4 Task的任务取消(CancellationTokenSource)

1 Thread取消任务执行

在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:

  1. static void Main(string[] args) 
  2.         { 
  3.             bool isStop = false
  4.             int index = 0; 
  5.             //开启一个线程执行任务 
  6.             Thread th1 = new Thread(() => 
  7.               { 
  8.                   while (!isStop) 
  9.                   { 
  10.                       Thread.Sleep(1000); 
  11.                       Console.WriteLine($"第{++index}次执行,线程运行中..."); 
  12.                   } 
  13.               }); 
  14.             th1.Start(); 
  15.             //五秒后取消任务执行 
  16.             Thread.Sleep(5000); 
  17.             isStop = true
  18.             Console.ReadKey(); 
  19.         } 

2 Task取消任务执行

Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。

  1. static void Main(string[] args) 
  2.         { 
  3.             CancellationTokenSource source = new CancellationTokenSource(); 
  4.             int index = 0; 
  5.             //开启一个task执行任务 
  6.             Task task1 = new Task(() => 
  7.               { 
  8.                   while (!source.IsCancellationRequested) 
  9.                   { 
  10.                       Thread.Sleep(1000); 
  11.                       Console.WriteLine($"第{++index}次执行,线程运行中..."); 
  12.                   } 
  13.               }); 
  14.             task1.Start(); 
  15.             //五秒后取消任务执行 
  16.             Thread.Sleep(5000); 
  17.             //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true 
  18.             source.Cancel(); 
  19.             Console.ReadKey(); 
  20.         } 

CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000) 实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action) 注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。看一个栗子:

  1. static void Main(string[] args) 
  2.         { 
  3.             CancellationTokenSource source = new CancellationTokenSource(); 
  4.             //注册任务取消的事件 
  5.             source.Token.Register(() => 
  6.             { 
  7.                 Console.WriteLine("任务被取消后执行xx操作!"); 
  8.             }); 
  9.  
  10.             int index = 0; 
  11.             //开启一个task执行任务 
  12.             Task task1 = new Task(() => 
  13.               { 
  14.                   while (!source.IsCancellationRequested) 
  15.                   { 
  16.                       Thread.Sleep(1000); 
  17.                       Console.WriteLine($"第{++index}次执行,线程运行中..."); 
  18.                   } 
  19.               }); 
  20.             task1.Start(); 
  21.             //延时取消,效果等同于Thread.Sleep(5000);source.Cancel(); 
  22.             source.CancelAfter(5000); 
  23.             Console.ReadKey(); 
  24.         } 

执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:

最后看上一篇跨线程的栗子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:

  1. public partial class Form1 : Form 
  2.     { 
  3.         public Form1() 
  4.         { 
  5.             InitializeComponent(); 
  6.         } 
  7.  
  8.         private void mySetValueBtn_Click(object sender, EventArgs e) 
  9.         { 
  10.             Task.Run(() => 
  11.             { 
  12.                 Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); }; 
  13.                 for (int i = 0; i < 1000000; i++) 
  14.                 { 
  15.                     myTxtbox.Invoke(setValue,i); 
  16.                 } 
  17.             }); 
  18.         } 
  19.     } 

运行界面如下,赋值的task不会阻塞UI线程:

回到顶部

三、异步方法(async/await)

在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看一个获取文件内容的栗子:

  1. class Program 
  2.     { 
  3.         static void Main(string[] args) 
  4.         { 
  5.             string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result; 
  6.             //调用同步方法 
  7.             //string content = GetContent(Environment.CurrentDirectory + @"/test.txt"); 
  8.             Console.WriteLine(content); 
  9.             Console.ReadKey(); 
  10.         } 
  11.         //异步读取文件内容 
  12.         async static Task<string> GetContentAsync(string filename) 
  13.         { 
  14.              
  15.             FileStream fs = new FileStream(filename, FileMode.Open); 
  16.             var bytes = new byte[fs.Length]; 
  17.             //ReadAync方法异步读取内容,不阻塞线程 
  18.             Console.WriteLine("开始读取文件"); 
  19.             int len = await fs.ReadAsync(bytes, 0, bytes.Length); 
  20.             string result = Encoding.UTF8.GetString(bytes); 
  21.             return result; 
  22.         } 
  23.         //同步读取文件内容 
  24.         static string GetContent(string filename) 
  25.         { 
  26.             FileStream fs = new FileStream(filename, FileMode.Open); 
  27.             var bytes = new byte[fs.Length]; 
  28.             //Read方法同步读取内容,阻塞线程 
  29.             int len =  fs.Read(bytes, 0, bytes.Length); 
  30.             string result = Encoding.UTF8.GetString(bytes); 
  31.             return result; 
  32.         } 
  33.     } 

test.txt内容是【hello world!】执行结果为:

上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task,代码中的返回值为T。上边栗子中GetContentAsync的签名返回值为Task,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。

异步方法签名的返回值有以下三种:

① Task:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task ;

② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;

③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。

小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

本文转载自微信公众号「CSharp编程大全」,可以通过以下二维码关注。转载本文请联系CSharp编程大全公众号。

 

责任编辑:武晓燕 来源: CSharp编程大全
相关推荐

2013-05-16 10:33:11

C#C# 5.0Async

2009-08-21 11:31:59

异步和多线程的区别

2014-07-15 10:08:42

异步编程In .NET

2009-08-20 18:11:08

C#异步委托

2023-10-08 10:21:11

JavaScriptAsync

2010-12-22 10:21:17

C#基础

2024-03-06 13:23:56

Task.RunC#异步陷阱

2009-08-17 16:49:46

C#多线程控制

2023-06-13 13:39:00

多线程异步编程

2009-03-12 10:52:43

Java线程多线程

2017-08-02 14:17:08

前端asyncawait

2016-12-14 15:05:08

C#异步编程

2021-09-09 07:16:00

C#多线程开发

2009-08-17 16:41:03

C#多线程控制

2021-06-28 08:10:59

JavaScript异步编程

2009-09-07 13:02:52

Java和C#线程

2009-08-12 18:04:44

编写C#多线程

2009-08-27 14:29:15

C# explicti

2023-05-08 11:49:05

asyncawait场景

2022-11-21 09:01:00

Swift并发结构
点赞
收藏

51CTO技术栈公众号