同步和异步主要用于修饰方法。当一个方法被调用时,调用者须要等待该方法执行完毕并返回才能继续执行,咱们称这个方法是同步方法;当一个方法被调用时当即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,咱们称这个方法为异步方法。html
异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),所以咱们把一些不须要当即使用结果、较耗时的任务设为异步执行,能够提升程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,如今C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其余异步操做的方式会在下一篇介绍。编程
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不能控制线程的执行顺序,咱们也不能获取线程池内线程取消/异常/完成的通知,即咱们不能有效监控和控制线程池中的线程。异步
咱们知道了ThreadPool的弊端:咱们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优势,同时也解决了使用线程池不易控制的弊端。async
首先看一下怎么去建立并运行一个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); 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,而后再执行后边的代码,程序运行结果以下: spa
上边的全部栗子中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(); }
执行结果以下:
使用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方法后执行结果以下,实现了线程阻塞:
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();】能够实现相同功能 Task.WaitAll(new Task[]{ task1,task2}); Console.WriteLine("主线程执行完毕!"); Console.ReadKey(); }
执行结果以下:
若是将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,而后打印【主线程执行完毕】,最后打印【线程2执行完毕】
上边的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(); }
在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(); }
Task中有一个专门的类 CancellationTokenSource 来取消任务执行,仍是使用上边的例子,咱们修改代码以下,程序运行的效果不变。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; //开启一个task执行任务 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { 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线程:
在C#5.0中出现的 async和await ,让异步编程变得更简单。咱们看一个获取文件内容的栗子:
class Program { static void Main(string[] args) { string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result; //调用同步方法 //string content = GetContent(Environment.CurrentDirectory + @"/test.txt"); 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; } }
test.txt内容是【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来提升任务执行效率。