此外,异步操作对于需要并发处理多个客户端请求的服务器应用程序来说非常重要。
在实践过程中出现的异步操作的典型例子包括向服务器发送请求并等待响应、从硬盘读取数据以及运行拼写检查等开销较大的计算。
当用户单击一个按钮时,UI 线程将选取该消息并执行 Click 事件处理程序。
现在,假设在 Click 事件处理程序中,应用程序将请求发送到服务器并等待响应:
// !!!
Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) {
WebClient client = new WebClient();
client.DownloadFile("http://www.microsoft.com", "index.html");
}
冻结界面会导致用户体验不佳,这种情况几乎都是不可接受的。
要使应用程序 UI 能随时响应,直到服务器做出响应,则需保证下载不是 UI 线程上的同步操作,这一点很重要。
下面是一个使用线程池线程与服务器通信的示例:
// Suboptimal code
void Button_Click(object sender, RoutedEventArgs e) {
ThreadPool.QueueUserWorkItem(_ => {
WebClient client = new WebClient();
client.DownloadFile(
"http://www.microsoft.com", "index.html");
});
}
让我们进一步了解一下这些问题。
问题 1:浪费线程池线程
我刚才介绍的解决方法使用来自线程池的线程将请求发送到服务器并等待服务器响应。
由于 UI 不会冻结,因此阻止线程池线程比阻止 UI 线程要好得多,但它确实会浪费线程池的一个线程。
线程池将尝试通过创建更多线程来应对这种情况,但会造成相当大的性能开销。
本文中介绍的所有其他异步编程模式可解决浪费线程池线程的问题。
问题 2:返回结果
使用线程进行异步编程的另一个难题是:从在帮助器线程上执行的操作返回值将变得略为凌乱。
请看问题的另一个版本,您希望将收到的 HTML 指定到 TextBox(名为 HtmlTextBox)的 Text 属性中,而不是将下载的网页写入一个文件。
实现上述过程的一种想当然的错误方法如下:
// !!!
Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) {
ThreadPool.QueueUserWorkItem(_ => {
WebClient client = new WebClient();
string html = client.DownloadString(
"http://www.microsoft.com", "index.html");
HtmlTextBox.Text = html;
});
}
出于多种很充分的理由,WPF 和 Windows 窗体中都存在此限制。
要解决此问题,您可以在 UI 线程上捕获同步环境,然后在线程池线程上将消息发布到该环境:
void Button_Click(object sender, RoutedEventArgs e) {
SynchronizationContext ctx = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ => {
WebClient client = new WebClient();
string html = client.DownloadString(
"http://www.microsoft.com");
ctx.Post(state => {
HtmlTextBox.Text = (string)state;
}, html);
});
}
通常,从一个线程将值返回给另一个线程相当复杂,需要使用同步基元。
问题 3:组合异步操作
例如,要并行下载多个网页,编写同步代码将变得更加困难,而且更容易出错。
所有这一切都会导致相当大量的代码容易出错。
不用说,使用基于线程的模式甚至将更难正确实现更为复杂的复合模式。
基于事件的模式
事件模型公开一个方法,以便在操作完成时启动异步操作并引发一个事件。
图 1 显示了正确实现基于事件的异步编程模式所公开的方法示例。
图 1 基于事件的模式的方法
public class AsyncExample {
// Synchronous methods.
public int Method1(string param);
public void Method2(double param);
// Asynchronous methods.
public void Method1Async(string param);
public void Method1Async(string param, object userState);
public event Method1CompletedEventHandler Method1Completed;
public void Method2Async(double param);
public void Method2Async(double param, object userState);
public event Method2CompletedEventHandler Method2Completed;
public void CancelAsync(object userState);
public bool IsBusy { get; }
// Class implementation not shown.
...
}
以下代码显示如何以异步方式实现我们的示例:
void Button_Click(object sender, RoutedEventArgs e) {
WebClient client = new WebClient();
client.DownloadStringCompleted += eventArgs => {
HtmlTextBox.Text = eventArgs.Result;
};
client.DownloadStringAsync("http://www.microsoft.com");
}
下载在后台执行,一旦下载完成,DownloadStringCompleted 事件将在相应线程上执行。
实现基于事件的模式的类通常可确保 Completed 处理程序在相应线程上执行。
不过,基于事件的模式有几个限制:
- 该模式是非正式且仅仅依据惯例的,类可以偏离该模式。
- 将多个异步操作组合起来可能会相当困难,例如处理并行启动的异步操作或处理异步操作序列。
- 您无法轮询和检查异步操作是否已完成。
- 例如,如果使用一个实例处理多个异步操作,则必须对注册事件处理程序进行编码,以便仅处理一个目标异步操作,即使多次调用该处理程序也是如此。
- 即使没有必要在 UI 线程上执行,也将始终在启动异步操作时捕获的 SynchronizationContext 上调用事件处理程序,从而导致额外的性能开销。
- 难以良好实现,并且需要定义多个类型(例如,事件处理程序或事件参数)。
图 2 列出了 .NET Framework 4 类的几个示例,这些类实现基于事件的异步模式。
图 2 .NET 类中基于事件的异步模式示例
| 类 | 操作 |
| System.Activities.WorkflowInvoker | InvokeAsync |
| System.ComponentModel.BackgroundWorker | RunWorkerAsync |
| System.Net.Mail.SmtpClient | SendAsync |
| System.Net.NetworkInformation.Ping | SendAsync |
| System.Net.WebClient | DownloadStringAsync |
IAsyncResult 模式
与基于事件的模型相比,IAsyncResult 是更高级的异步编程解决方案。
或者,可以轮询操作是否已完成或者同步等待该操作,而不是提供回调。
该方法同步版本的签名如下所示:
public static IPAddress[] GetHostAddresses( string hostNameOrAddress) The asynchronous version of the method is exposed as follows: public static IAsyncResult BeginGetHostAddresses( string hostNameOrAddress, AsyncCallback requestCallback, Object state) public static IPAddress[] EndGetHostAddresses( IAsyncResult asyncResult)
以下示例使用 BeginGetHostAddresses 和 EndGetHostAddresses 方法异步查询 DNS 以获得地址 www.microsoft.com:
static void Main() {
Dns.BeginGetHostAddresses(
"www.microsoft.com",
result => {
IPAddress[] addresses = Dns.EndGetHostAddresses(result);
Console.WriteLine(addresses[0]);
},
null);
Console.ReadKey();
}
通过比较图 2 和图 3,您将注意到某些类实现基于事件的模式,某些类实现 IAsyncResult 模式,而某些类实现两种模式。
图 3 .NET 类中 IAsyncResult 的示例
| 类 | 操作 |
| System.Action | BeginInvoke |
| System.IO.Stream | BeginRead |
| System.Net.Dns | BeginGetHostAddresses |
| System.Net.HttpWebRequest | BeginGetResponse |
| System.Net.Sockets.Socket | BeginSend |
| System.Text.RegularExpressions.MatchEvaluator | BeginInvoke |
| System.Data.SqlClient.SqlCommand | BeginExecuteReader |
| System.Web.DefaultHttpHandler | BeginProcessRequest |
在 .NET Framework 2.0 中引入基于事件的模式简化了 IAsyncResult 未能解决的 UI 方面的问题,该模式侧重于以下方案:UI 应用程序启动单个异步应用程序,然后与其一起运行。
任务模式
一个 Task 可表示在 CPU 上执行的一项普通计算:
static void Main() {
Task<double> task = Task.Factory.StartNew(() => {
double result = 0;
for (int i = 0; i < 10000000; i++)
result += Math.Sqrt(i);
return result;
});
Console.WriteLine("The task is running asynchronously...");
task.Wait();
Console.WriteLine("The task computed: {0}", task.Result);
}
但是,Task 更加通用并且可表示任意异步操作,甚至是与服务器相对应(或者说通信)或从磁盘读取数据的那些操作。
一旦对 TaskCompletionSource 调用 SetResult 方法,相关联的 Task 便会结束,返回 Task 的结果值(请参见图 4)。
图 4 使用 TaskCompletionSource
static void Main() {
// Construct a TaskCompletionSource and get its
// associated Task
TaskCompletionSource<int> tcs =
new TaskCompletionSource<int>();
Task<int> task = tcs.Task;
// Asynchronously, call SetResult on TaskCompletionSource
ThreadPool.QueueUserWorkItem( _ => {
Thread.Sleep(1000); // Do something
tcs.SetResult(123);
});
Console.WriteLine(
"The operation is executing asynchronously...");
task.Wait();
// And get the result that was placed into the task by
// the TaskCompletionSource
Console.WriteLine("The task computed: {0}", task.Result);
}
不过,要注意的重要一点是,对 TaskCompletionSource 有访问权限的任何代码都可以调用 SetResult 方法,比如 Button.Click 事件的事件处理程序、完成某些计算的 Task 以及因服务器响应某个请求而引发的事件等。
因此,TaskCompletionSource 是实现异步操作的很常规的机制。
转换 IAsyncResult 模式
虽然 TaskCompletionSource 可以封装任何异步操作并将其作为 Task 公开,但 Task API 提供一种方便的机制将 IAsyncResult 模式转换为 Task,即 FromAsync 方法。
以下示例使用 FromAsync 方法将基于 IAsyncResult 的异步操作 Dns.BeginGetHost Addresses 转换为 Task:
static void Main() {
Task<IPAddress[]> task =
Task<IPAddress[]>.Factory.FromAsync(
Dns.BeginGetHostAddresses,
Dns.EndGetHostAddresses,
"http://www.microsoft.com", null);
...
}
下面是实现该方法的简单近似方式,在本例中直接以 GetHostAddresses 为目标:
static Task<IPAddress[]> GetHostAddressesAsTask(
string hostNameOrAddress) {
var tcs = new TaskCompletionSource<IPAddress[]>();
Dns.BeginGetHostAddresses(hostNameOrAddress, iar => {
try {
tcs.SetResult(Dns.EndGetHostAddresses(iar)); }
catch(Exception exc) { tcs.SetException(exc); }
}, null);
return tcs.Task;
}
转换基于事件的模式
Task 类不为这一转换提供内置机制,由于基于事件的异步模式仅仅是一种惯例,因此常规机制是不实用的。
代码示例显示获取 Uri 并返回表示异步操作 WebClient.DownloadStringAsync 的 Task 的方法:
static Task<string> DownloadStringAsTask(Uri address) {
TaskCompletionSource<string> tcs =
new TaskCompletionSource<string>();
WebClient client = new WebClient();
client.DownloadStringCompleted += (sender, args) => {
if (args.Error != null) tcs.SetException(args.Error);
else if (args.Cancelled) tcs.SetCanceled();
else tcs.SetResult(args.Result);
};
client.DownloadStringAsync(address);
return tcs.Task;
}
使用这一模式和上节中介绍的模式,您可以将任何现有的异步模式(基于事件或基于 IAsyncResult)转换为 Task。
处理和组合任务
与 IAsyncResult 和基于事件的方法不同,Task 提供保留关于异步操作、如何与之联接、如何检索其结果等的所有相关信息的单个对象。
可以在一个 Task 上等待,等待集合中的所有 Task 完成,或等待集合中的任意 Task 完成。
static void Main() {
Task<int> task1 = new Task<int>(() => ComputeSomething(0));
Task<int> task2 = new Task<int>(() => ComputeSomething(1));
Task<int> task3 = new Task<int>(() => ComputeSomething(2));
task1.Wait();
Console.WriteLine("Task 1 is definitely done.");
Task.WaitAny(task2, task3);
Console.WriteLine("Task 2 or task 3 is also done.");
Task.WaitAll(task1, task2, task3);
Console.WriteLine("All tasks are done.");
}
与等待类似,您可以计划延续任务在特定 Task 完成时运行、在集合中的所有 Task 完成时运行或者在集合中的任意 Task 完成时运行。
该任务完成后,将启动延续任务并将结果输出到控制台:
static void Main() {
Task<IPAddress[]> task =
Task<IPAddress[]>.Factory.FromAsync(
Dns.BeginGetHostAddresses,
Dns.EndGetHostAddresses,
"www.microsoft.com", null);
task.ContinueWith(t => Console.WriteLine(t.Result));
Console.ReadKey();
}
当异步操作表示为任务时,很容易等待多个操作完成。
图 5 并行运行多个操作
static void Main() {
string[] urls = new[] { "www.microsoft.com", "www.msdn.com" };
Task<IPAddress[]>[] tasks = new Task<IPAddress[]>[urls.Length];
for(int i=0; i<urls.Length; i++) {
tasks[i] = Task<IPAddress[]>.Factory.FromAsync(
Dns.BeginGetHostAddresses,
Dns.EndGetHostAddresses,
urls[i], null);
}
Task.WaitAll(tasks);
Console.WriteLine(
"microsoft.com resolves to {0} IP addresses.
msdn.com resolves to {1}",
tasks[0].Result.Length,
tasks[1].Result.Length);
}
让我们看另一个组合多项任务的示例,它采用以下三个步骤:
- 通过异步方式并行下载多个 HTML 页面
- 处理 HTML 页面
- 从 HTML 页面聚合信息
在如今多核计算机盛行的条件下,将开销大的计算工作分散到多个线程的程序将获得性能优势。
图 6 异步下载字符串
static void Main() {
Task<string> page1Task = DownloadStringAsTask(
new Uri("http://www.microsoft.com"));
Task<string> page2Task = DownloadStringAsTask(
new Uri("http://www.msdn.com"));
Task<int> count1Task =
page1Task.ContinueWith(t => CountParagraphs(t.Result));
Task<int> count2Task =
page2Task.ContinueWith(t => CountParagraphs(t.Result));
Task.Factory.ContinueWhenAll(
new[] { count1Task, count2Task },
tasks => {
Console.WriteLine(
"<P> tags on microsoft.com: {0}",
count1Task.Result);
Console.WriteLine(
"<P> tags on msdn.com: {0}",
count2Task.Result);
});
Console.ReadKey();
}
在同步环境中运行任务
例如,在含 UI 的应用程序中,能够计划将在 UI 线程上执行的延续任务通常非常有用。
要为 UI 线程创建 TaskScheduler,请在 UI 线程上运行时对 TaskScheduler 类型调用 FromCurrentSynchronizationContext 静态方法。
以下示例异步下载 www.microsoft.com 网页,然后将下载的 HTML 指定到 WPF 文本框的 Text 属性中:
void Button_Click(object sender, RoutedEventArgs e) {
TaskScheduler uiTaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()
DownloadStringAsTask(new Uri("http://www.microsoft.com"))
.ContinueWith(
t => { textBox1.Text = t.Result; },
uiTaskScheduler);
}
这样,UI 线程将不会被阻止,可继续更新用户界面并响应用户操作。
code.msdn.microsoft.com/ParExtSamples 下载获得。
blogs.msdn.com/pfxteam)撰稿。
衷心感谢以下技术专家对本文的审阅: 并发运行时团队
原文地址:https://msdn.microsoft.com/zh-cn/magazine/ff959203.aspx
示范代码:https://code.msdn.microsoft.com/Samples-for-Parallel-b4b76364/file/44488/10/Samples%20for%20Parallel%20Programming%20with%20the%20.NET%20Framework.zip