异步方法
如果一个操作会返回Task,那么用这个操作续接后续操作,也会得到Task。
也就是说Task具有传染性,最终拼凑出来的Task非常复杂。
使用异步方法,可以简化Task的拼凑。
async修饰
异步方法需要添加async修饰符。并且通常方法名以Async结尾。
在异步方法内可以使用await关键字
(因为以前它不是关键字,可能有人用这个作为变量名。
为了不破坏以前的代码,只有在有async修饰是await才是关键字)。
返回值
返回值必须是void或Task或Task的泛型版本。
异步方法内直接返回值,编译器会自动把他封装成Task。
例如方法返回值是Task<int>,那你只要返回int就行了。
await
await可以放置在一个Task的前面。这回等待它完成,并获取他的返回值。
async Task<string[]> ModAsync(string path, string url)
{
Task<string[]> file = File.ReadAllLinesAsync(path);
Task<string[]> http = File.ReadAllLinesAsync(url);//假设这是个网络访问
var txt = await await Task.WhenAny(file, http);
var taskArr = new Task<string>[txt.Length];
for (int i = 0; i < taskArr.Length; i++)
{
taskArr[i] = File.ReadAllTextAsync(txt[i]);
}
return await Task.WhenAll(taskArr);
}
和直接调用Result或Wait不同的是,await会立刻视为方法完成然后继续运行这个方法后面的内容。
Console.WriteLine("主线程开始,即将调用异步方法");
var task = DelayAsync();
Console.WriteLine("异步方法调用完毕,等待异步方法输出值");
Console.WriteLine(task.Result);
async Task<int> DelayAsync()
{
Console.WriteLine("异步方法开始执行,等待1秒钟");
await Task.Delay(1000);
Console.WriteLine("异步方法执行结束");
return 12;
}
主线程开始,即将调用异步方法
异步方法开始执行,等待1秒钟
异步方法调用完毕,等待异步方法输出值
异步方法执行结束
12
可以看到,异步方法还没有结束的时候,主线程已经运作到了调用完毕处。
而await后面没有结束的内容,就会被打包成一个Task。在后台默默继续运行。
所以如果一个异步方法没有await的内容,就会弹出一个警告
状态机
迭代器和异步方法都会把方法内容做成一个很复杂的东西。
迭代器执行到yield return时就会停止,直到下一次被调用。
异步方法执行到await就会挂起线程,当等待结束时重新排队线程。
但他们把内容做复杂的时候,把变量的作用域还原的很好。
使用using语句的时候,一旦出了作用域,就会执行释放方法。
Task<string> Get4399()
{
using HttpClient client = new HttpClient();
return client.GetStringAsync("www.4399.com");
}
async Task<string> Get4399Async()
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync("www.4399.com");
}
第一个方法的网络访问会直接得到一个Task。把它直接返回是能和返回类型对应上的。
但实际上这样会报错。因为这个方法已经结束了,网络类就会关闭连接。
而还没有执行完成的任务,是需要保持连接才能访问到内容的。所以这个任务无法继续。
而第二个方法,会等到数据获取完成,这个方法才真正结束,网络类才释放。
即刻完成的任务
有时候,为了返回值能和接口对应上,或者重写基类的异步方法,
需要返回值是Task。而自己实际方法体内不需要等待。
又或者在异步方法内需要象征性的去await一个东西。
此时没有必要去创建一个需要通过线程池执行的任务,可以创建一个直接完成的任务。
已完成任务
CompletedTask静态属性是一个已经完成的任务。可以用来作为无返回值的任务来返回。
Task DoSomeAsync()
{
return Task.CompletedTask;
}
从值创建完成任务
FromResult静态方法可以创建一个任务,并把他的输出设置为指定的值。
Task<int> GetIntAsync()
{
return Task.FromResult(12);
}
屈服
Yield方法会创建一个可以await的东西,但不是Task。
它会让这个线程重新排队,等待一段非常小的时间。
async Task<int> DelayAsync()
{
Console.WriteLine("异步方法开始执行,等待1秒钟");
await Task.Yield();
Console.WriteLine("异步方法执行结束");
return 12;
}
如果使用Task.Delay(0)
是达不到这样的效果的。
主线程不是每次遇到await都会视为方法结束,而是在需要等待await完成的时候才会结束。
把0作为延迟的参数,创建出来的就是一个即刻完成的任务。
而使用Task.Delay(1)
也达不到这样的效果。
尽管你指定只延迟这么短暂的一点时间。但实际上操作系统无法管理得这么细致。
实际效果可能跟Task.Delay(15)
差不多。
切换上下文
在窗体应用中(WPF,winfrom,unity),通常主线程会进行死循环操作,
为你刷新界面,这称为UI线程。为了线程安全,所有改变UI的操作只允许在UI线程中进行。
通常,这类程序会在启动时配置线程环境(参阅SynchronizationContext),
让你的异步代码在await等待结束后,在合理时机,暂停UI线程的工作,让他接手await之后的操作。
因此,在窗体应用中使用异步方法,可以在方法内操控UI。
但如果不是使用await等待任务,而是调用了Wait或Result,会导致死锁
(你在控制台程序无法复现这个场景,因为控制台是系统调用的窗体,不是你程序的一部分)。
因为异步方法里的东西没有执行完成,他们需要继续执行才能完成。
而你使用Wait进行等待,主线程就会啥也不干。那他就干不了异步方法里的后续操作。
而后续操作没完成,主线程就得继续等。
Task的ConfigureAwait方法可以为当前的任务改变配置,让他可以通过别的线程来接手。
这样可以避免调用Wait造成的死锁。
async Task<int> Sum()
{
return await Task.Run(() =>
{
int sum = 0;
for (int i = 0; i < 100; i++)
{
sum += i;
}
return sum;
}).ConfigureAwait(false);
}
在第一个await的任务后面加上ConfigureAwait(false),会告诉线程池让别的线程来接手。
ConfigureAwait(true)是默认行为,不需要调用。调用了也只是尽量让同一线程接手,不能保证必然。