异步编程初探
异步编程基础
注:前段时间学习杨旭老师出的“C# 异步编程基础(完结)”视频,特地总结下笔记, 视频地址点这里。
线程:创建线程
什么是线程
- 线程是一个可执行路径,它可以独立于其他线程执行
- 每个线程都在操作系统的进程(Process)内执行,而操作系统进程提供了程序运行的独立环境
- 单线程应用,在进程的独立环境里只跑一个线程,所以该线程拥有独占权
- 多线程应用,单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存),内存中的数据被称作共享的状态
例子
static void Main(string[] args)
{
Thread thread = new Thread(WriteY);
thread.Name = "Y Thread ...";
thread.Start();
for (int i = 0; i < 1000; i++)
{
System.Console.Write("x");
}
}
static void WriteY()
{
for (int i = 0; i < 1000; i++)
{
System.Console.Write("y");
}
}
// 程序运行结果
// xxxxxxxxxxxyyyxxxxxx...xxxyyyxxxxxxxxxxyyyxxxxxx
- 在单核计算机上,操作系统必须为 每个线程分配“时间片”(在windows中通常为20ms) 来模拟并发,从而导致重复的x和y块
- 在多核或多处理器计算机上,这两个线程可以真正的并行执行(可能受到计算机上其它活动进程的竞争)
线程被强占
线程的执行与另外一个线程上代码的执行交织的那一点
线程的属性
- 线程一旦开始执行,IsAlive就是true,线程结束就变成false
- 线程结束的条件就是:线程构造函数传入的委托结束了执行
- 线程一旦结束,就无法再重启
- 每个线程都有个Name属性,通常用于调试
- 线程的Name只能设置一次,以后更改会抛出异常
- 静态的Thread.CurrentThread属性,会返回当前执行的线程
// 针对Thread.CurrentThread 的例子
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main Thread ...";
Thread thread = new Thread(WriteY);
thread.Name = "Y Thread ...";
thread.Start();
System.Console.Write(Thread.CurrentThread.Name);
for (int i = 0; i < 1000; i++)
{
System.Console.Write("x");
}
}
static void WriteY()
{
System.Console.Write(Thread.CurrentThread.Name);
for (int i = 0; i < 1000; i++)
{
System.Console.Write("y");
}
}
Thread.Join() & Thread.Sleep()
Join and Sleep
- 线程调用了Join方法后,只有当线程的所有程序执行完毕后,其它线程才会继续执行
// Join 简单例子
static void Main(string[] args)
{
Thread t1 = new Thread(Go);
t1.Start();
t1.Join();
System.Console.WriteLine("Thread t1 has ended");
}
static void Go()
{
for (int i = 0; i < 1000; i++)
{
Console.Write("Y");
}
}
// 程序运行结果
// YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
// ...
// YYYYYYYYYYYYYYYYYYYYYYYYYYYYYThread t1 has ended
// Join 复杂例子
static Thread thread1, thread2;
static void Main(string[] args)
{
thread1 = new Thread(ThreadProc);
thread1.Name = "thread1";
thread1.Start();
thread2 = new Thread(ThreadProc);
thread2.Name = "thread2";
thread2.Start();
}
static void ThreadProc()
{
System.Console.WriteLine("\nCurrent Thread {0}", Thread.CurrentThread.Name);
System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);
if (Thread.CurrentThread.Name == "thread1" &&
thread2.ThreadState != ThreadState.Unstarted)
{
thread2.Join();
}
// Thread.Sleep(2000);
System.Console.WriteLine("\nCurrent Thread : {0}", Thread.CurrentThread.Name);
System.Console.WriteLine("Thread1 State : {0}", thread1.ThreadState);
System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);
}
// 程序运行结果
// Current Thread thread1
// Thread2 State : WaitSleepJoin
// Current Thread thread2
// Thread2 State : Running
// Current Thread : thread2
// Thread1 State : WaitSleepJoin
// Thread2 State : Running
// Current Thread : thread1
// Thread1 State : Running
// Thread2 State : Stopped
添加超时
- 调用 Join 的时候,可以设置一个超时,用毫秒或者TimeSpan都可以
- 如果返回 true ,那就是线程结束了;如果超时了,就返回 false
- Thread.Sleep() 方法会暂停当前线程,并等一段时间,参数可以是毫秒,也可以是TimeSpan
注意⚠️:
- Thread.Sleep(0) 这样调用会导致线程立即放弃本身当前的时间片,自动将 CPU 移交给其它线程
- Thread.Yield() 做同样的事情,但是它只会把执行交给同一处理器上的其它线程
- 当等待Sleep和Join的时候,线程处于阻塞的状态
// Join 超时 毫秒例子🌰
static Thread thread1, thread2;
static void Main(string[] args)
{
thread1 = new Thread(ThreadProc);
thread1.Name = "thread1";
thread1.Start();
thread2 = new Thread(ThreadProc);
thread2.Name = "thread2";
thread2.Start();
}
static void ThreadProc()
{
System.Console.WriteLine("\nCurrent Thread {0}", Thread.CurrentThread.Name);
System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);
if (Thread.CurrentThread.Name == "thread1" &&
thread2.ThreadState != ThreadState.Unstarted)
{
if (thread2.Join(2000))
{
System.Console.WriteLine("Thread2 has termminated.");
}
else
{
System.Console.WriteLine("The timeout has elapsed and Thread1 will resume.");
}
}
System.Console.WriteLine("\nCurrent Thread : {0}", Thread.CurrentThread.Name);
System.Console.WriteLine("Thread1 State : {0}", thread1.ThreadState);
System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);
}
// 程序运行结果
// Current Thread thread2
// Current Thread thread1
// Thread2 State : WaitSleepJoin
// Thread2 State : Running
// Current Thread : thread2
// Thread1 State : WaitSleepJoin
// Thread2 State : Running
// Thread2 has termminated.
// Current Thread : thread1
// Thread1 State : Running
// Thread2 State : Stopped
// Join 超时 TimeSpan 例子🌰
static TimeSpan waitTime = new TimeSpan(0, 0, 1);
static void Main(string[] args)
{
Thread newThread = new Thread(Work);
newThread.Start();
if (newThread.Join(waitTime - waitTime))
{
System.Console.WriteLine("New thread terminated");
}
else
{
System.Console.WriteLine("Join timed out.");
}
}
static void Work()
{
Thread.Sleep(waitTime);
}
// 输出结果
// Join timed out.
阻塞 Blocking
阻塞
- 如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了
- 例如再Sleep或者通过Join等待其它线程结束
- 被阻塞的线程会立即将其处理器的时间片生成给其它线程,从此就不再消耗处理器时间,直到满足其阻塞条件为止
- 可以通过ThreadState这个属性来判断线程是否处于阻塞的状态
bool blocked = (someThread.ThreadState & ThreadState.WaitSleepJoin) != 0;
ThreadState
- ThreadState 是一个flags enum,通过按位的形式,可以合并数据的选项
- 常用的ThreadState的值:Unstarted、Running、WaitSleepJoin、Stopped
解除阻塞(Unblocking)
- 当遇到下列四种情况时就会解除阻塞
- 阻塞条件被满足
- 操作超时(如果设置超时的话)
- 通过Thread.Interrupt() 进行打断
- 通过Thread.Abort() 进行中止
上下文切换
- 当线程阻塞或解除阻塞时,操作系统将执行上下文切换。这会产生少量开销,通常为 1 或 2 微秒
I/O-bound vs Compute-bound(或 CPU-Bound)
- 一个花费大部分时间等待某事发生的操作称为 I/O-bound
- I/O 绑定操作通常涉及输入或输出,但这不是硬性要求:Thread.Sleep() 也被视为 I/O-bound
- 相反,一个花费大部分时间执行CPU密集型工作的操作称为 Compute-bound
阻塞Blocking vs 忙等待Spinging(自旋)
- I/O-bound 操作的工作方式有两种:
- 在当前线程上同步的等待
- Console.ReadLine(),Thread.Sleep(),Thread.Join()…
- 异步的操作,在稍后操作完成时触发一个回调动作
- 在当前线程上同步的等待
- 同步等待的 I/O-bound 操作将大部分时间花在阻塞上
- 它们也可以周期性的在一个循环里进行“打转(自旋)”
while (DateTime.Now < nexeStartTime)
Thread.Sleep(100);
while (DateTime.Now < nexeStartTime);
- 在忙等待和阻塞方面有一些细微差别
- 首先,如果您希望条件很快得到满足(在几微秒之内),则短暂的自旋可能更有效,因为它避免了上下文的切换的开销和延迟
- .NetFramework 提供了特殊的方法和类来提供帮助 SpingLock 和 SpinWait
- 其次,阻塞也不是零成本。这是因为每个线程在生存期间会占用大约1MB的内存,并会给CLR和操作系统带来持续的管理开销
- 因此,在需要处理成百上千个并发操作的大量 I/O-boung 程序的上下文中,阻塞可能会很麻烦
- 所以,此类程序需要使用回调的方法,在等待时完全撤销其线程
- 首先,如果您希望条件很快得到满足(在几微秒之内),则短暂的自旋可能更有效,因为它避免了上下文的切换的开销和延迟
什么是线程安全
本地 vs 共享的状态(Local vs Shared State)
- Local 本地独立
- CLR 为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立
- Shared 共享
- 如果多个线程都引用同一个对象的实例,那么它们就共享这个对象的数据
- 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享
- 静态字段(field)也会在线程间共享数据
线程安全(Thread Safety)
// 线程不安全的例子
static bool _done = false;
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
System.Console.WriteLine("Done");
Thread.Sleep(50);
_done = true;
}
}
// 输出结果
// Done
// Done
锁定与线程安全简介(Locking & Thread Safety)
- 在读取和写入共享数据的时候,通过使用一个互斥锁(exclusive lock),来解决线程不安全的问题
- C# 中使用lock 语句来加锁
- 当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态
- 在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全
- Lock 不是线程安全的银弹,很容易忘记对字段加锁,lock 也会引起一些其它的问题,不如死锁…
// 线程安全的例子
static bool _done = false;
static readonly object _object = new object();
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
lock (_object)
{
if (!_done)
{
System.Console.WriteLine("Done");
Thread.Sleep(50);
_done = true;
}
}
}
// 结果
// Done
向线程传递数据 & 异常处理
向线程传递数据
- 往线程的启动方法里面传递数据,最简单的方法就是使用 Lambda 表达式,在里面使用参数调用方法
static void Main(string[] args)
{
new Thread(() => Go("hi xiao ming")).Start();
}
static void Go(string msg)
{
System.Console.WriteLine(msg);
}
- 在C# 3.0 之前,没有 Lambda 表达式,可以使用 Thread 的 Start 方法来传递参数
static void Main(string[] args)
{
new Thread(Go).Start("hi xiao ming");
}
static void Go(object msg)
{
System.Console.WriteLine((string)msg);
}
- Thread 的重载构造函数可以接受下列两个委托之一作为参数
Public delegate void ThreadStart()
Public delegate void ParameterizedThreadStart (object obj)
Lambda 表达式与被捕获的变量
- 使用 Lambda 表达式可以很简单的给Thread 传递参数。但是线程开始后,可能会不小心修改了被捕获的变量,这个要注意
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(() => System.Console.Write(i)).Start();
}
}
// 结果
// 55555
// 原因
// i 在循环的整个生命周期内指向的是同一个内存地址
// 每个线程对 Console.Write() 的调用都会在它运行的时候对它进行修改
- 解决办法
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
int temp = i;
new Thread(() => System.Console.Write(temp)).Start();
}
}
// 结果
// 01243
// 但是输出顺序还是无法保证
异常处理
- 创建线程时,在作用域范围内的try/catch/finaly 块,在线程开始执行后就与线程无关了
static void Main(string[] args)
{
try
{
new Thread(Go).Start();
}
catch (System.Exception exce)
{
System.Console.WriteLine(exce.Message);
}
}
static void Go()
{
throw new Exception("Go 异常了");
}
// 结果
// Go 异常了
// ----------解决办法-----------
static void Go()
{
try
{
throw new Exception("Go 异常了");
}
catch (System.Exception)
{
System.Console.WriteLine("Go 的异常捕获到了");
}
}
// 结果
// Go 的异常捕获到了
- 在 WPF、WinForm 里,可以订阅全局异常处理事件:
- Application.DispatcherUnhandledException
- Application.ThreadException
- 在通过消息循环调用的程序的任何部分发生未处理的异常(这相当于应用程序处于活动状态时,在主线程上运行所有的代码)后,将触发这些异常
- 但是非UI线程上的未处理异常,并不会触发它
- 而任何线程有任何未处理的异常都会触发
- Application.CurrentDomain.UnhandledException
前台线程 VS 后台线程
前台(Foreground)和后台(Background)线程(Threads)
- 默认情况下,手动创建的线程(Thread)就是前台线程
- 只要有前台线程在运行,那么应用程序就会一直处于活跃状态
- 但是后台线程就不行,后台线程不会影响应用程序结束
- 一旦所有的前台线程停止,那么应用程序就停止了
- 任何的后台线程也会突然中止
- 线程的前台和后台状态,与它的优先级无关(所分配的执行时间)
- 可以通过 IsBackground 属性判断线程是否是后台线程
- 进程终止后,后台线程将立即停止执行,后台线程执行栈中的 finally 块就不会被执行了
- 如果想让它执行,可以在退出程序的时候使用Join 来等待后台线程(如果是你自己创建的线程),或者使用 signal construct
- 应用程序无法正常退出的一个常见原因是还有活跃的前台线程
static void Main(string[] args)
{
Thread worker = new Thread(() => Console.ReadLine());
System.Console.WriteLine(worker.IsBackground);
if (args.Length > 0)
{
worker.IsBackground = true;
}
System.Console.WriteLine(worker.IsBackground);
worker.Start();
worker.Join(2000);
}
线程优先级
线程优先级
- 线程的优先级(Thread的Priority 属性)决定了相对于操作系统中其它活跃线程所占的执行时间
- 优先级分为:
- enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
提升线程的优先级
- 提升线程优先级的时候要tebie注意,因为它可能“饿死”其它线程
- 若想让某线程的优先级比其它进程中的线程高,那就必须提升进程的优先级
- 使用 System.Diagnostics 下的 Process 类
using (Process p = Process.GetCurrentProcess())
p.PriorityClass = ProcessPriorityClass.High;
- 这可以很好地用于只做少量工作且需要较低延迟的非UI线程
- 对于需要进行大量计算的应用程序(尤其是有UI的应用程序),提高进程的优先级可能会使其它的进程饿死,从而降低整个计算机的速度
信号简介
- 有时,你需要让某个线程一直处于等待状态,直至接收到其它线程发来的通知。这就叫做 signaling(发送信号)
- 最简单的信号结构就是 ManualResetEvent
- 调用它上面的 WaitOne() 方法会阻塞当前线程,直到另一个线程通过调用 Set() 方法来开启信号
- 调用完 Set() 之后,信号会处于“打开”状态,可以通过调用 Reset 方法将其再次关闭
static void Main(string[] args)
{
var signal = new ManualResetEvent(false);
new Thread(() =>
{
System.Console.WriteLine("1. Waiting for signal...");
signal.WaitOne();
System.Console.WriteLine("1. Got signal");
}).Start();
Thread.Sleep(2000);
signal.Set();
signal.Reset();
new Thread(() =>
{
System.Console.WriteLine("2. Waiting for signal...");
signal.WaitOne();
signal.Dispose();
System.Console.WriteLine("2. Got signal");
}).Start();
}
// 结果
// 1. Waiting for signal...
// 1. Got signal
// 2. Waiting for signal...
}
富客户端应用处理耗时操作的一种办法
Synchronization Context
线程池
线程池
- 当开始一个线程的时候,将花费几百微秒来组织类似以下的内容:
- 一个新的局部变量栈(Stack)
- 线程池就可以节省这种开销
- 通过预先创建一个可循环使用线程的池来减少这一开销
- 线程池对于高效的并行编程和细粒度并发是必不可少的
- 它允许在不被线程启动的开销淹没的情况下运行短期操作
使用线程池需要注意的几点
- 不可以设置线程的Name
- 池线程都是后台线程
- 阻塞池线程可能使性能降级
- 你可以自由更改线程池的优先级
- 当它释放回池的时候优先级将还原回正常的状态
- 可以通过 Thread.CurrentThread.IsThreadPoolThread 属性来判断是否执行在池线程上
进入线程池
- 最简单的、显示的在池线程运行代码的方式就是使用 Task.Run
谁使用了线程池
- WCF、Remoting、ASP.NET、ASMX Web Services 应用服务器
- System.Timers.Timer、System.Threading.Timer
- 并行编程结构
- BackgroundWorker 类
- 异步委托
线程池中的整洁
- 线程池提供了另一个功能,即确保临时超出 CPU-Bound 的工作不会导致 CPU 超额订阅
- CPU 超额订阅:活跃的线程数超过 CPU 的核数,操作系统就需要对线程进行时间切片
- 超额订阅对性能影响很大,时间切片需要昂贵的上下文切换,并且可能是 CPU 缓存失效,而 CPU 缓存对于现代处理器的性能至关重要
CLR 对于保持线程池整洁的策略
- CLR 通过对任务排队并对其启动进行节流限制来避免线程池中的超额订阅
- 它首先可能运行较可能多的并发任务(只要还有CPU核),然后通过爬山算法调整并发级别,并在特定方向上不断调整工作负载
- 如果吞吐量提高,它将继续朝同一方向努力(否则将反转)
- 这确保它始终追随最佳性能曲线,即使面对计算机上竞争的进程活动也是如此
- 如果下面两点能够满足,那么CLR的策略将发挥出最佳效果
- 工作项大都是短时间运行的(< 250ms,或者理想情况下<100ms),因此CLR有很多机会进行测量和调整
- 大部分时间都被阻塞的工作项不会主宰线程池
如果想充分利用CPU,那么保持线程池的整洁是非常重要的
开始一个Task
Thread 的问题
- 线程(Thread)是用来创建并发(concurrency)的一种低级别工具,它有一些限制,尤其是:
- 虽然开始线程的时候可以很方便的传入数据,但是当Join 的时候,很难从线程获得返回值
- 可能需要设置一些共享字段
- 如果操作抛出异常,捕获和传播该异常都很麻烦
- 无法告诉线程在结束时开始做另外的工作,你必须进行 Join 操作(在进程中阻塞当前的线程)
- 虽然开始线程的时候可以很方便的传入数据,但是当Join 的时候,很难从线程获得返回值
- 很难使用较小的并发(concurrent)来组建大型的并发
- 导致了对手动同步的更大依赖以及随之而来的问题
Task class
- Task 类可以很好的解决上述问题
- Task 是一个相对高级的抽象:它代表了一个并发操作(concurrent)
- 该操作可能由 Thread 支持,或不由 Thread 支持
- Task 是可组合的(可使用 Continuation 把它们串成链)
- Tasks 可以使用线程池来减少启动延迟
- 使用 TaskCompletionSource,Tasks 可以利用回调的方式,在等待I/O 绑定操作时完全避免线程
开始一个Task(Task.Run)
- Task 类在System.Threading.Tasks 命名空间下
- 开始一个Task 最简单的办法就是使用 TaskRun (.NET 4.5 ,4.0 的时候是 Task.Factory.StartNew)这个静态方法:
- 传入一个 Action 委托即可
- Task 默认使用线程池,也就是后台线程:
- 当主线程结束时,你创建的所有 tasks 都会结束
- Task.Run 返回一个 Task 对象,可以使用它来监视其过程
- 在 Task.Run 之后,我们没有调用 Start ,因为该方法创建的是“热”任务(hot task)
- 可以通过 Task 的构造函数创建“冷”任务(cold task),但是很少这样做
长时间运行的任务(Long-running tasks)
- 默认情况下,CLR 在线程池中运行 Task ,这非常适合短时间运行的 Compute-Bound 类工作
- 针对长时间运行的任务或者阻塞操作,你可以不采用线程池,使用longRunning
- 如果同时运行多个 long-running tasks(尤其是其中有处于阻塞状态的),那么性能将会受到很大的影响,这时有比 TaskCompletionSource.LongRunning 更好的办法:
- 如果任务是 I/O-Bound,TaskCompletionSource 和异步函数可以让你用回调(Continuations)代替线程来实现并发
- 如果任务是 Compute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其它线程和进程饿死
static void Main(string[] args)
{
Task task = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
System.Console.WriteLine("Hello World");
}, TaskCreationOptions.LongRunning);
task.Wait();
}
Task 的返回值
- Task 有一个泛型子类 Task< Result> ,它允许发出一个返回值
- 使用 Func< TResult> 委托或兼容的Lambda表达式来调用 Task.Run 就可以得到 Task< TResult>
- 随后,可以通过 Result 属性来获得返回值
- 如果这个 Task 还没有完成操作,访问 Result 属性会阻塞该线程直到该 Task 完成操作
- Task< TResult> 可以看作是一种“未来/许诺”(future/promise),在它里面包裹着一个Result,在稍后的时候就会变得可用
- 在 CTP 版本的时候,Task< TResult> 实际上叫做 Future< TResult>
static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
System.Console.WriteLine("Hello World");
Thread.Sleep(3000);
return 1;
});
int result = task.Result;
System.Console.WriteLine(result);
}
// 结果
// Hello World
// 1
Task 的异常
Task 的异常
- 与Thread不一样,Task可以很方便的传播异常
- 如果你的task 抛出了一个未处理的异常,那么给异常就会重新被抛出给
- 调用了 Wait() 的地方
- 访问了 Task< TResult> 的 Result 属性的地方
- CLR 会将异常包裹在 AggregateException 里,以便在并行编程场景中发挥很好的作用
static void Main(string[] args)
{
Task task = Task.Run(() => throw new Exception("task异常了"));
try
{
task.Wait();
}
catch (System.Exception exce)
{
System.Console.WriteLine(exce.Message);
}
}
// result
// One or more errors occurred. (task异常了)
Task 的异常
- 无需重新抛出异常,通过Task 的 IsFaulted 和 IsCanceled 属性也可以检测出Task 是否发生了故障:
- 如果两个属性都返回了 false,那么没有错误发生
- 如果IsCanceled 为 true,那就说明OperationCanceledException 为该Task抛出了
- 如果IsFaulted 为 true,那就说明另一个类型的异常被抛出了,而Exception 属性也将指明错误
异常与“自治”的Task
- 自治的,“设置完就不管了”的Task。就是指不通过调用Wait()方法、Result 属性或 continuation 进行会和的任务
- 针对自治的Task,需要像Thread一样,避免发生“悄无声息的故障”
- 自治Task 上未处理的异常称为“未观察到的异常”
未观察到的异常
- 可以通过全局的 TaskScheduler.UnobservedTaskException 来订阅未观察到的异常
- 关于什么是“未观察到的异常”,有一些细微的差别
- 使用超时进行等到的Task,如果在超时后发生故障,那么它将产生一个“未观察到的异常”
- 在Task发生故障后,如果访问Task 的Exception属性,那么该异常就被认为是“已观察到的”
Coniuation
- 一个 Continuation 会对Task 说:“当你结束的时候,继续再做点其它的事”
- Continuation 通常是通过回调的方式实现的
- 当操作一结束,就开始执行
- 在task 上调用 GetAwaiter 会返回一个 awaiter 对象
- 它的 OnCompleted 方法会告诉之前的task:“当你结束/发生故障的时候要执行委托”
- 可以将 Continuation 附加到已经结束的 task 上面,此时 Continuation 将会被安排立即执行
static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
return 1;
});
var awaiter = task.GetAwaiter();
awaiter.OnCompleted(() =>
{
int result = awaiter.GetResult();
System.Console.WriteLine(result);
});
Console.ReadKey();
}
awaiter
- 任何可以暴露下列两个方法和一个属性的对象就是 awaiter:
- OnCompleted
- GetResult
- 一个叫做 IsCompleted 的 bool 属性
- 没有接口或者父类来统一这些成员
- 其中 OnCompleted 是 INotifyCompletion 的一部分
如果发生故障
- 如果之前的任务发生故障,那么当 Continuation 代码调用 awaiter.GetResult() 的时候,异常会被重新抛出
- 无需调用 GetResult ,我们可以直接访问 task 的 Result 属性
- 但调用GetResult的好处是,如果 task 发生故障,那么异常会被直接抛出,而不是包裹在 AggregateException 里面,这样的话 catch 块就简洁很多了
非泛型Task
- 针对非泛型Task,GetResult() 方法有一个void返回值,他就是用来重新抛出异常
同步上下文
- 如果同步上下文出现了,那么OnCompleted 会自动捕获它,并将 Continuation 提交到这个上下文中,这一点在富客户端应用中非常有用,因为它会把 Continuation 放回到UI线程中
- 如果是编写一个库,则不希望出现上述行为,因为开销较大的UI线程切换应该在程序运行离开库的时候只发生一次,而不是出现在方法调用之间,所以我们可以使用 ConfigureAwait 方法来避免这种行为
- 如果没有同步上下文出现,或者你使用的是 configureAwait(false),那么 Continuation 会运行在先前 task 的同一个线程上,从而避免不必要的开销
ContinueWith
- 另一种调用 Continuation 的方式就是调用task 的 ContinueWith 方法
- ContinueWith 本身返回的就是一个 Task ,它可以用来附加更多的Continuation
- 但是,必须直接处理AggregateException:
- 如果task 发生故障,需要写额外的代码来把Continuation 封装(marshal)到UI应用上
- 在非UI上下文中,若想让 Continuation 和 task 执行在同一个线程上,必须指定TaskContinuationOptions.ExecuteSynchronously,否则它将弹回线程池
- ContinueWith 对于并行编程来说非常有用
TaskCompletionSource
TaskCompletionSource
- Task.Run 创建 Task
- 另一种方式就是用 TaskCompletionSource 来创建 Task
- TaskCompletionSource 让你在稍后开始和结束的任意操作中创建Task
- 它会为你提供一个可手动执行的“从属” Task
- 指示操作何时结束或发生故障
- 它会为你提供一个可手动执行的“从属” Task
- 它对IO-Bound类工作比较理想
- 它可以获得所有task的好处(传播值、异常、Continuation等)
- 不需要在操作时阻塞线程
使用TaskCompletionSource
- 初始化一个实例即可
- 它由一个 Task 属性,可返回一个 Task
- 该 Task 完全由TaskCompletionSource 对象控制
- 调用任意一个方法都会给Task发信号:完成、故障、取消
- 这些方法只能调用一次,如果再次调用:
- SetXXX 会抛出异常
- TryXXX 会返回false
static void Main(string[] args)
{
var tcs = new TaskCompletionSource<int>();
new Thread(() =>
{
Thread.Sleep(5000);
tcs.SetResult(42);
})
{
IsBackground = true
}.Start();
Task<int> task = tcs.Task;
System.Console.WriteLine(task.Result);
}
// 使用 TaskCompletionSource 实现Task.Run的效果
static void Main(string[] args)
{
Task<int> task = Run(() =>
{
Thread.Sleep(5000);
return 42;
});
System.Console.WriteLine(task.Result);
}
// 调用此方法相当于调用 Task.Factory.StartNew,
// 并使用 TaskCreationOptions.LongRunning 选项来创建非线程池的线程
static Task<TResult> Run<TResult>(Func<TResult> function)
{
var tcs = new TaskCompletionSource<TResult>();
new Thread(() =>
{
try
{
tcs.SetResult(function());
}
catch (System.Exception ex)
{
tcs.SetException(ex);
}
}).Start();
return tcs.Task;
}
TaskCompletionSource的真正魔力
- 它创建Task,但不占用线程
// timer 例子
static void Main(string[] args)
{
var awaiter = GetAnswerToLife().GetAwaiter();
awaiter.OnCompleted(() =>
{
System.Console.WriteLine(awaiter.GetResult());
});
Console.ReadKey();
}
static Task<int> GetAnswerToLife()
{
var tcs = new TaskCompletionSource<int>();
var timer = new System.Timers.Timer(5000) { AutoReset = false };
timer.Elapsed += delegate
{
timer.Dispose();
tcs.SetResult(42);
};
timer.Start();
return tcs.Task;
}
// Delay 例子
static void Main(string[] args)
{
// 5 秒钟之后,Continuation 开始的时候,才占用线程
Delay(5000).GetAwaiter().OnCompleted(() =>
{
System.Console.WriteLine(42);
});
Console.ReadKey();
}
static Task Delay(int milliseconds)
{
// 没有非泛型版本的TaskCompletionSource
var tcs = new TaskCompletionSource<object>();
var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
timer.Elapsed += delegate
{
timer.Dispose();
tcs.SetResult(null);
};
timer.Start();
return tcs.Task;
}
Task.Delay
static void Main(string[] args)
{
Task.Delay(5000)
.GetAwaiter()
.OnCompleted(() => System.Console.WriteLine(42));
Task.Delay(5000)
.ContinueWith(a => System.Console.WriteLine(42));
Console.ReadKey();
}
同步和异步
- 同步操作会在返回调用者之前完成它的工作
- 异步操作会在返回调用者之后去做它的工作
- 异步的方法更为少见,会启用并发,因为它的工作会与调用者并行执行
- 异步方法通常立即就会返回调用者,所以叫非阻塞方法
- 目前见到的大部分异步方法,都是通用目的的
- Thread.Start
- Task.Run
- 可以将 Continuation 附加到Task 的方法
什么是异步编程
- 异步编程的原则是将长时间运行的函数写成异步的
- 传统的做法是将长时间运行的函数写成同步的,然后从新的线程或Task 进行调用,从而按需引入并发
- 上述异步方式的不同之处在于,它是从长时间运行函数的内部启动并发。这有两点好处:
- IO-Bound 并发可不使用线程来实现,可提高可扩展性和执行效率
- 富客户端在 worker 线程会使用更少的代码,简化了线程的安全性
异步编程的用途
- 编写高效处理大量并发IO的应用程序
- 挑战并不是线程安全(因为共享状态通常是最小化的),而是执行效率
经验之谈
- 为了获得上述好处,下列操作建议异步编写:
- IO-Bound 和 Compute-Bound 操作
- 执行超过50ms的操作
- 另一方面过细的粒度会损害性能,因为异步操作也有开销
异步和continuation 以及语言的支持
异步和continuation
- TASK 非常适合异步编程,因为它们支持Continuation(它对异步非常重要)
- TaskCompletionSource 的例子
- TaskCompletionSource 是实现底层IO-Bound异步方法的一种标准方式
- 对于 Compute-Bound,Task.Run 会初始化绑定线程的并发
- 把 Task 返回调用者,创建异步方法
- 异步编程的区别:目标是在调用图较低的位置来这样做
- 在富客户端应用中,高级方法可以保留在UI线程和访问控制以及共享状态上,不会出现线程安全问题
// 同步写法,顺序执行
static void Main(string[] args)
{
DisplayPrimeCounts();
}
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
" Primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
}
System.Console.WriteLine("Done");
}
static int GetPrimesCount(int start, int count)
{
return ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
}
// result
// 78498 Primes between 0 and 999999
// 70435 Primes between 1000000 and 1999999
// 67883 Primes between 2000000 and 2999999
// 66330 Primes between 3000000 and 3999999
// 65367 Primes between 4000000 and 4999999
// 64336 Primes between 5000000 and 5999999
// 63799 Primes between 6000000 and 6999999
// 63129 Primes between 7000000 and 7999999
// 62712 Primes between 8000000 and 8999999
// 62090 Primes between 9000000 and 9999999
// Done
// 异步执行,非顺序
static void Main(string[] args)
{
DisplayPrimeCounts();
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
{
System.Console.WriteLine(awaiter.GetResult() + " Primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
});
}
System.Console.WriteLine("Done");
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
// result
// Done
// 66330 Primes between 10000000 and 10999999
// 64336 Primes between 10000000 and 10999999
// 67883 Primes between 10000000 and 10999999
// 70435 Primes between 10000000 and 10999999
// 65367 Primes between 10000000 and 10999999
// 63129 Primes between 10000000 and 10999999
// 63799 Primes between 10000000 and 10999999
// 78498 Primes between 10000000 and 10999999
// 62712 Primes between 10000000 and 10999999
// 62090 Primes between 10000000 and 10999999
语言对异步的支持非常重要
- async、await 之前的写法,看下面的例子
- 需要对 task 的执行序列化
- 例如 task b 依赖于 task a 的执行结果
- 为此,必须在 continuation 内部触发下一次循环
- async 和 await
- 对于不想复杂的实现异步非常重要
- 命令式循环结构不要和 Continuation 混合在一起,因为它们依赖于当前本地状态
- 另一种实现,函数式写法(Linq 查询),它也是响应式编程(Rx)的基础
static void Main(string[] args)
{
DisplayPrimeCounts();
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
DisplayPrimeCountsFrom(0);
}
// 内部触发下一次循环(同步)
static void DisplayPrimeCountsFrom(int i)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
{
System.Console.WriteLine(awaiter.GetResult() + " Primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
if (++i < 10)
{
DisplayPrimeCountsFrom(i);
}
else
{
System.Console.WriteLine("Done");
}
});
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
// result
// 78498 Primes between 0 and 999999
// 70435 Primes between 1000000 and 1999999
// 67883 Primes between 2000000 and 2999999
// 66330 Primes between 3000000 and 3999999
// 65367 Primes between 4000000 and 4999999
// 64336 Primes between 5000000 and 5999999
// 63799 Primes between 6000000 and 6999999
// 63129 Primes between 7000000 and 7999999
// 62712 Primes between 8000000 and 8999999
// 62090 Primes between 9000000 and 9999999
// Done
class Program
{
static void Main(string[] args)
{
DisplayPrimeCountAsync();
Console.ReadKey();
}
public static Task DisplayPrimeCountAsync()
{
var machine = new PrimesStateMachine();
machine.DisplayPrimeCountsFrom(0);
return machine.Task;
}
public static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
}
class PrimesStateMachine
{
TaskCompletionSource<object> _tsc =
new TaskCompletionSource<object>();
public Task Task { get { return _tsc.Task; } }
// 内部触发下一次循环(异步)
public void DisplayPrimeCountsFrom(int i)
{
var awaiter = Program.GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
{
System.Console.WriteLine(awaiter.GetResult() + " Primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
if (++i < 10)
{
DisplayPrimeCountsFrom(i);
}
else
{
System.Console.WriteLine("Done");
_tsc.SetResult(null);
}
});
}
}
// result
// 78498 Primes between 0 and 999999
// 70435 Primes between 1000000 and 1999999
// 67883 Primes between 2000000 and 2999999
// 66330 Primes between 3000000 and 3999999
// 65367 Primes between 4000000 and 4999999
// 64336 Primes between 5000000 and 5999999
// 63799 Primes between 6000000 and 6999999
// 63129 Primes between 7000000 and 7999999
// 62712 Primes between 8000000 and 8999999
// 62090 Primes between 9000000 and 9999999
// Done
- 使用async、await
static void Main(string[] args)
{
DisplayPrimeCountsAsync();
Console.ReadKey();
}
static async Task DisplayPrimeCountsAsync()
{
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) +
" Primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
}
System.Console.WriteLine("Done");
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}
// result
// 78498 Primes between 0 and 999999
// 70435 Primes between 1000000 and 1999999
// 67883 Primes between 2000000 and 2999999
// 66330 Primes between 3000000 and 3999999
// 65367 Primes between 4000000 and 4999999
// 64336 Primes between 5000000 and 5999999
// 63799 Primes between 6000000 and 6999999
// 63129 Primes between 7000000 and 7999999
// 62712 Primes between 8000000 and 8999999
// 62090 Primes between 9000000 and 9999999
// Done
await
异步函数
- async 和 await 关键字可以让你写出和同步代码一样简洁且结构相同的异步代码
awaiting
- await 关键字简化了附加 continuation 的过程
- 其结构如下
var result = await expression;
statement(s);
- 它的作用相当于
var awaiter = expression.GetAwaiter();
awaiter.OnCompleted(() => {
var result = awaiter.GetResult();
statement(s);
});
async 修饰符
- async 修饰符会让编译器把 await 当作关键字而不是标识符(C#5 以前可能会使用 await 作为标识符)
- async 修饰符只能应用于方法(包括 lambda 表达式)
- 该方法可以返回 void、Task、Task< TResult >
- async 修饰符对方法的签名或 public 元数据没有影响(和 unsafe 一样),它只会影响方法内部
- 在接口内,使用 async 是没有意义的
- 使用 async 来重载非 async 的方法是合法的(只要方法签名一致)
- 使用了 async 修饰符的方法就是“异步函数”
static void Main(string[] args)
{
GetNumber();
GetNumber(1);
}
static void GetNumber(int a)
{
var awaiter = GetNumberAsync().GetAwaiter();
awaiter.OnCompleted(() =>
{
var result = awaiter.GetResult();
Console.WriteLine(result);
});
}
static async void GetNumber()
{
Console.WriteLine(await GetNumberAsync());
}
static Task<int> GetNumberAsync()
{
return Task.Run(() => 1);
}
异步方法如何执行的
- 遇到 await 表达式,执行(正常情况下)会返回调用者
- 就像 iterator 里面的 yield return
- 在返回前,运行时会附加一个 continuation 到 await 的 task
- 为保证 task 结束时,执行会跳会原来的方法,从停止的地方继续执行
- 如果发生故障,那异常会被重新抛出
- 如果一切正常,那么它的返回值就会赋给 await 表达式
可以 await 什么?
- await 的表达式通常是一个 task
- 也可以是满足下列条件的任意对象
- 有 GetAwaiter() 方法,它返回一个 awaiter(实现了 INotifyCompletion.OnCompleted 接口)
- 返回适当类型的 GetResult 方法
- 一个 bool 类型的 IsCompleted 属性
捕获本地状态
- await 表达式最牛的地方是它几乎可以出现在任何地方
- 特别的,在异步方法内,await 表达式可以替换任何表达式
- 除了 lock 表达式和 unsafe 上下文
await 之后在哪个线程上执行
- 在 await 表达式之后,编译器依赖于 continuation(通过 awaiter 模式)来继续执行
- 如果在富客户端的UI线程上,同步上下文会保证后续是在原线程上执行
- 否则,就会在 task 结束的线程上继续执行
编写异步函数
编写异步函数
- 对于任何异步函数,你可以使用Task代替void作为返回类型,让该方法成为更有效的异步(可以进行await)
- 并不需要在方法体中显示的返回 Task ,编译器会生成一个Task(当方法完成或者发生异常时),这使得创建异步的调用链非常方便
- 编译器会对返回task 的异步函数进行扩展,使其成为当发送信号或发生故障时,使用 TaskCompletionSource 来创建 Task 的代码
- 因此,当返回 Task 的异步方法结束的时候,执行就会跳回到对它进行await 的地方(通过 Continuation)
返回Task< TResult>
- 如果方法返回 TResult,那么异步方法就可以返回 Task< TResult>
- 其原理就是给 TaskCompletion 发送的信号带有值,而不是null
- 异步编程的写法跟同步编程的写法很相似
C#中如何设计异步函数
- 以同步的方式编写方法
- 使用异步调用来代替同步调用,并且进行await
- 除了顶层方法外(UI控件的Event Handler),把你方法的返回类型升级为Task 或 Task< TResult>,这样他们就可以进行 await 了
编译器能对异步函数生成 Task 意味着什么
- 大多数情况下,你只需要在初始化IO-Bound并发的底层方法里显示的初始化TaskCompletionSource,这种情况很少见
- 针对初始化Compute-Bound 的并发方法,你可以使用Task.Run 来创建 Task
异步调用图的执行
- 整个执行与同步写法的调用图执行顺序一样,因为我们对每个异步函数的调用都进行了 await
- 在调用图中创建了一个没有并行和重叠的连续流
- 每个 await 在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执行
并行(Parallelism)
- 不使用 await 来调用异步函数会导致并行执行的发生
异步 Lambda 表达式
- 匿名方法(包括 Lambda 表达式),通过 使用 async 也可以编程异步方法
- 调用方式也一样
- 附加 event handler 的时候,也可以使用异步 Lambda 表达式
// 伪代码
myButton.Click += async (sender, args) =>
{
await Task.Delay(1000);
myButton.Content = "Done";
}
//----------------------------------------
myButton.Click += ButtonHandler;
async void ButtonHandler(object sender, EventArgs args)
{
await Task.Delay(1000);
myButton.Content = "Done";
}
- 也可以返回Task< TResult>
Func< Task<int>> unnamed = async () =>
{
await Task.Delay(1000);
return 123;
}
int answer = await unnamed();
异步中的同步上下文
优化:同步完成
- 异步函数可以在await之前就返回
static async Task Main(string[] args)
{
Console.WriteLine(await GetWebPageAsync("http://oreilly.com"));
}
private static Dictionary<string, string> _cache
= new Dictionary<String, String>();
static async Task<string> GetWebPageAsync(string uri)
{
if (_cache.TryGetValue(uri, out string html))
{
return html;
}
return _cache[uri] =
await new WebClient().DownloadStringTaskAsync(uri);
}
// 如果 uri 在缓存中存在,那么不会有await发生,执行就会返回给调用者,方法会返回一个已经设置好信号的task,这就是同步完成
- 当 await 同步完成的Task 时,执行不会返回到调用者,也不会通过continuation 跳回,它会立即执行到下个语句
- 编译器是通过检查 awaiter 上的IsCompleted属性来实现这个优化的
- 如果是同步完成,编译器会释放可短路 continuation 的代码
var awaiter = GetWebPageAsync().GetAwaiter();
if(awaiter.IsCompleted)
Console.WriteLine(awaiter.GetResut());
else
awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult()));
- 对一个同步返回的异步方法进行await,仍然会引起一个小的开销(20纳秒左右,2019年的PC);反过来,跳会到线程池,会引入上下文切换开销,可能是1 - 2毫秒;而跳回到UI的消息循环,至少是10倍的开销(如果UI繁忙,那么时间更长)
- 编写完全没有await的异步方法也是合法的,但是编译器会发出警告;但这类方法可以用于重载 virtual/abstract 方法;另外一种可以达到相同结果的方式是:使用 Task.FromResult,它会返回一个已经设置好信号的Task
Task< string> Foo(){ return Task.FromResult("abc"); }
- 如果从UI线程上调用GetWebPageAsync,那么GetWebPageAsync方法是隐式线程安全的,你可以连续多次调用他(从而启动多个并发下载),并且不需要lock来保护缓存;但是如果调用多次,传入的参数是同一个网址,就会启动多个冗余的下载,这样就比较低效,有一个简单的方式可以改变这一点,而不必求助于lock或信令结构,我们创建一个“futures”(Task< string>)的缓存,而不是字符串缓存
private static Dictionary<string, Task<string>> _cache = new Dictionary<string, Task<string>>();
static Task<string> GetWebPageAsync(string uri)
{
if (_cache.TryGetValue(uri, out var downloadTask))
{
return downloadTask;
}
else
{
return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri);
}
}
- 不使用同步上下文,使用lock也可以达到线程安全,lock 的不是下载的过程,lock 的是检查缓存的过程(很短暂)
// 使用 lock
private static Dictionary<string, Task<string>> _cache = new Dictionary<string, Task<string>>();
static Task<string> GetWebPageAsync(string uri)
{
lock (_cache)
{
if (_cache.TryGetValue(uri, out var downloadTask))
{
return downloadTask;
}
else
{
return _cache[uri] = new WebClient().DownloadStringTaskAsync(uri);
}
}
}
ValueTask< T>
ValueTask< T>
- ValueTask< T> 用于微优化场景,可能你永远不需要编写返回此类型的方法
- Task 和 Task< T> 是引用类型,实例化他们需要基于堆的内存分配和后续的收集
- 优化的一种极端的形式是编写无需分配此类内存的代码,换句话说,这不会实例化任何引用类型,不会给垃圾收集器增加负担
- 为了支持这种模式,C# 引入了 ValueTask 和 ValueTask< T> 这两个 Struct,编译器允许使用它们代替 Task 和 Task< T>
- async ValueTask< int> Foo() { … };
- 如果操作同步完成,则 await ValueTask< T> 是无分配的
- 如果操作不是同步完成的,ValueTask< T> 实际上就会创建一个普通的 Task< T> (并将 await 转发给他)
- 使用 AsTask 方法,可以把 ValueTask< T> 转换为 Task< T>(也包括非泛型版本)
使用 ValueTask< T> 的注意事项
- ValueTask< T> 并不常见,它的出现纯粹是为了性能
- 这意味着它被不恰当的值类型语义所困扰,这可能会导致意外,为避免错误行为,必须避免以下情况:
- 多次 await 同一个 ValueTask< T>
- 操作没结束的时候就调用 GetAwaiter().GetResult()
- 如果你需要进行这些操作,那么先调用 AsTask() 方法,操作它返回的 Task
- 避免上述陷阱最简单的办法就是:
- await Foo()
- 将 ValueTask 赋给变量时,就可能引发错误
- ValueTask< int> valueTask = Foo();
- 将其立即转为普通的 Task ,就可以避免此类错误的发生
- Task< int> task = Foo().AsTask();
避免过度的弹回
- 对于在循环中多次调用的方法,通过调用 ConfigureAwait 方法,就可以避免重复的弹回到UI 消息循环所带来的开销
- 这强迫 Task 不把 Continuation 弹回给同步上下文,从而将开销削减到接近上下文切换的成本(如果 await 的方法同步完成,则开销会小的多)
async void A(){ ... await B(); ... };
async Task B() {
for(int i = 0; i < 10; i++)
{
await C().ConfigureAwait(false);
}
}
async Task C() { ... }
// 这意味着对于方法 B 和 C ,我们取消了UI程序中的简单线程安全模型,即代码在UI线程中运行,并且在 await 语句期间被抢占。但是方法A不受影响,如果在一个UI线程上启动,它将保留在UI线程上
// 这种优化在编写库时特别重要:你不需要简化线程安全性带来的好处,因为你的代码通常不与调用方共享状态,也不访问UI控件
//
取消
取消(cancellation)
- 使用取消标志,来实现,对并发进行取消,可以封装一个类
class CanccellationToken
{
public bool IsCancellationReauested { get; private set; }
public void Cancel() { IsCancellationRequested = true; }
public void ThrowIfCancellationRequested()
{
if (IsCancellationReauested)
throw new OperationCanceledException();
}
}
// 使用
async Task Foo (CancelationToken cancellationToken)
{
for(int i = 0; i < 10; i++)
{
Console.writeLine(i);
await Task.Delay(1000);
cancellationToken.ThrowIfCancellationRequested();
}
}
// 原理介绍
// 当调用者想取消并行方法的时候,它调用 CancellationToken 上的 Cancel 方法,这就会把 IsCancellationRequested 设置为 true,即会导致短时间后 Foo 会通过 OperationCanceledException 引发错误
CancelationToken 和 CancelationTokenSource
- 先不管线程安全(应该在读写 IsCancellationRequested 时进行lock),这个模式非常的有效,CLR 也提供了一个 CancelationToken 类,它的功能和前面的例子类似
- 但是它缺少一个 Cancel 方法,Cancel 方法在另外一个类上进行暴露 :CancellationTokenSource
- 这种分离的设计时出于安全的考虑:只能对 CancellationToken 访问的方法,可以检查取消,但是不能实例化取消
获取 CancellationToken
-
想获得取消标志(cabcellation token),先实例化 CancellationTokenSource:
var cancelSource = new CancellationTokenSource(); -
这会暴露一个Token属性,它会返回一个 CancellationToken,所以我们可以这样调用:
var cancelSource = new CancellationTokenSource();
Task foo = Foo (cancelSource.Token);
…
… ( some time later )// 想取消的时候,执行取消操作
// cancelSource.Token 只能对取消进行检查
cancelSource.Cancel();
Delay
- CLR 里大部分的异步方法都支持 CancellationToken,包括Delay 方法
async Task Foo(CancellationToken cancellationToken)
{
for(int i = 0; i < 10; i++)
{
Console.WriteLine(i);
await Task.Delay(1000, cancellationToken);
}
}
- 这时,task 在遇到取消请求时会立即停止(而不是 1 秒钟之后才停止)
- 这里,我们无需调用 ThrowIfCancellationRequested,因为 Delay 会自动调用
- 取消标记在调用栈中很好的向下传播(就像是因为异常,取消请求在调用栈中向上级联一样)
同步方法
- 同步方法也支持取消(例如 Task 的 Wait 方法),这种情况下,取消指令需要异步发出(例如,来自另一个Task)
var cancelSource = new CancellationTokenSource();
Task.Delay(5000).ContinueWith(ant => cancelSource.Cancel());
其它内容
- 事实上,你可以在构造 CancellationTokenSource 时指定一个时间间隔,以便在一段时间后启动取消。它对于实现超时非常有用,无论是同步还是异步:
var cancelSource = new CancellationTokenSource(5000);
try { await Foo(cancelSource.Token); }
catch(OperationCanceledException ex)
{
Console.WiteLine( Console.WriteLine(“Cancelled”));
} - CancellationToken 这个struct 提供了一个 Register 方法,它可以让你注册一个回调委托,这个委托会在取消时触发。它返回一个对象,这个对象在取消注册时可以被 Dispose 掉
- 编译器的异步函数生成的Task 在遇到未处理的OperationCanceledException 异常时会自动进入取消状态(IsCanceled 返回 true,IsFaulted 返回 false)
- 使用 Task.Run 创建的Task 也是如此。这里是指向构造函数传递(相同的)CancellationToken
- 在异步场景中,故障 Task 和取消的Task 之间的区别并不重要,因为他们在await 时都会抛出一个 OperationCanceledException。但这在高级并行编程场景(特别是条件 continuation)中很重要
进度报告
进度报告
- 有时,你希望异步操作在运行的过程中能实时的反馈进度。一个简单的解决办法是向异步方法传入一个 Action 委托,当进度变化的时候触发方法调用:
Task Foo (Action<int> onProgressPercentChanged)
{
return Task.Run (() =>
{
for (int i = 0; i < 1000; i++)
{
if (i % 10 == 0)
onProgressPercentChanged (i / 10);
// Do something compute-bound ...
}
});
}
Action<int> progress = i => Console.WriteLine(i + " %");
await Foo(progress);
// 尽管上述代码可以在 Console App 上很好的应用,但在富客户端应用中却很不理想。因为它是从 Worker 线程中报告的,可能会导致消费者的线程安全问题
IPROGRESS< T> 和 PROGRESS< T>
- CLR 提供了一对类型来解决此问题(富客户端应用进度报告):
- IPROGRESS< T> 接口
- PROGRESS< T> 类(实现了上述接口)
- 它们的目的就是包装一个委托,以便 UI 程序可以安全的通过同步上下文来报告进度
- 接口定义如下:
public interface IProgress<in T>
{
void Report(T value);
}
使用 IPROGRESS< T>
- 使用 IPROGRESS< T>:
Task Foo (IProgress<int> onProgressPercentChanged)
{
return Task.Run(() =>
{
for (int i = 0; i< 1000; i++)
{
if (i % 10 == 0)
onProgressPercentChanged.Report(i / 10);
// Do something compute-bound ...
}
});
}
- PROGRESS< T> 的一个构造函数可以接受 Action< T> 类型的委托:
var progress = new Progress<int>(i => Console.WriteLine(i + " %"));
await Foo(progress);
- PROGRESS< T> 还有一个 ProgressChanged 事件,您可以订阅它,而不是将 Action 委托传递给构造函数
- 在实例化 PROGRESS< T> 时,类捕获同步上下文(如果存在)
- 当 Foo 调用 Report 时,委托是通过该上下文调用的
- 异步方法可以通过 int 替换为公开一系列属性的自定义类型来实现更精细的进度报告
TAP
Task-Based Asynchronous Pattern
- .NET Core 暴露了数百个返回 Task 且可以 await 的异步方法(主要和 I/O 相关)。大多数方法都遵循一个模式,叫做基于 Task 的异步模式(TAP)。这是我们迄今为止所描述的合理形式化。TAP 方法执行以下操作:
- 返回一个“热” (运行中的)Task 或 Task< Result>
- 方法名以 Async 结尾(除了像 Task 组合器等情况)
- 会被重载,以便接收 CancellationToken 或(和)IProgress< T>,如果支持相关操作的话
- 快速返回调用者(只有很小的初始化同步阶段)
- 如果是 I/O 绑定,那么无需线程绑定
Task 组合器
- 异步函数有一个让其保持一致的协议(可以一致的返回 Task),这能让其保持良好的结果:可以使用以及编写 Task 组合器,也就是可以组合 Task,但是并不关心 Task 具体做什么的函数。
- CLR 提供了两个 Task 组合器:
- Task.WhenAny
- Task.WhenAll
// 定义以下方法,供后续使用
async Task<int> Delay1() { await Task.Delay(1000); return 1;}
async Task<int> Delay2() { await Task.Delay(2000); return 2;}
async Task<int> Delay3() { await Task.Delay(3000); return 3;}
WhenAny
- 当一组 Task 中任何一个 Task 完成时,Task.WhenAny 会返回完成的 Task
Task<int> winningTask = await Task.WhenAny(Delay1(), Delay2(), Delay3());
Console.WriteLine("Done");
Console.WriteLine(winningTask.Result); // 1
- 因为 Task.WhenAny 本身就返回一个 Task,我们对它进行 await,就会返回最先完成的 Task
- 上例完全是非阻塞的,包括最后一行(当访问 Result 属性时,winningTask 已经完成),但最好还是对 winningTask 进行 await,因为异常无需 AggregateException 包装就会重新抛出
Console.WriteLine(await winningTask); // 1
- 实际上,我们可以在一步中执行两个 await:
int answer = await await Task.WhenAny(Delay1(), Delay2(), Delay3());
- 如果“没赢”的 Task 后续发生了错误,那么异常将不会被观察到,除非你后续对它们进行 await(或者查询其 Exception 属性)
- WhenAny 很适合不支持超时或取消的操作添加这些功能:
Task<string> task = SomeAsyncFunc();
Task winner = await (Task.WhenAny(task, Task.Delay(5000)));
if (winner != task) throw new TimeoutException();
string result = await task; // Umwrap result/re-throw
- ⚠️:本例中返回的结果是 Task 类型,不是 Task< T>
WhenAll
- 当传给它的所有的 Task 都完成后,Task.WhenAll 会返回一个 Task
Task<int[]> result = await Task.WhenAll(Delay1(), Delay2(), Delay3());
// 本例会在三秒后结束
- 通过轮流对三个 Task 进行 await,也可以得到类似的结果:
Task task1 = Delay1(), task2 = Delay2(), task3 = Delay3();
await task1;
await task2;
await task3;
- 不同点是(除了三个 await 的低效):如果 task1 出错,我们就无需等待 task2 和 task3 了,它们的错误也不会被观察到
- 与之相对,Task.WhenAll 直到所有 Task 完成,它才会完成,即使有错误发生。如果有多个错误,它们的异常会包裹在 Task 的 AggregationException里
- await 组合的 Task ,只会抛出第一个异常,想要看到所有的异常,你需要这样做
Task task1 = Task.Run(() => {throw null;});
Task task2 = Task.Run(() => {throw null;});
Task all = Task.WhenAll(task1, task2);
try {
await all;
}
catch {
Console.WriteLine(all.Exception.InnerExceptions.Count) // 2
}
- 对一组 Task< TResult> 调用 WhenAll 会返回 Task< TResult[]>,也就是所有 Task 的组合结果
- 如果进行 await,那么就会得到 TResult[]
Task<int> task1 = Task.Run(() => 1);
Task<int> task2 = Task.Run(() => 2);
int[] results = await Task.WhenAll(task1, task2);
// [1, 2]