async/await特性结构
由三部分组成
- 调用方法:使用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
- 异步方法(async):该方法异步执行其工作,然后立即返回到调用方法。
- await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。
异步方法
异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作
特点
- 方法头中包含
async
方法修饰符。 - 包含一个或多个await表达式,表示可以异步完成的任务。
- 必须具备以下三种返回类型。第二种(Task)和第三种(Task
)的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。 - void
- Task
- Task
- 异步方法的参数可以为任意类型任意数量,但不能为out或ref参数。
- 异步方法的名称应该以Async为后缀。
- 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象。
例
//async关键字 Task<int>返回值
async Task<int> CountCharactersAsync(int id, string site)
{
Console.WriteLine("CountCharacters开始");
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync(new Uri(site)); //await表达式
return result.Length;
}
分析
1.异步方法在方法头中必须包含
async
关键字,且必须出现在返回类型之前。
2.该修饰符只是标识该方法包含一个或多个await表达式。也就是说,它本身并不能创建任何异步操作。
3.async关键字是一个上下文关键字,也就是说除了作为方法修饰符(或Lambda表达式修饰符、匿名方法修饰符)之外,async还可用作标识符。
4.返回类型必须是一下三种类型之一:
- Task
:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task 。调用方法将通过读取Task的Result属性来获取这个T类型的值。下面的代码来自一个调用方法,阐明了这一点: Task<ints value = DoStuff.CalculateSumAsync(5, 6); .... Console.writeLine( "value: {0}",value.Result );
- Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句,也不会返回任何东西。下面的代码同样来自调用方法:
Task someTask = DoStuff.calculateSumAsync(S, 6); .... someTask.wait();
- void:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时[这称为“调用并忘记”],异步方法可以返回void类型。这时,与上一种情况类似,即使异步方法中包含任何return语句,也不会返回任何东西。
- 任何返回Task
类型的异步方法其返回值必须为T类型或可以隐式转换为T的类型。如上面例子
实例1
使用返回Task
static class DoAsyncStuff
{
static private int GetSum(int i1, int i2)
{
return i1 + i2;
}
public static async Task<int> CalculateSumAsync(int i1,int i2)
{
//Task.Run()创建Task
int sum = await Task.Run(() => GetSum(i1, i2));
return sum;
}
}
class Program
{
static void Main(string[] args)
{
Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
Console.WriteLine($"Value:{value.Result}");
Console.ReadKey();
}
}
实例2
使用返回Task对象的异步方法
static class DoAsyncStuff
{
static private int GetSum(int i1, int i2)
{
return i1 + i2;
}
public static async Task CalculateSumAsync(int i1,int i2)
{
//Task.Run()创建Task
int value = await Task.Run(() => GetSum(i1, i2));
Console.WriteLine($"Value:{value}");
}
}
class Program
{
static void Main(string[] args)
{
Task someTask = DoAsyncStuff.CalculateSumAsync(5, 6);
someTask.Wait();
Console.ReadKey();
}
}
实例3
使用"调用并忘记"的异步方法
using System.Threading;
using System.Threading.Tasks;
static class DoAsyncStuff
{
static private int GetSum(int i1, int i2)
{
return i1 + i2;
}
public static async void CalculateSumAsync(int i1,int i2)
{
//Task.Run()创建Task
int value = await Task.Run(() => GetSum(i1, i2));
Console.WriteLine($"Value:{value}");
}
}
class Program
{
static void Main(string[] args)
{
DoAsyncStuff.CalculateSumAsync(5, 6);
Thread.Sleep(200);
Console.ReadKey();
}
}
异步方法控制流
流程
- 异步执行await表达式的空闲任务。
- 当await表达式完成时,执行后续部分。后续部分本身也可能包含其他await表达式,这些表达式也将按照相同的方式处理,即异步执行await表达式,然后执行后续部分。
- 当后续部分遇到return语句或到达方法末尾时,将:
- 如果方法返回类型为void,控制流将退出。
- 如果方法返回类型为
Task
,后续部分设置Task的属性并退出。如果返回类型为TaskcT>
,后续部分还将设置Task对免的Recult属性
await表达式
指定一个异步执行的任务
语法
由await关键字和一个空闲对象(任务)组成
这个任务可能是一个Task对象,也可能不是
await task
一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员:
- bool IsCompleted{ get;}
- void OnCompleted(Action);
- 它还包含以下成员之一:
- void GetResult();
- T GetResult();(T为任意类型)
无需使用awaitable
,使用Task
即可,因为task
就是awaitable
类型
Task
异步操作
Run
Task.Run不同线程上运行方法
方法名
//Func<TReturn>委托参数
Task Run(Func<TReturn> func)
实例1
class MyClass
{
public int Get10() //与Func<int>兼容
{
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("{0} {1} {2}", a, b, c);
}
}
class Program
{
static void Main(string[] args)
{
Task t = (new MyClass()).DoworkAsync();
t.Wait(); //等待所有task对象执行过程
Console.ReadKey();
}
}
分析
第一个实例(DoworkAsync方法的前两行)使用Get1o创建名为ten的
Func<int>
委托。然后在下一行将该委托用于Task.Run方法。
第二个实例在Task.Run方法的参数列表中创建Func<int``委托。 第三个实例没有使用Get10方法。而是使用了组成Get10方法的return语句,将其用于与
Func`委托兼容的Lambda表达式。该Lambda表达式将隐式转换为该委托。
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() | 不需要参数返回TResult类型对象的方法 |
Func<Task> | Task Func() | 不需要参数返回简单Task对象的方法 |
Func<Task<TRsult>> | Task<TResult> Func() | 不需要参数返回Task<T> 类型对象的方法 |
实例2
static class MyClass
{
public static async Task DoworkAsync()
{
//Action
await Task.Run(() => Console.WriteLine(5.ToString()));
//TResult Func()
Console.WriteLine((await Task.Run(() => 6)).ToString());
//Task Func()
await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));
//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(); //等待所有task对象执行过程
Console.WriteLine("退出");
Console.ReadKey();
}
}
结果
5
6
7
8
退出
分析
第一个和第三个实例将await表达式用作语句。
第二个实例将await表达式用作writeline方法的参数。
第四个实例将await表达式用作赋值语句的右端。
使用Lambda表达式
上面有讲过,异步方法的实例3
static class MyClass
{
private static int GetSum(int t1, int i2) {
return i1 + i2;
}
public static async Task DoworkAsync()
{
int value = await Task.Run(() => GetSum(5, 6)));
Console.WriteLine(value.ToString());
}
}
class Program
{
static void Main(string[] args)
{
Task t = MyClass.DoworkAsync();
t.Wait(); //等待所有task对象执行过程
Console.WriteLine("退出");
Console.ReadKey();
}
}
结果
11
退出
取消异步操作
using System.Threading.Tasks;命名空间
CancellationToken和CancellationTokenSource
规则
CancellationToken
对象包含一个任务是否应被取消的信息。- 拥有
CancellationToken
对象的任务需要定期检查其令牌(token)状态。如果CancellationToken
对象的IsCancellationRequested
属性为true,任务需停止其操作并返回。CancellationToken
是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested
属性被设置为true,就不能更改了。 CancellationTokenSource
对象创建可分配给不同任务的CancellationToken
对象。任何CancellationTokenSource的对象都可以调用其Cancel
方法,这会将CancellationToken
的IsCancellationRequested
属性设置为true。
实例
下面的代码展示了如何使用CancellationTokenSource和CancellationToken来实现取消操作。注意,该过程是协同的。即调用cancellationTokenSource的Cancel时,它本身并不会执行取消操作。而是会将CancellationToken的IsCancellationRequested属性设置为true。包含CancellationToken的代码负责检查该属性,并判断是否需要停止执行并返回。
class MyClass
{
public async Task RunAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
//监控任务是否停止
return;
await Task.Run(() => CycleMethod(ct), ct);
}
void CycleMethod(CancellationToken ct)
{
Console.WriteLine("CycleMethod开始");
const int max = 5;
for(int i = 0; i < max; i++)
{
if(ct.IsCancellationRequested)
{
//监控任务是否停止
return;
}
Thread.Sleep(1000); //线程挂起时间
Console.WriteLine(" {0} of {1} iterations completed", i + 1, max);
}
}
}
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); //等待3秒
//cts.Cancel(); //取消操作
t.Wait(); //等待全部执行
Console.WriteLine($"是否取消操作:{token.IsCancellationRequested}");
Console.ReadKey();
}
}
结果
保留注释
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
是否取消操作:False
取消注释
CycleMethod开始
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
是否取消操作:True
异常处理
将await表达式放在try语句内,try...catch...finally结构
实例
static async Task BadAsync()
{
try
{
await Task.Run(() => { throw new Exception(); });
}
catch
{
Console.WriteLine("Exception in BadAsync");
}
}
static void Main(string[] args)
{
Task t = BadAsync();
t.Wait();
Console.WriteLine("Task Status:{0}", t.Status); //表示 Task 的生命周期中的当前阶段。
Console.WriteLine("Task IsFaulted:{0}", t.IsFaulted); //获取 Task 是否由于未经处理异常的原因而完成。
Console.ReadKey();
}
结果
运行时,会抛出异常,继续又可以运行
Exception in BadAsync
Task Status:RanToCompletion
Task IsFaulted:False
在调用方法中同步等待任务
调用方法可以调用任意多个异步方法并接收它们返回的Task对象
实例
下面的示例展示了其用法。在代码中,调用方法DoRun调用异步方法CountCharactersAsync并接收其返回的Taskint>。然后调用Task实例的wait方法,等待任务Task结束。等结束时再显示结果信息。
static class MyDownloadString
{
public static void DoRun()
{
Task<int> t = CountCharacterAsync("https://www.lhuanblog.com");
//等待任务结束
t.Wait();
Console.WriteLine("任务完成,返回:{0}", t.Result);
}
private static async Task<int> CountCharacterAsync(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();
}
}
wait
用于等待任务完成
wait方法用于单一Task对象。而你也可以等待一组Task对象。
对于一组Task,可以等待所有任务都结束,也可以等待某一个任务结束。
实现这两个功能的是Task类中的两个静态方法:
- WaitAll
- WaitAny
没有返回值,直到满足条件再继续执行
实例2
下面代码,包含一个DoRun方法,两次调用一个异步方法并获取其返回两个Taskint>对象。然后,方法继续执行,检查任务是否完成并打印。方法最后会等待调用Console.Read,该方法等待并接收键盘输入的字符。
注释部分包含等待代码
class MyDownloadString
{
Stopwatch sw = new Stopwatch(); //测量时间
public void DoRun()
{
sw.Start();
Task<int> t1 = CountCharacterAsync(1, "https://www.lhuanblog.com");
Task<int> t2 = CountCharacterAsync(2, "https://www.microsoft.com");
//Task<int>[] tasks = new Task<int>[] { t1, t2 };
//Task.WaitAll(tasks);
//Task.WaitAny(tasks);
Console.WriteLine("Task 1 是否完成:{0}", t1.IsCompleted ? "是" : "否");
Console.WriteLine("Task 2 是否完成:{0}", t2.IsCompleted ? "是" : "否");
Console.Read();
}
private async Task<int> CountCharacterAsync(int id,string site)
{
string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
Console.WriteLine("id: {0} 已完成 : {1, 4} ms", id, sw.Elapsed.TotalMilliseconds);
return result.Length;
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
Console.ReadKey();
}
}
结果
Task 1 是否完成:否
Task 2 是否完成:否
id: 1 已完成 : 244.0255 ms
id: 2 已完成 : 1644.0659 ms
分析
可以看到没有一个是完成的(CountCharacterAsync未执行完),就直接进行下一步
如果把注释部分去掉
Task<int>[] tasks = new Task<int>[] { t1, t2 };
Task.WaitAll(tasks);
//Task.WaitAny(tasks);
结果
id: 1 已完成 : 243.06 ms
id: 2 已完成 : 3684.7435 ms
Task 1 是否完成:是
Task 2 是否完成:是
换一个
Task<int>[] tasks = new Task<int>[] { t1, t2 };
// Task.WaitAll(tasks);
Task.WaitAny(tasks);
结果
id: 1 已完成 : 225.2587 ms
Task 1 是否完成:是
Task 2 是否完成:否
id: 2 已完成 : 585.0498 ms
waitAll和WaitAny重载方法
方法名 | 描述 |
---|---|
void WaitAll(params Task[] tasks) | 等待所有任务完成 |
void WaitAll(params Task[] tasks, int millisecondsTimeout) | 等待所有任务完成。如果在超时时限内没有全部完成,则继续执行 |
void WaitAll(params Task[] tasks, CancelationToken token) | 等待所有任务完成,或CancellationToke发出了取消的信号 |
void WaitAll(params Task[] tasks, TimsSpan span) | 等待所有任务完成。如果在超时时限内没有全部完成,则继续执行 |
void WaitAll(params Task[] tasks, int millisecondsTimeout, CancelationToken token) | 等待所有任务完成,或CancellationToken发出了取消的信号。如果在超时时限内没有发生上述情况,则继续执行 |
void WaitAny(params Task[] tasks) | 等待任一个任务完成 |
void WaitAny(params Task[] tasks, int millisecondsTimeout) | 等待任一个任务完成。如果在超时时限内没有完成的,则继续执行 |
void WaitAny(params Task[] tasks, CancelationToken token) | 等待任一个任务完成,或CancellationToken发出了取消的信号 |
void WaitAny(params Task[] tasks, TimsSpan span) | 等待任一个任务完成。如果在超时时限内没有完成的,则继续执行 |
void WaitAny(params Task[] tasks, int millisecondsTimeout, CancelationToken token) | 等待任一个任务完成,或CancellationToken发出了取消的信号。如果在超时时限内没有发生上述情况,则继续执行 |
在异步方法中异步地等待任务
await表达式等待Task
通过Task.WhenAll和Task.WhenAny来实现
实例
下面展示Task.WhenAll方法,它异步地等待所有与之相关地Task完成,不会占用主线程时间
class MyDownloadString
{
public void DoRun()
{
Task<int> t = CountCharacterAsync("https://www.lhuanblog.com", "https://www.microsoft.com");
Console.WriteLine("DoRun:Task 是否完成:{0}", t.IsCompleted ? "是" : "否");
Console.WriteLine("DoRun:Result:{0}", t.Result);
}
private async Task<int> CountCharacterAsync(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}", t1.IsCompleted ? "是" : "否");
Console.WriteLine(" CCA: T2 是否完成:{0}", t2.IsCompleted ? "是" : "否");
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 是否完成:否
CCA: T1 是否完成:是
CCA: T2 是否完成:是
DoRun:Result:62902
将WhenAll改为WhenAny
结果
DoRun:Task 是否完成:否
CCA: T1 是否完成:是
CCA: T2 是否完成:否
DoRun:Result:62902
Task.Delay方法
Task.Delay方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。
然而与Thread.Sleep阻塞线程不同的是,Task.Delay不会阻塞线程,线程可以继续处理其他工作。
实例
class Simple
{
Stopwatch sw = new Stopwatch();
public void DoRun()
{
Console.WriteLine("调用前");
ShowDelayAsync();
Console.WriteLine("调用后");
}
private async void ShowDelayAsync()
{
sw.Start();
Console.WriteLine(" 延迟前:{0}", sw.ElapsedMilliseconds);
await Task.Delay(1000);
Console.WriteLine(" 延迟后:{0}", sw.ElapsedMilliseconds);
}
}
class Program
{
static void Main(string[] args)
{
Simple ds = new Simple();
ds.DoRun();
Console.ReadKey();
}
}
结果
调用前
延迟前:0
调用后
延迟后:1024
Task.Delay方法重载
方法名 | 描述 |
---|---|
Task Delay(int millisecondDelay) | 在以毫秒表示的延迟时间到期后,返回完成的Task对象 |
Task Delay(TimeSpan span) | 在以 .NET TimeSpan对象表示的延迟时间到期后,返回完成的Task对象 |
Task Delay(int millisecondDelay, CancellationToken token) | 在以毫秒表示的延迟时间到期后,返回完成的Task对象。可通过取消令牌来取消该操作 |
Task Delay(TimeSpan span, CancellationToken token) | 在以 .NET TimeSpan对象表示的延迟时间到期后,返回完成的Task对象。可通过取消令牌来取消该操作 |
并行循环
Parallel.For循环和Parallel.ForEach循环
位于System.Threading.Tasks命名空间下
语法
//fromInclusive参数是迭代系列地第一个整数
//toExclusive参数是比迭代系列最后一个索引号大1的整数。与index < toExclusive一样
//body是接受单个输入参数的委托,body的代码在每次迭代中执行一次
public static ParallelLoopResult.For(int fromInclusive, int toExclusive, Action body);
实例1
如下代码是使用Parallel.For结构的例子。它从0到14迭代(记住实际的参数15超出了最大迭代索引)并且打印出迭代索引和索引的平方。该应用程序满足各个迭代之间是相互独立的条件。
Parallel.For(0, 15, i => Console.WriteLine("{0}的平方是 {1}", i, i * i));
结果
每次输出结果可能不一样
1的平方是 1
2的平方是 4
0的平方是 0
4的平方是 16
3的平方是 9
11的平方是 121
12的平方是 144
13的平方是 169
14的平方是 196
8的平方是 64
9的平方是 81
10的平方是 100
7的平方是 49
6的平方是 36
5的平方是 25
实例2
另外一个并行循环结构是Parallel.ForEach方法。
string[] squares = new string[]
{
"We","hold","these","truths","to","be","self-evident","that"
};
Parallel.ForEach(squares, i => Console.WriteLine(string.Format("{0}有{1}字符长度", i, i.Length)));
每次输出结果可能不一样
其它异步模式知识点