[C#图解教程]笔记 - 20.异步编程
什么是异步
启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。
在进程内部,系统创建了一个称为线程的内核(kernel)对象。
关于线程:
- 默认情况下,一个进程只包含一个线程,从程序的开始一直到执行到结束。
- 线程可以派生其他线程,一个进程可能包含不同状态的多个线程。
- 如果一个进程有多个线程,它们将共享进程的资源。
- 系统为处理器执行所调度的单元是线程,不是进程。
async/await
异步的方法在完成其所有工作之前就返回到调用方法。
- 调用方法(calling method):该方法调用异步方法,然后再异步方法执行其任务的时候继续执行。
- 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法。
- await 表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个 await 表达式,如果一个都不包含的话编译器会发出警告。
异步方法
- 异步方法的头方法中必须包含 async 关键字,且必须位于返回类型之前。
- async 关键字是一个上下文关键字,除了作为方法修饰符之外,还可以用作标识符。
- 异步方法的返回类型:
- Task:不需要返回值,但需要检查异步方法的状态
- Task<T>:调用方法通过读取 Task 的Result 属性来获取这个 T 类型的值。
- ValueTask<T>:这是一个值类型对象,他与 Task<T> 类似,但用于任务结果可能已经可用的情况。因为它是值类型,所以它可以放在栈上,而无须像 Task<T> 对象那样再堆上分配空间。某种情况下可以提高性能。
- void:调用方法仅想执行异步,而不需要与他做任何进一步的交互时。
- 任何具有可访问的 GetAwaiter 方法的类型。
await 表达式
await 表达式指定了一个异步执行的任务,由 await 关键字和一个空闲对象(称为任务,可能是 Task 类型,也可能不是)组成。默认情况下,这个任务在当前线程上异步执行。
一个空闲对象即是 awaitable 类型的实例。awaitable 类型包含了 GetAwaiter 方法,该方法没有参数,返回一个 awaiter 类型的对象。
awaiter 类型包含如下成员:
bool IsCompleted { get; }
-
void OnCompleted(Action)
同时还包含以下成员之一: void GetResult();
-
T GetResult();
T 为任意类型
实际上,不需要构建自己的 awaitable,而是使用 Task 类或 ValueTask 类,它们是 awaitable 类型。通过 Task.Run 方法来创建 Task,其签名如下。其中 Func<TReturn> 是一个预定义委托,不包含任何参数,返回类型为 TReturn。
取消一个异步操作
System.Threading.Tasks 命名空间中有两个类被设计为取消异步操作,分别为 CancellationToken 和 CancellationTokenSource。
- CancellationToken 包含一个任务是否应被取消的信息 IsCancellationRequested。
- 拥有 CancellationToken 对象的任务需要定期检查其令牌状态,如果 IsCancellationRequested 属性为 true,任务需停止其操作并返回。
- CancellationToken 不可逆,且只能使用一次。即,一旦 IsCancellationRequested 被设置为 true,就不能更改。
- CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象,任何持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法,这将使 IsCancellationRequested 被设置为 true。
异常处理和 await 表达式
await 表达式可以像其他表达式一样放在 try 语句内,try … catch … finally 结构将按你期望的那样工作。
C#6.0 后也可以在 catch 和 finally 块中使用 await 表达式。在异常不需要终止应用程序时,可以使用 await 来记录日志或运行其他时间较长的任务。如果新的异步任务也产生了一场,则任何原有的异常信息都将丢失。
在调用方法中同步地等待任务
使用 Wait 方法可以等待 Task 对象完成。
Wait 方法用于单一 Task 对象。
可以使用 Task 类中的静态方法等待一组 Task 对象完成。
- WaitAll :等待一组 Task 对象全部完成。
- WaitAny:等待一组中至少一个 Task 对象完成。
WaitAll 和 WaitAny 分别包含 4 个重载,除了完成任务之外,还允许以不同的方式继续执行,如设置超时时间或使用 CancellationToken 来强制执行处理的后续部分。
在异步方法中异步地等待任务
使用 Task.WhenAll 和 Task.WhenAny 异步等待多个 Task。这两个方法称为组合子(combinator)。
Task.Delay 方法
Task.Delay 方法创建一个 Task 对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。
- Thread.Sleep:阻塞线程。
- Task.Delay:不阻塞线程,线程可以继续处理其他工作。
例:
public class Simple
{
private Stopwatch sw = new Stopwatch();
public void DoRun()
{
Console.WriteLine("Caller: Before call");
ShowDelayAsync();
Console.WriteLine("Caller: After call");
}
private async void ShowDelayAsync()
{
sw.Start();
Console.WriteLine("Before Delay:"+sw.ElapsedMilliseconds);
await Task.Delay(1000);
Console.WriteLine("After Delay:"+sw.ElapsedMilliseconds);
}
}
public class Program
{
public static Main(string[] args)
{
Simple ds = new Simple();
ds.DoRun();
Console.ReadLine();
}
}
执行结果:
Caller: Before call
Before Delay:0
Caller: After call
After Delay:1003
Task.Yield
Task.Yield 方法创建一个立即返回的 awaitable。 等待一个 Yield 可以让异步方法在执行后续部分的同时返回到调用方法。可以将其理解成离开当前的消息队列,回到队列末尾,让处理器有时间处理其他任务。
使用异步 Lambda 表达式
除了异步方法,还可以使用异步匿名方法和异步 Lambda 表达式。
例:
startWorkButton.Click += async (sender, e) => { ... };
BackgroundWorker 类
可以使用 BackgroundWorker 类创建后台线程。
并行循环
任务并行库(Task Parallel Library)
这里只介绍两个:
- Parallel.For 循环
- Parallel.ForEach 循环
其他异步编程
当委托调用时,他的调用列表只有一个方法,它就可以异步执行这个方法。
- BeginInvoke:
- 参数列表:引用方法需要的参数,callback 参数,state 参数
- BeginInvoke 从线程池中获取一个线程并且引用方法在新的线程中开始运行,返回给调用线程一个实现 IAsyncResult 的接口的对象的引用。这个接口引用包含了在线程池中运行的异步方法的当前状态。然后原始线程可以继续执行。
- EndInvoke:
- 参数列表:IAsyncResult 对象的引用
- EndInvoke 方法用来获取由异步方法调用返回的值,并且释放线程使用的资源。
- EndInvoke 提供了异步方法调用的所有输出,包括 ref 和 out 参数,如果委托的引用方法有 ref 或 out 参数,则它们必须包含在 EndInvoke 的参数列表中,并且在 IAsyncResult 对象引用之前。如:
long result = del.EndInvoke(out someInt, iar);
。
异步方法调用的三种标准模式
- 在等待直到完成模式中,在发起了异步方法以及做了一些其他处理之后,原始线程就中断并且等异步方法完成之后再继续。
- 在轮询模式中,原始线程定期检查发起的线程是否完成,如果没有则可以继续做一些其他的事情。
- 在回调模式中,原始线程一直执行,无须等待或检查发起的线程是否完成。