1、什么是异步同步
如果一个方法被调用,调用者需要等待该方法被执行完毕之后才能继续执行,则是同步。
如果方法被调用后立刻返回,即使该方法是一个耗时操作,也能立刻返回到调用者,调用者不需要等待该方法,则称之为异步。
异步编程需要用到Task任务函数,不返回值的任务由 System.Threading.Tasks.Task 类表示。返回值的任务由 System.Threading.Tasks.Task<TResult> 类表示,该类从Task 继承。
C#提供了基于任务的异步编程方法(TPL),更多资料在《.NET 中的并行编程》https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/
先来个例子看下Task的基本用法。
static void Main(string[] args) { var task = new Task(() => { Console.WriteLine($"hello, task的线程ID为{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("task start...."); Thread.Sleep(2000); Console.WriteLine("task end...."); }); task.Start(); Console.WriteLine("main start...."); Thread.Sleep(1000); Console.WriteLine("main end...."); Console.ReadLine(); }
运行结果为:
2、Task的三种启动方法
通过new方法创建,然后start启动。
通过Task.Factory.StartNew创建。
通过Task.Run创建。
Task task1 = new Task(() => Console.WriteLine($"hello, task 1的线程ID{Thread.CurrentThread.ManagedThreadId}");}); task1.Start(); Task task2 = Task.Run(() => {Console.WriteLine($"hello, task 2的线程ID{Thread.CurrentThread.ManagedThreadId}");}); Task task3 = Task.Factory.StartNew(() => {Console.WriteLine($"hello,task 3的线程ID为{Thread.CurrentThread.ManagedThreadId}");});
3、Task阻塞与等待
如果要等待任务完成可以使用Task.Wait方法,使用WaitAll方法等待所有任务完成。如果等待多个任务但是只需要任何一个任务完成就可以执行下一步,则可以使用WaitAny。
static void Main(string[] args) { var t1 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t1线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); var t2 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t2线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); // t1.Wait(); var t3 = Task.Run(() => { Console.WriteLine($"t3线程ID为{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("t3 start...."); Thread.Sleep(2000); Console.WriteLine("t3 end...."); });
Console.WriteLine("main start...."); Thread.Sleep(1000); Console.WriteLine("main end...."); Console.ReadLine(); }
运行效果:
如果打开t1.Wait(); 如下t1先执行。
如果t1.Wait()改为:Task.WaitAll(t1, t2);输出如下,先等待t1和t2执行完毕。
如果需要某个任务先执行完再运行主线程,则使用RunSynchronously就可以同步执行Task任务。下面的例子就是在当前的调度器上同步的执行任务t1。
var t1 = new Task(() =>
{ Thread.Sleep(100); Console.WriteLine($"t1线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); t1.RunSynchronously();
Console.WriteLine($"主线程ID为{ Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
4、任务的状态和返回值
Task<Tresult>获取异步任务的返回值。
var t1 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t1线程ID为{ Thread.CurrentThread.ManagedThreadId}"); return "t1 return"; }); Console.WriteLine($"t1 return value {t1.Result}");
Task<Tresult>会阻塞任务直到任务返回,Task.Result给出任务的返回值给调用它的任务,下面例子显示了Result等待任务完成。
var cts = new CancellationTokenSource(); Task<int> t1 = new Task<int>(() => { try { //while (!cts.IsCancellationRequested) { //cts.Token.ThrowIfCancellationRequested(); Console.WriteLine($"t1 start 线程ID为{ Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"t1 end 线程ID为{ Thread.CurrentThread.ManagedThreadId}"); } return 1; } catch (Exception ex) { Console.WriteLine("---Exception---"); return 0; } }, cts.Token); t1.Start(); Console.WriteLine("---end---{0}", t1.Result);
输出结果如下,t.Result为1.
t.Result会导致阻塞直至等待的任务完成返回。如果使用返回值,那么异步方法中方法签名返回值为Task<T>,代码中的返回值也要为T。
static void Main(string[] args) { var cts = new CancellationTokenSource(); Task<int> t1 = new Task<int>(() => { try { //while (!cts.IsCancellationRequested) { //cts.Token.ThrowIfCancellationRequested(); Console.WriteLine($"t1 start 线程ID为{ Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"t1 end 线程ID为{ Thread.CurrentThread.ManagedThreadId}"); } return 1; } catch (Exception ex) { Console.WriteLine("---Exception---"); return 0; } }, cts.Token); t1.Start(); // Console.WriteLine("---end---{0}", t1.Result); var t2 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t2线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); Console.ReadLine(); } }
输出为:
如果打开注释Console.WriteLine("---end---{0}", t1.Result);则任务阻塞,等待t1执行完,拿到返回值t1.Result之后才继续执行,输出为:
5、任务的取消
任务出错或用户取消了操作,则需要用到任务的取消TaskCanceledException。
cts.Cancel()设置了cts.IsCancellationRequested为true,
然后cts.Token.ThrowIfCancellationRequested()这一句抛出了异常。
执行结果为:
如果主线程不延时或延时为10,则有可能在任务启动之前就停止了任务,就不会发生任务抛出异常了,由于系统调度问题,一定情况下输出与上面的一样,大部分情况输出为:
此外,也可以在task中查询标志。while (!cts.IsCancellationRequested),如果取消则不在执行。如果一次取消多个任务,可以使用CancellationTokenSource.CreateLinkedTokenSource实现。
帮助文档:
从开始.NET Framework 4,.NET Framework 使用统一的模型来协作取消异步操作或长时间运行的同步操作涉及两个对象:
一个CancellationTokenSource对象,它提供取消标记通过其Token属性,并将取消消息通过调用其Cancel或CancelAfter方法。
一个CancellationToken对象,指示是否请求取消。
用于实现协作取消模型的常规模式是:
实例化 CancellationTokenSource 对象,此对象管理取消通知并将其发送给单个取消标记。
将 CancellationTokenSource.Token 属性返回的标记传递给每个侦听取消的任务或线程。
调用CancellationToken.IsCancellationRequested从接收取消标记的操作的方法。 提供有关每个任务或线程来响应取消请求的机制。 无论你选择取消操作和完全操作方式,取决于你的应用程序逻辑。
调用 CancellationTokenSource.Cancel 方法以提供取消通知。 这将设置CancellationToken.IsCancellationRequested到取消标记的每个副本上的属性true。
调用Dispose方法在您使用完CancellationTokenSource对象。
6、Task和Thread
Task是基于Thread的,是比较高层级的封装,Task任务最终还是需要Thread来执行。
Task比Thread的开销小,Task默认使用线程池。
Task默认使用后台线程来执行,Thread默认是前台线程。
Task使用参数和返回值都比Thread简单。
Task的多任务调度比Thread灵活,Thead的join需要挂起调用者去执行被调用线程然后返回到主线程。而Task提供了多种Wait函数,可以等待其他线程执行。
7、参考文档
本文只是学习笔记,水平有限,抛砖迎玉。
1、基于任务的异步编程
2、Task 类
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task?view=netframework-4.8
3、Difference Between Task and Thread
https://www.dotnetforall.com/difference-task-and-thread/
4、Task And Thread In C#
https://www.c-sharpcorner.com/article/task-and-thread-in-c-sharp/
5、C#多线程和异步(一)——基本概念和使用方法