原文链接:https://www.cnblogs.com/wyt007/p/9486752.html
到现在为止,我们学习了任务并行库,这是微软提供的最新的异步编程基础设施。它允许我们以模块化的方式设计程序,来组合不同的异步操作。
遗憾的是,当阅读此类程序时仍然非常难理解程序的实际执行顺序。在大型程序中将会有许多相互依赖的任务和后续操作,用于运行其他后续操作的后续操作,处理异常的后续操作,并且它们都出现在程序代码中不同的地方。因此了解程序的先后执行次序变成了一个极具挑战性的问题。
另一个需要关注的问题是,能够接触用户界面控制器的每个异步任务是否得到了正确的同步上下文。程序只允许通过UI线程使用这些控制器,否则将会得到多线程访问异常。
说到异常,我们不得不使用单独的后续操作任务来处理在之前的异步操作中发生的错误。这又导致了分散在代码的不同部分的复杂的处理错误的代码,逻辑上无法相互关联。
为了解决这些问题, C#5.0的作者引入了新的语言特性,称为异步函数(asynchronous function),它是TPL之上的更高级别的抽象,真正简化了异步编程。正如在第4章提到的,抽象隐藏了主要的实现细节,使得程序员无须考虑许多重要的事情,从而使异步编程更容易。了解异步函数背后的概念是非常重要的,有助于我们编写健壮的高扩展性的应用程序。
要创建一个异步函数,首先需要用async关键字标注一个方法。如果不先做这个,就不可能拥有async属性或事件访问方法和构造函数。代码如下所示:
另一个重要的事实是,异步函数必须返回Task或Task
使用async关键字标注的方法内部,可以使用await操作符。该操作符可与TPL的任务一起工作,并获取该任务中异步操作的结果。在本章中稍后会讲述细节。在async方法外不能使用await关键字,否则会有编译错误。另外,异步函数在其代码中至少要拥有一个await操作符。然而,如果没有只会导致编译警告,而不是编译错误。
需要注意的是,在执行完await调用的代码行后该方法会立即返回。如果是同步执行,执行线程将会阻塞两秒然后返回结果。这里当执行完await操作后,立即将工作线程,放回线程池的过程中,我们会异步等待。2秒后,我们又一次从线程池中得到工作线程并继续运行其中剩余的异步方法。这允许我们在等待2秒时重用工作线程做些其他事,这对提高应用程序的可伸缩性非常重要。借助于异步函数我们拥有了线性的程序控制流,但它,的执行依然是异步的。这虽然好用,但是难以理解。本章将帮助你学习异步函数所有重要的方面。
以我的自身经验而言,如果程序中有两个连续的await操作符,此时程序如何工作有一个常见的误解。很多人认为如果在另一个异步操作之后使用await函数,它们将会并行运行。然而,事实上它们是顺序运行的,即第一个完成后第二个才会开始运行。记住这一点很重要,在本章中稍后会覆盖该细节。
在C# 5.0中关联async和await有一定的限制。例如,不能把控制台程序的Main方法标,记为async,不能在catch, finally, lock或unsafe代码块中使用await操作符。不允许对任何异步函数使用ref或out参数。还有其他微妙的地方,但是以上已经包括了主要的需要注意的,地方。
异步函数会被C#编译器在后台编译成复杂的程序结构。这里我不会说明该细节。生成的代码与另一个C#构造很类似,称为迭代器。生成的代码被实现为一种状态机。尽管很多程序员几乎开始为每个方法使用async修饰符,我还是想强调如果方法本来无需异步 ,或并行运行,那么将该方法标注为async是没有道理的。调用async方法会有显著的性能损失,通常的方法调用比使用async关键字的同样的方法调用要快上40~50倍。请注意这一点。
在本章中我们将学习如何使用C# 5.0中的async和await关键字实现异步操作。本章将讲述如何使用await按顺序或并行地执行异步操作,还将讨论如何在lambda表达式中使用await,如何处理异常,以及在使用async void方法时如何避免陷阱。在本章结束前,我们会深入探究同步上下文传播机制并学习如何创建自定义的awaitable对象,从而无需使用任务。
本节将讲述使用异步函数的基本场景。我们将比较使用TPL和使用await操作符获取异步操作结果的不同之处。
class Program
{
static void Main(string[] args)
{
Task t = AsynchronyWithTPL();
t.Wait();
t = AsynchronyWithAwait();
t.Wait();
Console.ReadKey();
}
static Task AsynchronyWithTPL()
{
Task t = GetInfoAsync("Task 1");
Task t2 = t.ContinueWith(task => Console.WriteLine(t.Result),
TaskContinuationOptions.NotOnFaulted);
Task t3 = t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted);
return Task.WhenAny(t2, t3);
}
async static Task AsynchronyWithAwait()
{
try
{
string result = await GetInfoAsync("Task 2");
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task GetInfoAsync(string name)
{
await Task.Delay(TimeSpan.FromSeconds(2));
//throw new Exception("Boom!");
return string.Format("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
当程序运行时运行了两个异步操作。其中一个是标准的TPL模式的代码,第二个使用了 C#的新特性async和awaito。AsynchronyWithTPL方法启动了一个任务,运行两秒后返回关于工作线程信息的字符串。然后我们定义了一个后续操作,用于在异步操作完成后打印出该 "操作结果,还有另一个后续操作,用于万一有错误发生时打印出异常的细节。最终,返回了一个代表其中一个后续操作任务的任务,并等待其在Main函数中完成。
在AsynchronyWithAwait方法中,我们对任务使用await并得到了相同的结果。这和编写通常的同步代码的风格一样,即我们获取任务的结果,打印出结果,如果任务完成时带有错误则捕获异常。关键不同的是这实际上是一个异步程序。使用await后, C#立即创建了一 1个任务,其有一个后续操作任务,包含了await操作符后面的所有剩余代码。这个新任务也处理了异常传播。然后,将该任务返回到主方法中并等待其完成。
请注意根据底层异步操作的性质和当前异步的上下文,执行异步代码的具体方式可能会不同。稍后在本章中会解释这一点。
因此可以看到程序的第一部分和第二部分在概念上是等同的,但是在第二部分中C# ,编译器隐式地处理了异步代码。事实上,第二部分比第一部分更复杂,接下来我们将讲述细节。
请记住在Windows GUI或ASPNET之类的环境中不推荐使用Task.Wait和Task.Result方法。如果程序员不是百分百地清楚代码在做什么,很可能会导致死锁。在第4章的4.10节中,在WPF应用程序中使用Task.Result时已经演示了该一点。
请取消对GetInfoAsync方法的throw new Exception代码行的注释来测试异常处理是否工作。
本节将展示如何在lambda表达式中使用await,我们将编写一个使用了await的匿名方法,并且获取异步执行该方法的结果。
class Program
{
static void Main(string[] args)
{
Task t = AsynchronousProcessing();
t.Wait();
Console.ReadKey();
}
async static Task AsynchronousProcessing()
{
Func> asyncLambda = async name => {
await Task.Delay(TimeSpan.FromSeconds(2));
return string.Format("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
};
string result = await asyncLambda("async lambda");
Console.WriteLine(result);
}
}
首先,由于不能在Main方法中使用async,我们将异步函数移到了Asynchronous Processing方法中。然后使用async关键字声明了一个lambda表达式。由于任何lambda表达式的类型都不能通过lambda自身来推断,所以不得不显式向C#编译器指定它的类型。在本例中,该类型说明该lambda表达式接受一个字符串参数,并返回一个Task
接着,我们定义了lambda表达式体。有个问题是该方法被定义为返回一个Task
最后一步是等待异步lambda表达式执行并打印出结果。
本节将展示当代码中有多个连续的await方法时程序的实际流程是怎样的。我们将学习如何阅读有await方法的代码,以及理解为什么await调用是异步操作。
class Program
{
static void Main(string[] args)
{
Task t = AsynchronyWithTPL();
t.Wait();
t = AsynchronyWithAwait();
t.Wait();
Console.ReadKey();
}
static Task AsynchronyWithTPL()
{
var containerTask = new Task(() => {
Task t = GetInfoAsync("TPL 1");
t.ContinueWith(task => {
Console.WriteLine(t.Result);
Task t2 = GetInfoAsync("TPL 2");
t2.ContinueWith(innerTask => Console.WriteLine(innerTask.Result),
TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);
t2.ContinueWith(innerTask => Console.WriteLine(innerTask.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
},
TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);
t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
});
containerTask.Start();
return containerTask;
}
async static Task AsynchronyWithAwait()
{
try
{
string result = await GetInfoAsync("Async 1");
Console.WriteLine(result);
result = await GetInfoAsync("Async 2");
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task GetInfoAsync(string name)
{
Console.WriteLine("Task {0} started!", name);
await Task.Delay(TimeSpan.FromSeconds(2));
if(name == "TPL 2")
throw new Exception("Boom!");
return string.Format("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
当程序运行时,与上节一样运行了两个异步操作。然而这次从AsynchronyWithAwait方法讲起。它看起来仍然像平常的同步代码,唯一不同之处是使用了两个await声明。最重要的一点是该代码依然是顺序执行的, Async2任务只有等之前的任务完成后才会开始执行。当阅读该代码时,程序流很清晰,可以看到什么先运行,什么后运行。但该程序如何是异步程序呢?首先,它不总是异步的。当使用await时如果一个任务已经完成,我们会异步地得到该任务结果。否则,当在代码中看到await声明时,通常的行为是方法执行到该await代码行时将立即返回,并且剩下的代码将会在一个后续操作任务中运行。因此等待操作结果时并没有阻塞程序执行,这是一个异步调用。当AsynchronyWithAwait方法中的代码在执行时,除了在Main方法中调用t.Wait外,我们可以执行任何其他任务。然而, 主线程必须等待直到所有异步操作完成,否则主线程完成后所有运行异步操作的后台线程会停止运行。
AsynchronyWithTPL方法模仿了AsynchronyWithAwait的程序流。我们需要一个容器任务来处理所有相互依赖的任务。然后启动主任务,给其加了一组后续操作。当该任务完成后,会打印出其结果。然后又启动了一个任务,在该任务完成后会依次运行更多的后续操"作。为了测试对异常的处理,当运行第二个任务时故意抛出一个异常,并打印出异常信息。这组后续操作创建了与第一个方法中一样的程序流。如果用它与await方法比较,可以看到它更容易阅读和理解。唯一的技巧是请记住异步并不总是意味着并行执行。
本节将学习如何使用await来并行地运行异步任务,而不是采用常用的顺序执行。
class Program
{
static void Main(string[] args)
{
Task t = AsynchronousProcessing();
t.Wait();
}
async static Task AsynchronousProcessing()
{
Task t1 = GetInfoAsync("Task 1", 3);
Task t2 = GetInfoAsync("Task 2", 5);
string[] results = await Task.WhenAll(t1, t2);
foreach (string result in results)
{
Console.WriteLine(result);
}
Console.ReadKey();
}
async static Task GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
//await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(seconds)));
return string.Format("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
这里定义了两个异步任务,分别运行3秒和5秒。然后使用Task.WhenAll辅助方法创!建了另一个任务,该任务只有在所有底层任务完成后才会运行。之后我们等待该组合任务的结果。5秒后,我们获取了所有结果,说明了这些任务是同时运行的。
然而这里观察到一个有意思的现象。当运行该程序时,你可能注意到这两个任务似平是被线程池中的同一个工作线程执行的。当我们并行运行任务时怎么可能发生这样的事情呢?为了让事情更有趣,我们来注释掉GetIntroAsync方法中的await Task.Delay代码行,并解除对await Task.Run代码行的注释,然后再次运行程序。
我们会看到该情况下两个任务会被不同的工作线程执行。不同之处是Task.Delay在幕后使用了一个计时器,过程如下:从线程池中获取工作线程,它将等待Task.Delay方法返回结果。然后, Task.Delay方法启动计时器并指定一块代码,该代码会在计时器时间到了Task.Delay方法中指定的秒数后被调用。之后立即将工作线程返回到线程池中。当计时器事件运行时,我们又从线程池中任意获取一个可用的工作线程(可能就是运行一个任务时使用的线,程)并运行计时器提供给它的代码。
当使用Task.Run方法时,从线程池中获取了一个工作线程并将其阻塞几秒,具体秒数由Thread.Sleep方法提供。然后获取了第二个工作线程并且也将其阻塞。在这种场景下,我们消费了两个工作线程,而它们绝对什么事没做,因为在它们等待时不能执行任何其他,操作。
我们将在第9章中讨论第一个场景的细节。在第9章我们将讨论用大量的异步操作进行数据输入和输出。尽可能地使用第一种方式是创建高伸缩性的服务器程序的关键。
本节将描述在C#中使用异步函数时如何处理异常。我们将学习对多个并行的异步操作,使用await时如何聚合异常。
class Program
{
static void Main(string[] args)
{
Task t = AsynchronousProcessing();
t.Wait();
}
async static Task AsynchronousProcessing()
{
Console.WriteLine("1. Single exception");
try
{
string result = await GetInfoAsync("Task 1", 2);
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine("Exception details: {0}", ex);
}
Console.WriteLine();
Console.WriteLine("2. Multiple exceptions");
Task t1 = GetInfoAsync("Task 1", 3);
Task t2 = GetInfoAsync("Task 2", 2);
try
{
string[] results = await Task.WhenAll(t1, t2);
Console.WriteLine(results.Length);
}
catch (Exception ex)
{
Console.WriteLine("Exception details: {0}", ex);
}
Console.WriteLine();
Console.WriteLine("2. Multiple exceptions with AggregateException");
t1 = GetInfoAsync("Task 1", 3);
t2 = GetInfoAsync("Task 2", 2);
Task t3 = Task.WhenAll(t1, t2);
try
{
string[] results = await t3;
Console.WriteLine(results.Length);
}
catch
{
var ae = t3.Exception.Flatten();
var exceptions = ae.InnerExceptions;
Console.WriteLine("Exceptions caught: {0}", exceptions.Count);
foreach (var e in exceptions)
{
Console.WriteLine("Exception details: {0}", e);
Console.WriteLine();
}
}
Console.ReadKey();
}
async static Task GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
throw new Exception(string.Format("Boom from {0}!", name));
}
}
我们运行了三个场景来展示在C#中使用async和await时关于错误处理的最常见情况。第一种情况是最简单的,并且与常见的同步代码几乎完全一样。我们只使用try/catch声明即 可获取异常细节。
一个很常见的错误是对一个以上的异步操作使用await时还使用以上方式。如果仍像第一种情况一样使用catch代码块,则只能从底层的AggregateException对象中得到第一个异常。
为了收集所有异常信息,可以使用await任务的Exception属性。在第三种情况中,我们使用AggregateException的Flatten方法将层级异常放入一个列表,并且从中提取出所有的底层异常。
本节描述了当使用await来获取异步操作结果时,同步上下文行为的细节。我们将学习如何以及何时关闭同步上下文流。
加入对Windows Presentation Foundation库的引用。
(1)右键点击项目中的引用文件夹,选择添加引用菜单选项。
(2)添加对PresentationCore, PresentationFramework, System.Xaml及Windows.Base库的引用。
class Program
{
[STAThread]
static void Main(string[] args)
{
var app = new Application();
var win = new Window();
var panel = new StackPanel();
var button = new Button();
_label = new Label();
_label.FontSize = 32;
_label.Height = 200;
button.Height = 100;
button.FontSize = 32;
button.Content = new TextBlock { Text = "Start asynchronous operations" };
button.Click += Click;
panel.Children.Add(_label);
panel.Children.Add(button);
win.Content = panel;
app.Run(win);
Console.ReadLine();
}
async static void Click(object sender, EventArgs e)
{
_label.Content = new TextBlock { Text = "Calculating..." };
TimeSpan resultWithContext = await Test();
TimeSpan resultNoContext = await TestNoContext();
//TimeSpan resultNoContext = await TestNoContext().ConfigureAwait(false);
var sb = new StringBuilder();
sb.AppendLine(string.Format("With the context: {0}", resultWithContext));
sb.AppendLine(string.Format("Without the context: {0}", resultNoContext));
sb.AppendLine(string.Format("Ratio: {0:0.00}",
resultWithContext.TotalMilliseconds / resultNoContext.TotalMilliseconds));
_label.Content = new TextBlock { Text = sb.ToString() };
}
async static Task Test()
{
const int iterationsNumber = 100000;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterationsNumber; i++)
{
var t = Task.Run(() => { });
await t;
}
sw.Stop();
return sw.Elapsed;
}
async static Task TestNoContext()
{
const int iterationsNumber = 100000;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterationsNumber; i++)
{
var t = Task.Run(() => { });
await t.ConfigureAwait(
continueOnCapturedContext: false);
}
sw.Stop();
return sw.Elapsed;
}
private static Label _label;
}
在本例中,我们将学习异步函数默认行为的最重要的方面之一。我们已经从第4章中了解了任务调度程序和同步上下文。默认情况下, await操作符会尝试捕获同步上下文,并在其中执行代码。我们已经知道这有助于我们编写与用户界面控制器协作的异步代码。另外,使用await不会发生在之前章节中描述过的死锁情况,因为当等待结果时并不会阻塞UI线程。
这是合理的,但是让我们看看潜在会发生什么事。在本例中,我们使用编程方式创建了一个Windows Presentation Foundation应用程序并订阅了它的按钮点击事件。当点击该按钮!时,运行了两个异步操作。其中一个使用了一个常规的await操作符,另一个使用了带false参数值的ConfigureAwait方法。false参数明确指出我们不能对其使用捕获的同步上下文来运行后续操作代码。在每个操作中,我们测量了执行完成花费的时间,然后将各自的时间和比例显示在主屏幕上。
结果看到常规的await操作符花费了更多的时间来完成。这是因为我们向UI线程中放,入了成百上千个后续操作任务,这会使用它的消息循环来异步地执行这些任务。在本例中,我们无需在UI线程中运行该代码,因为异步操作并未访问UI组件。使用带false参数值的, ConfigureAwait方法是一个更高效的方案。
还有一件事值得一提。尝试运行程序并只点击按钮然后等待结果,然后再这样做一次,但是这次点击按钮后尝试随机地拖拽应用程序窗口从一侧到另一侧。你将注意到在捕获的同步上下文中的代码执行速度变慢了!这个有趣的副作用完美演示了异步编程是多么危险。经历类似的情况是非常容易的,而且如果你之前从未经历过这样的情况,那么几乎不可能通过,调试来找出问题所在。
公平起见,让我们来看看相反的情况。在前面的代码片段中,在Click方法中,取消注释的代码行,并注释掉紧挨着它的前一行代码。当运行程序时,我们将得到多线程控制访问异常,因为设置Label控制器文本的代码不会放置到捕捉的上下文中,而是在线程池的工作线程中执行。
本节描述了为什么使用async void方法非常危险。我们将学习以及如何尽可能地替代该方法。在哪种情况下可使用该方法。
class Program
{
static void Main(string[] args)
{
Task t = AsyncTask();
t.Wait();
AsyncVoid();
Thread.Sleep(TimeSpan.FromSeconds(3));
t = AsyncTaskWithErrors();
while(!t.IsFaulted)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine(t.Exception);
//try
//{
// AsyncVoidWithErrors();
// Thread.Sleep(TimeSpan.FromSeconds(3));
//}
//catch (Exception ex)
//{
// Console.WriteLine(ex);
//}
int[] numbers = new[] {1, 2, 3, 4, 5};
Array.ForEach(numbers, async number => {
await Task.Delay(TimeSpan.FromSeconds(1));
if (number == 3) throw new Exception("Boom!");
Console.WriteLine(number);
});
Console.ReadLine();
}
async static Task AsyncTaskWithErrors()
{
string result = await GetInfoAsync("AsyncTaskException", 2);
Console.WriteLine(result);
}
async static void AsyncVoidWithErrors()
{
string result = await GetInfoAsync("AsyncVoidException", 2);
Console.WriteLine(result);
}
async static Task AsyncTask()
{
string result = await GetInfoAsync("AsyncTask", 2);
Console.WriteLine(result);
}
private static async void AsyncVoid()
{
string result = await GetInfoAsync("AsyncVoid", 2);
Console.WriteLine(result);
}
async static Task GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
if(name.Contains("Exception"))
throw new Exception(string.Format("Boom from {0}!", name));
return string.Format("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
当程序启动时,我们通过调用AsyncTask和AsyncVoid这两个方法启动了两个异步操作。第一个方法返回一个Task对象,而另一个由于被声明为async void所以没有返回值。由于它们都是异步的所以都会立即返回。但是第一个方法通过返回的任务状态或对其调用, Wait方法从而很容易实现监控。等待第二个方法完成的唯一方式是确切地等待多长时间,因为我们没有声明任何对象可以监控该异步操作的状态。当然可以使用某种共享的状态变量,将其设置到async void方法中,并从调用方法中检查其值,但返回一个Task对象的方式更好些。
最危险的部分是异常处理。使用async void方法,异常处理方法将被放置到当前的同步上下文中,在本例中即线程池中。线程池中未被处理的异常会终结整个进程。使用 AppDomain.UnhandledException事件可以拦截未被处理的异常,但不能从拦截的地方恢复进程。为了重现该场景,可以取消Main方法中对try/catch代码块的注释,然后运行程序。
关于使用async void lambda表达式的另一个事实是:它们与Action类型是兼容的,而 Action类型在标准.NET Framework类库中的使用非常广泛。在lambda表达式中很容易忘记对异常的处理,这将再次导致程序崩溃。可以取消在Main方法中第二个被注释的代码块的,注释来重现该场景。
强烈建议只在UI事件处理器中使用async void方法。在其他所有的情况下,请使用返回Task的方法。