C#多线程和异步(⼆)——Task和asyncawait详解⼀、什么是异步
  同步和异步主要⽤于修饰⽅法。当⼀个⽅法被调⽤时,调⽤者需要等待该⽅法执⾏完毕并
返回才能继续执⾏,我们称这个⽅法是同步⽅法;当⼀个⽅法被调⽤时⽴即返回,并获取⼀个
线程执⾏该⽅法内部的业务,调⽤者不⽤等待该⽅法执⾏完毕,我们称这个⽅法为异步⽅法。
  异步的好处在于⾮阻塞(调⽤线程不会暂停执⾏去等待⼦线程完成),因此我们把⼀些不需要
⽴即使⽤结果、较耗时的任务设为异步执⾏,可以提⾼程序的运⾏效率。net4.0在ThreadPool
的基础上推出了Task类,微软极⼒推荐使⽤Task来执⾏异步任务,现在C#类库中的异步⽅法基
本都⽤到了Task;net5.0推出了async/await,让异步编程更为⽅便。本篇主要介绍Task、
async/await相关的内容,其他异步操作的⽅式会在下⼀篇介绍。
回到顶部
⼆、Task介绍
  Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若
⼲数量的线程,如果有任务需要处理时,会从线程池中获取⼀个空闲的线程来执⾏任务,任务
执⾏完毕后线程不会销毁,⽽是被线程池回收以供后续任务使⽤。当线程池中所有的线程都在
忙碌时,⼜有新任务要处理时,线程池才会新建⼀个线程来处理该任务,如果线程数量达到设
置的最⼤值,任务会排队,等待其他任务释放线程后再执⾏。线程池能减少线程的创建,节省
开销,看⼀个ThreadPool的栗⼦吧
static void Main(string[] args) { for (int i = 1; i <=10; i++) { //ThreadPool执⾏任务
ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { Console.WriteLine($"第{obj}个执⾏任务");
}),i); } Console.ReadKey(); }
  上边的代码通过ThreadPool执⾏了10个任务,执⾏结果为:
  ThreadPool相对于Thread来说可以减少线程的创建,有效减⼩系统开销;但是
ThreadPool不能控制线程的执⾏顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即
我们不能有效监控和控制线程池中的线程。
1 Task创建和运⾏
  我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执⾏顺序,也不能获取线程
池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的
优点,同时也解决了使⽤线程池不易控制的弊端。
⾸先看⼀下怎么去创建并运⾏⼀个Task,Task的创建和执⾏⽅式有如下三种:
static void Main(string[] args) { //1.new⽅式实例化⼀个Task,需要通过Start⽅法启动 Task task = new
Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的线程ID为
{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)创
建和启动⼀个Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100);
await和async使用方法
Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); });
//3.Task.Run(Action action)将任务放在线程池队列,返回并启动⼀个Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的线程ID为{
Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("执⾏主线程!"); Console.ReadKey();
}
执⾏结果如下:
  我们看到先打印"执⾏主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边
的栗⼦Task都没有返回值,我们也可以创建有返回值的Task<TResult>,⽤法和没有返回值的
基本⼀致,我们简单修改⼀下上边的栗⼦,代码如下:
static void Main(string[] args) { ////1.new⽅式实例化⼀个Task,需要通过Start⽅法启动 Task<string> task =
new Task<string>(() => { return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}"; });
task.Start(); ////2.Task.Factory.StartNew(Func func)创建和启动⼀个Task Task<string> task2
=Task.Factory.StartNew<string>(() => { return $"hello, task2的ID为{
Thread.CurrentThread.ManagedThreadId}"; }); ////3.Task.Run(Func func)将任务放在线程池队列,返回并
启动⼀个Task Task<string> task3= Task.Run<string>(() => { return $"hello, task3的ID为{
Thread.CurrentThread.ManagedThreadId}"; }); Console.WriteLine("执⾏主线程!");
Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result);
Console.ReadKey(); }
  注意task.Resut获取结果时会阻塞线程,即如果task没有执⾏完成,会等待task执⾏完成获取到Result,然后再执⾏后边的代码,程序运⾏结果如下:
 上边的所有栗⼦中Task的执⾏都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执⾏怎
么办呢?Task提供了  task.RunSynchronously()⽤于同步执⾏Task任务,代码如下:
static void Main(string[] args) { Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine("执⾏Task结束!"); }); //同步执⾏,task会阻塞主线程 task.RunSynchronously(); Console.WriteLine("执⾏主线程结束!"); Console.ReadKey(); }
执⾏结果如下:
2 Task的阻塞⽅法(Wait/WaitAll/WaitAny)
1 Thread阻塞线程的⽅法
  使⽤Thread时,我们知道⽤thread.Join()⽅法即可阻塞主线程。看⼀个例⼦:
static void Main(string[] args) { Thread th1 = new Thread(() => { Thread.Sleep(500);
Console.WriteLine("线程1执⾏完毕!"); }); th1.Start(); Thread th2 = new Thread(() => {
Thread.Sleep(1000); Console.WriteLine("线程2执⾏完毕!"); }); th2.Start(); //阻塞主线程 th1.Join();
th2.Join(); Console.WriteLine("主线程执⾏完毕!"); Console.ReadKey(); }
  如果注释掉两个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 执⾏完毕就解除阻塞,看⼀个栗⼦:
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500);
Console.WriteLine("线程1执⾏完毕!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("线程2执⾏完毕!"); }); task2.Start(); //阻塞主线程。task1,task2都执⾏完毕再执⾏主线程       //执⾏【task1.Wait();task2.Wait();】可以实现相同功能 Tas
k.WaitAll(new Task[]{ task1,task2}); Console.WriteLine("主线程执⾏完毕!"); Console.ReadKey(); }
  执⾏结果如下:
  如果将栗⼦中的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执⾏完毕后就开始执⾏后续操作。看⼀个栗⼦:
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("线程1执⾏完毕!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000);
Console.WriteLine("线程2执⾏完毕!"); }); task2.Start(); //task1,task2执⾏完了后执⾏后续操作
Task.WhenAll(task1, task2).ContinueWith((t) => { Thread.Sleep(100); Console.WriteLine("执⾏后续操作完毕!"); }); Console.WriteLine("主线程执⾏完毕!"); Console.ReadKey(); }
  执⾏结果如下,我们看到WhenAll/WhenAny⽅法不会阻塞主线程,当使⽤WhenAll⽅法时所有的task都执⾏完毕才会执⾏后续操作;如果把栗⼦中的WhenAll替换成WhenAny,则只要有⼀个线程执⾏完毕就会开始执⾏后续操作,这⾥不再演⽰。
  上边的栗⼦也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)  和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现,修改上边栗⼦代码如下,执⾏结果不变。
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("线程1执⾏完毕!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000);
Console.WriteLine("线程2执⾏完毕!"); }); task2.Start(); //通过TaskFactroy实现
Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(100);
Console.WriteLine("执⾏后续操作"); }); Console.WriteLine("主线程执⾏完毕!"); Console.ReadKey();
}
4 Task的任务取消(CancellationTokenSource)
1 Thread取消任务执⾏
  在Task前我们执⾏任务采⽤的是Thread,Thread怎么取消任务呢?⼀般流程是:设置⼀个变量来控制任务是否停⽌,如设置⼀个变量isStop,然后线程轮询查看isStop,如果isStop为true 就停⽌,代码如下:
static void Main(string[] args) { bool isStop = false; int index = 0; //开启⼀个线程执⾏任务 Thread th1 = new Thread(() => { while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执⾏,线程运⾏中..."); } }); th1.Start(); //五秒后取消任务执⾏ Thread.Sleep(5000); isStop = true; Console.ReadKey(); } 2 Task取消任务执⾏
  Task中有⼀个专门的类 CancellationTokenSource  来取消任务执⾏,还是使⽤上边的例⼦,我们修改代码如下,程序运⾏的效果不变。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; //开启⼀个task执⾏任务 Task task1 = new Task(() => { while (!source.IsCancellationReq
uested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执⾏,线程运⾏中..."); } }); task1.Start(); //五秒后取消任务执⾏ Thread.Sleep(5000); //source.Cancel()⽅法请求取消任务,IsCancellationRequested会变成true source.Cancel(); Console.ReadKey(); }
  CancellationTokenSource的功能不仅仅是取消任务执⾏,我们可以使
⽤  source.CancelAfter(5000)  实现5秒后⾃动取消任务,也可以通
过  source.Token.Register(Action action)  注册取消任务触发的回调函数,即任务被取消时注册的action会被执⾏。看⼀个栗⼦:
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注册任务取消的事件 source.Token.Register(() => { Console.WriteLine("任务被取消后执⾏xx操作!"); }); int index = 0; //开启⼀个task执⾏任务 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执⾏,线程运⾏中..."); } }); task1.Start(); //延时取消,效果等同于Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }  执⾏结果如下,第5次执⾏在取消回调后打印,这是因为,执⾏取消的时候第5次任务已经通过了while()判断,任务已经执⾏中了:
  最后看上⼀篇跨线程的栗⼦,点击按钮启动⼀个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void mySetValueBtn_Click(object sender, EventArgs e) { Task.Run(() => { Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); }; for (int i = 0; i < 1000000; i++) { myTxtbox.Invoke(setValue,i); } }); } }
  运⾏界⾯如下,赋值的task不会阻塞UI线程:
回到顶部
三、异步⽅法(async/await)
  在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看⼀个获取⽂件内容的栗⼦:
class Program { static void Main(string[] args) { string content =
GetContentAsync(Environment.CurrentDirectory + @"/").Result; //调⽤同步⽅法 //string content = GetContent(Environment.CurrentDirectory + @"/"); Console.WriteLine(content);
Console.ReadKey(); } //异步读取⽂件内容 async static Task<string> GetContentAsync(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //ReadAync ⽅法异步读取内容,不阻塞线程 Console.WriteLine("开始读取⽂件"); int len = await fs.ReadAsync(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } //同步读取⽂件内容 static
string GetContent(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //Read⽅法同步读取内容,阻塞线程 int len = fs.Read(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } }
  内容是【hello world!】执⾏结果为:
  上边的栗⼦也写出了同步读取的⽅式,将main函数中的注释去掉即可同步读取⽂件内容。我们可以看到异步读取代码和同步读取代码基本⼀致。async/await让异步编码变得更简单,我们可以像写同步代码⼀样去写异步代码。注意⼀个⼩问题:异步⽅法中⽅法签名返回值为Task<T>,代码中的返回值为T。上边栗⼦中GetContentAsync的签名返回值为Task<string>,⽽代码中返回值为string。牢记这⼀细节对我们分析异步代码很有帮助。
  异步⽅法签名的返回值有以下三种:
    ① Task<T>:如果调⽤⽅法想通过调⽤异步⽅法获取⼀个T类型的返回值,那么签名必须为Task<TResult>;
    ② 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来提⾼任务执⾏效率。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。