一、什么是异步方法
异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。
特点:
- 方法头中包含 async 方法修饰符。
- 包含一个或多个 await 表达式,表示可以异步完成的任务。
- 必须具备3种返回类型之一:
- void
- Task
- Task<T>
- ValueTask<T>
- 任何具有公开可访问的 GetAwaiter 方法的类型。
- 异步方法的形参可以为任意类型、任意数量,但不能为 out 或 ref 参数。
- 按照约定,异步方法的名称应该以 Async 为后缀。
- 除了方法以外, Lambda 表达式和匿名方法也可以作为异步对象。
async Task<int> CountCharactersAsync(int id,string site)
{
Console.WriteLine("Starting CountCharacters");
WebClient wc = new WebClient();
//await 表达式
string result = await wc.DownloadStringTaskAsync( new Uri(site) );
//Lambda 表达式
// var result = await Task.Run(() => wc.DownloadString(new Uri(site)));
return result = Length;
}
async 关键字
- 异步方法的方法头必须包含 async 关键字,且必须位于返回类型之前。
- 该修饰符只是标识该方法包含一个或多个 await 表达式。它本身并不能创建任何异步操作。
- async 关键字是一个上下文关键字。除了作为方法修饰符(或 Lambda 表达式修饰符、匿名方法修饰符)之外,async 还可用作标识符。(比如 int async = 0;)
返回类型:
- 1)Task: 如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个 Task 类型的对象。如果异步方法中包含任何 return 语句,则它们不能返回任何东西:
Task someTask = DoStuff.CalculateSumAsync(5,6);
...
someTask.Wait();
- 2)ask<T>: 如果调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task<T>。调用方法将通过读取 Task 的 Result 属性来获取整个 T 类型的值。
Task<T> value = DoStuff.CalcuateSumAsync(5,6);
...
Console.WriteLine($"Value:{ value.Result }");
- 3)ValueTask<T>: 这是一个值类型对象,它与 Task<Tgt; 类似,但用于任务结果可能已经可用的情况。因为它是一个值类型,所以它可以放在栈上。,而无须像 Task<T> 对象那样在堆上分配空间。
class Program
{
static void Main(string[] args)
{
ValueTask<int> value = DoAsynStuff.CalculateSumAsync(0, 6);
//处理其他事情
Console.WriteLine($"Value:{ value.Result }");
value = DoAsynStuff.CalculateSumAsync(5, 6);
//处理其他事情
Console.WriteLine($"Value:{ value.Result }");
Console.ReadKey();
}
static class DoAsynStuff
{
public static async ValueTask<int> CalculateSumAsync(int i1,int i2)
{
// 如i1 == 0,则可以避免执行长时间运行的任务
if(i1 == 0)
{
return i2;
}
int sum = await Task<int>.Run(() => GetSum(i1, i2));
return sum;
}
private static int GetSum(int i1,int i2){ return i1 + i2; }
}
}
我的编程环境不支持ValueTask<T>,查了相关资料可知,可能.NET 5.0 版本及以上才支持,而我电脑里最高版本才 4.6。算了,我就不测试了,先把例子贴上去,以后有条件了(不想再花时间安装 .NET 框架)再研究。
- 4)void: 如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时,异步方法可以返回 void 类型。如果异步方法中包含任何 return 语句,则它们不能返回任何东西。(这跟返回类型 Task 一样,包含 await时,等待任务完成之前,还可以执行下一个代码块,否则,等待任务完成才能执行下一步)
void 和 Task 的区别代码例子:
class MyDownloadString
{
const string Str = "https://www.baidu.com/";
Stopwatch sw = new Stopwatch();
public async void DoRun()
{
sw.Start();
await returnTaskValue();
Console.WriteLine("----------:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
returnVoidValue();
Console.WriteLine("++++++++++:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
Thread.Sleep(500);
Console.WriteLine();
Console.WriteLine("暂停线程,为的是测试Task<string>,跟 Task 和 void 的无关:");
Task<string> t = CountCharacters("Task<string>");
Console.WriteLine("**********:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
//总结:
//1、如果 Task 和 void 异步方法里有 await,则方法内需要等待完成才能返回
//假设 Task 和 void 异步方法内有 await:
//2、在同层级方法内调用异步方法,Task 方法 添加 await 等待任务完成才能执行下一个语句
//void 方法并不会等待任务完成,而继续执行下一个语句
//3、 Task<string> 在同一层方法被调用,则执行异步处理。跟 void 一样。
//4、同一层方法作为调用者,如果被调用者为异步方法时:
//执行 void 和 Task<string>方法,不需要 await,则执行异步处理。
//执行 Task 方法 需要 await 执行等待任务完成,才能执行下一个语句;否则,就异步处理。
}
private async Task returnTaskValue()
{
Console.WriteLine("Task_Start:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
var result = await Task.Run(() => CountCharacters("Task"));
Console.WriteLine("Task_End:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
}
private async void returnVoidValue()
{
Console.WriteLine("Void_Start:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
var result = await Task.Run(() => CountCharacters("Void"));
Console.WriteLine("Void_End:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
}
private async Task<string> CountCharacters(string strId)
{
WebClient wc1 = new WebClient();
Console.WriteLine("{0}_DownloadBefore:{1,4:N0}", strId, sw.Elapsed.TotalMilliseconds);
var result = await wc1.DownloadStringTaskAsync(new Uri("https://www.baidu.com/"));
Console.WriteLine("{0}_DownloadAfter:{1,4:N0}", strId, sw.Elapsed.TotalMilliseconds);
return result;
}
}
输出结果:
>Task_Start: 1
Task_DownloadBefore: 68
Task_DownloadAfter: 420
Task_End: 435
----------: 435
Void_Start: 436
Void_DownloadBefore: 437
++++++++++: 440
Void_DownloadAfter: 469
Void_End: 469
暂停线程,为的是测试Task<string>,跟 Task 和 void 的无关:
Task<string>_DownloadBefore: 950
**********: 952
Task<string>_DownloadAfter: 983
void 返回类型使用“调用并忘记”的异步方法:
class Program
{
static void Main(string[] args)
{
DoAsynStuff.CalculateSumAsync(5, 6);
//如果参数值为1,则先等待void 方法执行完成之后才能执行下一个语句
//这样做法会有安全隐患,适合 void 异步方法处理时间短,Sleep 参数值设置足够大
Thread.Sleep(200);
Console.WriteLine("Program Exiting");
Console.ReadKey();
}
static class DoAsynStuff
{
public static async void CalculateSumAsync(int i1,int i2)
{
int value = await Task.Run(() => GetSum(i1, i2));
Console.WriteLine("Value:{0}", value);
}
private static int GetSum(int i1,int i2)
{
return i1 + i2;
}
}
}
- 任何具有可访问的 GetAwaiter 方法的类型。
二、异步方法的控制流
异步方法的结构包含三个不同区域:
- await 表达式之前的部分
- await 表达式:异步执行的任务
- await 表达式之后的部分
图-阐明了一个异步方法的控制流
目前有两个控制流: 一个在异步方法内,一个在调用方法内。
当后续部分遇到 retun 语句或到达方法末尾时:
- 如果方法的返回类型为 void,控制流将退出。
- 如果方法的返回类型为 Task,则后续部分设置 Task 的状态属性并退出。如果返回类型为
Task<T> 或 ValueTask<T>,则后续部分还将设置对象的 Result 属性。
1、await 表达式
await 表达式指定了一个异步执行的任务。
await task //task:空闲对象
一个空闲对象即是一个 awaitable 类型的实例。 awaitable 类型是指包含 GetAwaiter 方法的类型,该方法没有参数,返回一个 awaiter 类型的对象。
awaiter 类型包含以下成员:
bool IsCompleted{ get; }
void OnCompleted(Action);
void GetResult();
T GetResult();
2、关于 Task.Run:
使用 Task.Run 方法来创建一个 Task。
//是以 Func<TReturn> 委托为参数
public static Task Run(Func<TReturn> function);
//还有其他重载的类型
...
创建委托三种实现方式:
class MyClass
{
public int Get10()
{
return 10;
}
public async Task DoWorkAsync()
{
Func<int> ten = new Func<int>(Get10);
int a = await Task.Run(ten);
int b = await Task.Run(new Func<int>(Get10));
int c = await Task.Run(()=> { return 10; });
Console.WriteLine($"{ a } { b } { c }");
}
}
class Program
{
static void Main(string[] args)
{
Task t = (new MyClass()).DoWorkAsync();
t.Wait();
Console.ReadKey();
}
}
输出结果:
10 10 10
表-Task.Run 重载的返回类型和签名
返回类型 | 签名 |
---|---|
Task | Run( Action action) |
Task | Run( Action action, CancellationToken token ) |
Task<TResult> | Run( Func<TResult> function ) |
Task<TResult> | Run( Func<TResult> function, CancellationToken token) |
Task | Run( Func<Task> function ) |
Task | Run( Func<Task> function, CancellationToken token ) |
Task<TResult> | Run( Func<Task<TResult>> function ) |
Task<TResult> | Run( Func<Task<TResult>> function, CancellationToken token ) |
表-可作为 Task.Run 方法第一个参数的委托类型
委托类型 | 签名 | 含义 |
---|---|---|
Action | void Action() | 不需要参数且无返回值的方法 |
Func<TResult> | TResult Func() | 不需要参数,但返回 TRsult 类型对象的方法 |
Func<Task> | Task Func() | 不需要参数,但返回简单 Task 对象的方法 |
Func<Task<TResult>> | Task<TResult> Func() | 不需要参数,但返回 Task<T> 类型对象的方法 |
4个 await 语句的示例:
static class MyClass
{
public static async Task DoWorkAsync()
{
//Run(Action)
await Task.Run(() => Console.WriteLine(5.ToString()));
//Run(TResult Func())
Console.WriteLine((await Task.Run(() => 6)).ToString());
//Run(Task Func())
await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));
//Run(Task<TResult> Func())
int value = await Task.Run(() => Task.Run(() => 8));
Console.WriteLine(value.ToString());
}
}
class Program
{
static void Main(string[] args)
{
Task t = MyClass.DoWorkAsync();
t.Wait();
Console.WriteLine("Press Enter key to exit");
Console.ReadKey();
}
}
Lambda 函数() = > GetSum(5,6) 满足 Func<TResult> 委托
int value = awaite Task.Run(() => GetSum(5,6));
三、取消一个异步操作
一些 .NET 异步方法允许你请求终止执行。
System.Threading.Tasks 命名空间中有两个类是为此目的而设计的:CancellationToken 和 CancellationTokenSource。
- CancellationToken 对象包含一个任务是否应被取消的信息。
- 拥有 CancellationToken 对象的任务需要定期检查其令牌(token)状态。如果 CancellationToken 对象的 IsCancellationRequested 属性为 true,任务需停止其操作并返回。
- CancellationToken 是不可逆的,并且只能使用一次。也就是说,一旦 IsCancellationRequested 属性被设置为 true,就不能更改了。
- CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象。任务持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法,这会将 CancellationTToken 的 IsCancellationRequested 属性设置为 true。
使用这两个类,触发取消行为:
class Program
{
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
MyClass mc = new MyClass();
Task t = mc.RunAsync(token);
//Thread.Sleep(3000);
//cts.Cancel();
t.Wait();
Console.WriteLine($"Was Cancelled:{ token.IsCancellationRequested }");
Console.ReadKey();
}
class MyClass
{
public async Task RunAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
return;
await Task.Run(() => CycleMethod(ct), ct);
}
void CycleMethod(CancellationToken ct)
{
Console.WriteLine("Starting CycleMethod");
const int max = 5;
for(int i = 0; i < max;i++)
{
if (ct.IsCancellationRequested)
return;
Thread.Sleep(1000);
Console.WriteLine($"{ i + 1 } of { max } iterations completed");
}
}
}
}
输出结果:
Starting CycleMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
4 of 5 iterations completed
5 of 5 iterations completed
Was Cancelled:False
若在 Main 方法里恢复执行这两行代码:
Thread.Sleep(3000);//3秒后取消任务执行
cts.Cancel();
输出结果:
Starting CycleMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
Was Cancelled:True
四、在调用方法中同步地等待任务
你的代码会继续执行其他任务,但在某个点上可能会需要等待某个特殊 Task 对象完成,然后再继续。
static class MyDownloadString
{
public static void DoRun()
{
Task<int> t = CountCharactersAsync("http://illustratedcsharp.com");
t.Wait();//等待任务 t 结束
Console.WriteLine($"The task has finished,returning value { t.Result }.");
}
private static async Task<int> CountCharactersAsync(string site)
{
string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
return result.Length;
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString.DoRun();
Console.ReadKey();
}
}
输出结果:
The task has finished,returning value 5164.
五、Wait 方法用于单一 Task 对象。
1、使用 WaitAll 和 WaitAny
对于一组 Task ,可以等待所有任务都结束,也可以等待某一个任务结束。
- WaitAll
- WaitAny
class MyDownloadString
{
Stopwatch sw = new Stopwatch();
public void DoRun()
{
sw.Start();
Task<int> t1 = CountCharactersAsync(1,"http://illustratedcsharp.com");
Task<int> t2 = CountCharactersAsync(2, "http://illustratedcsharp.com");
//Task.WaitAll(t1, t2);
//Task.WaitAny(t1, t2);
Console.WriteLine("Task 1: {0} Finished",t1.IsCompleted?"":"Not");
Console.WriteLine("Task 2: {0} Finished", t2.IsCompleted ? "" : "Not");
Console.Read();
}
private async Task<int> CountCharactersAsync(int id,string site)
{
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync(new Uri(site));
Console.WriteLine(" Call {0} completed: {1,4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
return result.Length;
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
Console.ReadKey();
}
}
输出结果:
不使用 WaitAll 和 WaitAny 方法时,t1 和 t2 异步执行。
Task 1: Not Finished
Task 2: Not Finished
Call 1 completed: 598 ms
Call 2 completed: 600 ms
1)如果在 DoRun 方法里,恢复 Task.WaitAll(t1, t2); 代码:
Task.WaitAll(t1, t2);
//Task.WaitAny(t1, t2);
输出结果:
使用了 WaitAll 方法后,所有任务都要等待完成,之后代码才能继续执行。
Call 2 completed: 695 ms
Call 1 completed: 617 ms
Task 1: Finished
Task 2: Finished
2)如果在 DoRun 方法里,恢复 Task.WaitAny(t1, t2); 代码:
//Task.WaitAll(t1, t2);
Task.WaitAny(t1, t2);
输出结果:
使用了 WaitAll 方法后,至少有一个 Task 需要等待完成才能继续执行。
Call 2 completed: 659 ms
Call 1 completed: 658 ms
Task 1: Not Finished
Task 2: Finished
2、WaitAll 和 WaitAny 分别还包含4个重载。
签名 | 描述 |
---|---|
WaitAll | – |
void WaitAll(params Task[] tasks) | 等待所有任务完成 |
bool WaitAll(Task[] tasks, int millisecondsTimeout) | 等待所有任务完成。如果在超时时限内没有全部完成,则返回 false 并继续执行 |
void WaitAll(Task[] tasks, CancellationToken token) | 等待所有任务完成,或等待 CancellationToken 发出取消信号 |
bool WaitAll(Task[] tasks, TimeSpan span) | 等待所有任务完成。如果在超时时限内没有全部完成,则返回 false 并继续执行 |
bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken token) | 等待所有任务完成,或等待 CancellationToken 发出取消信号。如果在超时时限内没有发生上述情况,则返回 false 并继续执行 |
WaitAny | – |
void WaitAny(params Task[] tasks) | 等待任意一个任务完成 |
bool WaitAny(Task[] tasks, int millisecondsTimeout) | 等待任意一个任务完成。如果在超时时限内没有完成的,则返回 false 并继续执行 |
void WaitAny(Task[] tasks, CancellationToken token) | 等待任意一个任务完成,或等待 CancellationToken 发出取消信号 |
bool WaitAny(Task[] tasks, TimeSpan span) | 等待任意一个任务完成。如果在超时时限内没有完成的,则返回 false |
bool WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken token) | 等待任意一个任务完成,或等待 CancellationToken 发出取消信号。如果在超时时限内没有发生上述情况,则返回 false 并继续执行 |
3、在异步方法中异步地等待任务
可以通过 Task.WhenAll 和 Task.WhenAny 方法,来实现在异步方法中等待一个或所有任务完成。这两个方法称为组合子。
class MyDownloadString
{
Stopwatch sw = new Stopwatch();
public void DoRun()
{
sw.Start();
Task<int> t = CountCharactersAsync("http://www.baidu.com", "http://illustratedcsharp.com");
Console.WriteLine("DoRun: Task {0}Finshed", t.IsCompleted ?"":"Not");
Console.WriteLine("DoRun: Result = {0}", t.Result);
}
private async Task<int> CountCharactersAsync(string site1,string site2)
{
WebClient wc1 = new WebClient();
WebClient wc2 = new WebClient();
Task<string> t1 = wc1.DownloadStringTaskAsync(new Uri(site1));
Task<string> t2 = wc2.DownloadStringTaskAsync(new Uri(site2));
List<Task<string>> tasks = new List<Task<string>>();
tasks.Add(t1);
tasks.Add(t2);
await Task.WhenAll(tasks);//关键代码语句
Console.WriteLine(" CCA: T1 {0}Finished", t1.IsCompleted ? "" : "Not");
Console.WriteLine(" CCA: T2 {0}Finished", t2.IsCompleted ? "" : "Not");
return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
Console.ReadKey();
}
}
输出结果:
在异步方法内,需要等待所有任务完成后,之后代码才能继续执行。
DoRun: Task NotFinshed
CCA: T1 Finished
CCA: T2 Finished
DoRun: Result = 9269
若把 Task.WhenAll 方法 改为 Task.WhenAny 方法:
输出结果:
在异步方法内,至少有一个任务需要等待完成,之后代码才能继续执行。
DoRun: Task NotFinshed
CCA: T1 Finished
CCA: T2 NotFinished
DoRun: Result = 9269
六、Task.Delay 方法
Thread.Sleep 会阻塞线程,而 Task.Delay 不会阻塞线程,线程还可以继续处理其他工作。
class Simple
{
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 }");
}
}
class Program
{
static void Main(string[] args)
{
Simple ds = new Simple();
ds.DoRun();
Console.ReadKey();
}
}
输出结果:
Caller:Before call
Before Delay:0
Caller:After call
After Delay : 1052
Delay 方法包含4个重载,允许以不同方法来指定时间周期,同时还允许使用 CancellationToken 对象。
签名 | 描述 |
---|---|
Task Delay(int millisecondsDelay) | 在以毫秒表示的延迟时间到期后,返回完成的 Task 对象 |
Task Delay(TimeSpan delay) | 在以 .NET TimeSpan 对象表示的延迟时间到期后,返回完成的 Task 对象 |
Task Delay(int millisecondsDelay, CancellationToken token) | 在以毫秒表示的延迟时间到期后,返回完成的 Task 对象。可通过取消令牌来取消该操作 |
Task Delay(TimeSpan delay, CancellationToken token) | 在以 .NET TimeSpan 对象表示的延迟时间到期后,返回完成的 Task 对象。可通过取消令牌来取消该操作 |