异步编程模式详解
在现代编程中,异步编程模式是提高程序性能和响应能力的关键技术。本文将深入探讨几种常见的异步编程模式,包括使用任务并行库(TPL)调用异步编程模型(APM)、异步委托调用、基于事件的异步模式(EAP)以及后台工作者模式。
使用 TPL 调用 APM
虽然 TPL 能显著简化对长时间运行方法的异步调用,但通常使用 API 提供的 APM 方法比针对同步版本编写 TPL 代码更好。这是因为 API 开发者最了解如何编写最高效的线程代码、同步哪些数据以及使用何种同步类型。幸运的是,TPL 的
TaskFactory
提供了专门用于调用 APM 方法的特殊方法。
TPL 的
FromAsync
方法有一组重载,用于调用 APM。以下是一个示例代码:
using System;
using System.IO;
using System.Net;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
public class Program
{
static private object ConsoleSyncObject =
new object();
public static void Main(string[] args)
{
string[] urls = args;
if (args.Length == 0)
{
urls = new string[]
{
"http://www.habitat-spokane.org",
"http://www.partnersintl.org",
"http://www.iassist.org",
"http://www.fh.org",
"http://www.worldvision.org"
};
}
int line = 0;
Task<WebResponse>[] tasksWithState =
urls.Select(
url=>DisplayPageSizeAsync(
url, line++)).ToArray();
while (
!Task.WaitAll(tasksWithState.ToArray(), 50))
{
DisplayProgress(tasksWithState);
}
Console.SetCursorPosition(0, line);
}
private static Task<WebResponse>
DisplayPageSizeAsync(string url, int line)
{
lock (ConsoleSyncObject)
{
Console.WriteLine(url);
}
WebRequest webRequest = WebRequest.Create(url);
WebRequestState state =
new WebRequestState(webRequest, line);
return task;
}
{
WebRequestState completedState =
(WebRequestState)asyncResult.AsyncState;
HttpWebResponse response =
(HttpWebResponse)completedState.WebRequest
.EndGetResponse(asyncResult);
Stream stream =
response.GetResponseStream();
using (StreamReader reader =
new StreamReader(stream))
Task<WebResponse> task =
Task<WebResponse>.Factory.FromAsync(
webRequest.BeginGetResponse,
GetResponseAsyncCompleted, state);
private static WebResponse GetResponseAsyncCompleted(
IAsyncResult asyncResult)
{
int length = reader.ReadToEnd().Length;
DisplayPageSize(completedState, length);
}
return response;
}
private static void DisplayProgress(
IEnumerable<Task<WebResponse>> tasksWithState)
{
foreach (
WebRequestState state in tasksWithState
.Where(task => !task.IsCompleted)
.Select(task=>
(WebRequestState)task.AsyncState))
{
DisplayProgress(state);
}
}
private static void DisplayPageSize(
WebRequestState completedState, int length)
{
lock (ConsoleSyncObject)
{
Console.SetCursorPosition(
completedState.ConsoleColumn,
completedState.ConsoleLine);
Console.Write(FormatBytes(length));
completedState.ConsoleColumn +=
length.ToString().Length;
}
}
private static void DisplayProgress(
WebRequestState state)
{
int left = state.ConsoleColumn;
int top = state.ConsoleLine;
lock (ConsoleSyncObject)
{
if (left >= Console.BufferWidth -
int.MaxValue.ToString().Length)
{
left = state.Url.Length;
Console.SetCursorPosition(left, top);
Console.Write("".PadRight(
Console.BufferWidth -
state.Url.Length));
state.ConsoleColumn = left;
}
else
{
state.ConsoleColumn++;
}
Console.SetCursorPosition(left, top);
Console.Write('.');
}
}
static public string FormatBytes(long bytes)
{
string[] magnitudes =
new string[] { "GB", "MB", "KB", "Bytes" };
long max =
(long)Math.Pow(1024, magnitudes.Length);
return string.Format("{1:##.##} {0}",
magnitudes.FirstOrDefault(
magnitude =>
bytes > (max /= 1024) )?? "0 Bytes",
(decimal)bytes / (decimal)max).Trim();
}
}
class WebRequestState
{
public WebRequestState(
WebRequest webRequest, int line)
{
WebRequest = webRequest;
ConsoleLine = line;
ConsoleColumn = Url.Length + 1;
}
public WebRequestState(WebRequest webRequest)
{
WebRequest = webRequest;
}
public WebRequest WebRequest { get; private set; }
public string Url
{
get
{
return WebRequest.RequestUri.ToString();
}
}
public int ConsoleLine { get; set; }
public int ConsoleColumn { get; set; }
}
将任务与 APM 方法对连接起来相对容易。上述代码中使用的重载方法接受三个参数:
- 第一个是
BeginX
方法委托(如
webRequest.BeginGetResponse
)。
- 第二个是与
EndX
方法匹配的委托。虽然可以直接使用
EndX
方法(如
webRequest.EndGetResponse
),但传递一个委托(如
GetResponseAsyncCompleted
)并使用延续传递风格(CPS)可以执行额外的完成活动。
- 最后一个参数是类似于
BeginX
方法接受的状态参数。
使用 TPL 调用 APM 方法对的一个优点是,我们不必担心发出
AsyncCallback
方法结束的信号。相反,我们监视任务是否完成。因此,
WebRequestState
不再需要包含
ManualResetEventSlim
。
使用 TPL 和 ContinueWith() 调用 APM
调用
TaskFactory.FromAsync()
的另一个选择是直接传递
EndX
方法,然后使用
ContinueWith()
处理任何后续代码。这种方法的优点是,你可以查询
continueWithTask
参数(如下面代码中的
continueWithTask
)以获取结果(
continueWithTask.Result
),而不是通过异步状态对象或使用闭包和匿名委托来存储访问
EndX
方法的方式。
// ...
{
lock (ConsoleSyncObject)
{
Console.WriteLine(url);
}
WebRequest webRequest = WebRequest.Create(url);
WebRequestState state = new WebRequestState(url, line);
return new Tuple<
Task<WebResponse>,WebRequestState>(
task, state);
}
// ...
Task<WebResponse> task =
Task<WebResponse>.Factory.FromAsync(
webRequest.BeginGetResponse,
webRequest.EndGetResponse, state)
.ContinueWith(continueWithTask =>
{
// Optional since state is available
// with closure
WebRequestState completedState =
(WebRequestState)continueWithTask.AsyncState;
Stream stream =
continueWithTask.Result.
GetResponseStream();
using (StreamReader reader =
new StreamReader(stream))
{
int length =
reader.ReadToEnd().Length;
DisplayPageSize(
completedState, length);
}
return continueWithTask.Result;
});
然而,
ContinueWith()
方法也有一个问题。
ContinueWith()
返回的任务的
AsyncState
属性包含
null
,而不是调用
FromAsync()
时指定的状态。要在
ContinueWith()
外部访问状态,需要将其保存到另一个位置。上述代码通过将其放入
Tuple<T1, T2>
并返回该元组来实现这一点。
同步控制台输出
在前面的代码中,我们多次更改控制台光标位置,然后向控制台写入文本。由于多个线程可能同时执行并向控制台写入文本,可能会同时更改光标位置,因此我们需要同步光标位置的更改和写入操作,以确保它们是原子操作。
代码中使用了一个
ConsoleSyncObject
类型的对象作为同步锁标识符。在移动光标或向控制台写入文本时,使用
lock
构造可以防止在移动和写入操作之间进行临时更新。即使是单行的
Console.WriteLine()
语句也被
lock
包围,因为我们不希望它们中断非原子的不同代码块。因此,只要有多个线程在执行,所有控制台更改都需要进行同步。
异步委托调用
有一种派生的 APM 模式称为异步委托调用,它利用了所有委托数据类型上由 C# 编译器生成的特殊代码。例如,对于
Func<string, int>
类型的委托实例,有一对 APM 方法可用:
System.IAsyncResult BeginInvoke(
string arg, AsyncCallback callback, object @object)
int EndInvoke(IAsyncResult result)
这意味着你可以通过使用 C# 编译器生成的方法同步调用任何委托(因此也可以调用任何方法)。
然而,异步委托调用模式使用的底层技术是一种用于分布式编程的不再进一步发展的技术,称为远程处理。尽管微软仍然支持异步委托调用的使用,并且在可预见的未来它将继续像现在一样工作,但与其他方法(如
Thread
、
ThreadPool
和 TPL)相比,其性能特征并不理想。因此,开发人员应倾向于选择这些替代方法,而不是使用异步委托调用 API 进行新的开发。
以下是一个异步委托调用的详细示例:
using System;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Application started....");
Console.WriteLine("Starting thread....");
// Display periods as progress bar.
100, false))
{
Console.Write('.');
}
Console.WriteLine();
Console.WriteLine("Thread ending....");
Func<int,string> workerMethod =
PiCalculator.Calculate;
IAsyncResult asyncResult =
workerMethod.BeginInvoke(500, null, null);
while(!asyncResult.AsyncWaitHandle.WaitOne(
Console.WriteLine(
workerMethod.EndInvoke(asyncResult));
Console.WriteLine(
"Application shutting down....");
}
}
Main()
方法首先分配一个
Func<string, int>
类型的委托,该委托指向
PiCalculator.Calculate(int digits)
方法。然后调用
BeginInvoke()
方法,该方法将在一个线程池线程上启动
PiCalculator.Calculate()
方法,然后立即返回。这允许其他代码与 π 计算并行运行。在这个示例中,我们在等待
PiCalculator.Calculate()
方法完成时打印句点。
我们使用
IAsyncResult.AsyncWaitHandle.WaitOne()
方法轮询委托的状态,这与 APM 中可用的机制相同。因此,在
PiCalculator.Calculate()
方法执行期间,代码每秒会在屏幕上打印句点。一旦等待句柄发出信号,代码就会调用
EndInvoke()
方法。与所有 APM 实现一样,将调用
BeginInvoke()
时返回的相同
IAsyncResult
引用传递给
EndInvoke()
方法非常重要。在这个示例中,
EndInvoke()
不会阻塞,因为我们在
while
循环中轮询线程的状态,并仅在线程完成后才调用
EndInvoke()
。
向另一个线程传递数据和从另一个线程接收数据
上述示例中传递了一个整数并接收了一个字符串,这是
Func<int, string>
的签名。异步委托调用的关键特性是,向目标调用传递数据和从目标调用接收数据非常简单,它与同步方法签名一致,就像在 APM 模式中一样。
考虑一个包含
out
和
ref
参数的委托类型,
BeginInvoke()
方法与委托签名匹配,但额外包含
AsyncCallback
和
object
参数。与
IAsyncResult
返回值一样,这些额外参数对应于标准 APM 参数,用于指定回调和传递状态对象。类似地,
EndInvoke()
方法与原始签名匹配,但只包含输出参数。由于
object[]
数据只是输入参数,因此它不会出现在
EndInvoke()
方法中。此外,由于
EndInvoke()
方法结束异步调用,其返回值也与原始委托的返回值匹配。
由于所有委托都包含由 C# 编译器生成的
BeginInvoke()
和
EndInvoke()
方法,用于异步委托调用模式,因此同步调用任何方法变得相对容易,特别是使用
Func
和
Action
委托时。此外,这使得调用者可以轻松地异步调用方法,无论 API 程序员是否明确实现了异步调用。
在 TPL 出现之前,异步委托调用模式比其他替代方法简单得多,因此在 API 没有提供显式异步调用模式时,它是一种常见的做法。然而,除了支持 .NET 3.5 及更早版本的框架外,TPL 的出现减少了使用异步委托调用方法的需求。
基于事件的异步模式(EAP)
与 APM 相比,基于事件的异步模式(EAP)更常用于高级编程。与 APM 一样,API 开发者为长时间运行的方法实现 EAP。
实现 EAP 模式的最简单形式是复制一个长时间运行的方法签名,并在方法名称后附加 “Async”,同时删除任何输出参数和返回值。“Async” 后缀向调用者表明该方法的此版本将异步执行,而不是阻塞直到方法的工作完成。由于调用结束时方法不一定完成,因此需要删除输出参数。
例如,考虑
PiCalculator.Calculate(int digits)
方法的 EAP 调用约定签名:
void PiCalculator.CalculateAsync(int digits)
与 APM 不同,EAP 模型不需要返回
IAsyncResult
对象。然而,API 实现者可以通过添加一个对象状态参数来支持传递任意状态:
void PiCalculator.CalculateAsync(int digits, object state)
甚至可能有一个泛型版本:
void PiCalculator.CalculateAsync<T>(int digits, T state)
在 .NET Framework 4 中,接受
CancellationToken
的版本也是一个不错的选择。
公开 “Async” 方法允许调用者开始执行,但仅靠它本身无法监视执行或使用 CPS。为此,需要添加一个完成事件和一个适当的
EventArgs
实现,以传回输出结果。以下是一个示例代码:
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
partial class PiCalculation
{
public void CalculateAsync(
int digits)
{
CalculateAsync(digits, null);
}
public void CalculateAsync(
int digits, object userState)
{
CalculateAsync(
digits, default(CancellationToken),
userState);
}
{
if (SynchronizationContext.Current == null)
{
SynchronizationContext.
SetSynchronizationContext(
new SynchronizationContext());
}
TaskScheduler scheduler =
TaskScheduler.
FromCurrentSynchronizationContext();
Task<string>.Factory.StartNew(
() =>
{
return PiCalculator.Calculate(digits);
}, cancelToken)
.ContinueWith<string>(
continueTask =>
{
CalculateCompleted(
typeof(PiCalculator),
new CalculateCompletedEventArgs(
continueTask.Result,
continueTask.Exception,
cancelToken.
IsCancellationRequested,
userState));
return continueTask.Result;
}, scheduler);
}
{
public CalculateCompletedEventArgs(
string value,
Exception error,
bool cancelled,
object userState) : base(
error, cancelled, userState)
{
Result = value;
}
public string Result { get; private set; }
}
public event
EventHandler<CalculateCompletedEventArgs>
CalculateCompleted = delegate { };
public class CalculateCompletedEventArgs
: AsyncCompletedEventArgs
}
在上述代码中,通过
CalculateCompleted
事件提供了支持。注册此事件将允许调用者在计算完成时收到通知。计算的值将在
CalculateCompletedEventArgs
类的
Result
属性上(该类派生自
AsyncCompletedEventArgs
)。这个类还允许调用者检查错误(通过
Error
属性)、取消(通过
Canceled
属性)和用户状态(通过
UserState
属性)。
在过去,EAP 中的取消支持是通过添加一个
CancelAsync
方法来实现的,该方法可选地接受一个
object
类型的
objectState
参数。然而,在 .NET Framework 4 中,使用
CancellationToken
是首选方法,因为它可以避免保存状态的需要。
在多线程操作中,通常不仅希望在线程完成时得到通知,还希望方法提供操作状态的更新。EAP 通过声明一个
ProgressChangedEventHandler
类型的事件(或在 C# 4.0 中支持变体的派生类型)并将该事件命名为
ProgressChanged
来支持这一点。然而,这会使 EAP 类需要保存状态。为了避免这种情况,开发人员也可以将进度监听器传递给
Async
方法。
关于上述代码,有几点需要注意:
-
PiCalculation
是一个实例类,而不是静态类。由于实现依赖于事件和初始
Async
成员调用之间的协调,使用实例类有助于避免在有多个调用和多个相同事件的监听器时出现的复杂性。例如,如果不使用实例方法,支持
CancelAsync(object state)
成员将是次优的(至少需要同步),因为需要查找与状态关联的调用。更糟糕的是,使用标准签名的进度更改通知将是不可能的。
-
PiCalculation
是线程安全的,因为它不存储任何状态信息。如果添加了对
CancelAsync()
或进度监视的支持,需要确保状态的保存不会破坏类的线程安全特性。
后台工作者模式
另一种提供操作状态和取消可能性的模式是后台工作者模式,它是 EAP 的一种具体实现。.NET Framework 2.0(或更高版本)包含一个
BackgroundWorker
类,用于编程这种类型的模式。
以下是一个使用后台工作者模式计算 π 的示例代码:
using System;
using System.Threading;
using System.ComponentModel;
using System.Text;
public class PiCalculator
{
public static AutoResetEvent resetEvent =
new AutoResetEvent(false);
public static void Main()
{
int digitCount;
Console.Write(
"Enter the number of digits to calculate:");
if (int.TryParse(Console.ReadLine(), out digitCount))
{
Console.WriteLine("ENTER to cancel");
// C# 2.0 Syntax for registering delegates
resetEvent.WaitOne();
}
else
{
Console.WriteLine(
"The value entered is an invalid integer.");
}
}
public static BackgroundWorker calculationWorker =
new BackgroundWorker();
calculationWorker.DoWork += CalculatePi;
// Register the ProgressChanged callback
calculationWorker.ProgressChanged +=
UpdateDisplayWithMoreDigits;
calculationWorker.WorkerReportsProgress =
true;
// Register a callback for when the
// calculation completes
calculationWorker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(
Complete);
calculationWorker.
WorkerSupportsCancellation = true;
// Begin calculating pi for up to
// digitCount digits
calculationWorker.RunWorkerAsync(
digitCount);
Console.ReadLine();
// If cancel is called after the calculation
// has completed it doesn't matter.
calculationWorker.CancelAsync();
// Wait for Complete() to run.
private static void CalculatePi(
object sender, DoWorkEventArgs eventArgs)
{
int digits = (int)eventArgs.Argument;
StringBuilder pi =
new StringBuilder("3.", digits + 2);
calculationWorker.ReportProgress(
0, pi.ToString());
// Calculate rest of pi, if required
if (digits > 0)
{
for (int i = 0; i < digits; i += 9)
{
// Calculate next i decimal places
int nextDigit =
PiDigitCalculator.StartingAt(
i + 1);
int digitCount =
Math.Min(digits - i, 9);
string ds =
string.Format("{0:D9}", nextDigit);
pi.Append(ds.Substring(0, digitCount));
// Show current progress
calculationWorker.ReportProgress(
0, ds.Substring(0, digitCount));
// Check for cancellation
if (
calculationWorker.CancellationPending)
{
// Need to set Cancel if you need to
// distinguish how a worker thread
// completed
// i.e., by checking
// RunWorkerCompletedEventArgs.Cancelled
eventArgs.Cancel = true;
break;
}
}
}
eventArgs.Result = pi.ToString();
}
private static void UpdateDisplayWithMoreDigits(
object sender,
ProgressChangedEventArgs eventArgs)
在这个示例中,
Main()
方法首先提示用户输入要计算的 π 的位数。如果输入有效,它会创建一个
BackgroundWorker
实例,并注册
DoWork
、
ProgressChanged
和
RunWorkerCompleted
事件的处理程序。然后,它调用
RunWorkerAsync()
方法开始计算。用户可以按回车键取消计算,程序会调用
CancelAsync()
方法。
CalculatePi()
方法是实际执行计算的地方。它使用
StringBuilder
构建 π 的值,并在每次计算出更多位数后调用
ReportProgress()
方法更新进度。如果用户取消了计算,它会设置
eventArgs.Cancel
为
true
并退出循环。
UpdateDisplayWithMoreDigits()
方法用于处理进度更新事件,它可以根据需要更新用户界面。
综上所述,不同的异步编程模式适用于不同的场景。开发人员应根据具体需求选择合适的模式,以提高程序的性能和响应能力。
异步编程模式对比总结
各模式特点对比
为了更好地理解不同异步编程模式的适用场景,我们可以通过以下表格进行对比:
| 模式名称 | 特点 | 适用场景 | 优缺点 |
| — | — | — | — |
| 使用 TPL 调用 APM | 借助 TPL 的
TaskFactory
调用 APM 方法,可利用 API 开发者的高效线程代码;可使用
FromAsync
重载方法;可通过
ContinueWith
处理后续代码 | 当 API 提供 APM 方法,且希望利用其高效实现时;需要对异步操作进行灵活控制和后续处理时 | 优点:利用 API 高效实现,灵活控制异步操作;缺点:
ContinueWith
返回任务的
AsyncState
可能为
null
,需额外处理状态 |
| 异步委托调用 | 利用 C# 编译器为委托生成的
BeginInvoke
和
EndInvoke
方法,可同步或异步调用委托方法 | 旧代码兼容,或在 API 未提供异步调用模式且不考虑性能时 | 优点:调用简单,与同步方法签名一致;缺点:底层技术性能不佳,不适合新开发 |
| 基于事件的异步模式(EAP) | 复制方法签名并添加 “Async” 后缀,通过事件通知完成和进度;可传递状态和支持取消操作 | 高级编程,需要事件驱动和状态传递,以及对长时间运行方法的监控时 | 优点:事件驱动,方便状态管理和取消操作;缺点:实现相对复杂,可能需要保存状态 |
| 后台工作者模式 | 是 EAP 的具体实现,使用
BackgroundWorker
类,提供操作状态和取消可能性 | 简单的后台任务,需要进度报告和取消功能时 | 优点:简单易用,提供进度和取消功能;缺点:功能相对有限,适用于简单场景 |
模式选择流程图
graph TD
A[开始选择模式] --> B{API 是否提供 APM 方法?}
B -- 是 --> C{是否需要灵活控制后续操作?}
C -- 是 --> D[使用 TPL 调用 APM 并结合 ContinueWith]
C -- 否 --> E[使用 TPL 调用 APM]
B -- 否 --> F{是否为旧代码兼容或不考虑性能?}
F -- 是 --> G[异步委托调用]
F -- 否 --> H{是否需要事件驱动和状态管理?}
H -- 是 --> I{是否为简单后台任务?}
I -- 是 --> J[后台工作者模式]
I -- 否 --> K[基于事件的异步模式(EAP)]
H -- 否 --> L[考虑其他模式]
异步编程模式的实际应用建议
性能优化建议
-
使用 TPL 调用 APM
:优先使用 API 提供的 APM 方法,因为 API 开发者通常对其内部实现有更深入的了解,能提供更高效的线程代码。同时,合理使用
ContinueWith方法处理后续操作,避免不必要的状态保存和传递。 - 异步委托调用 :尽量避免在新开发中使用,因为其底层技术性能不佳。如果是旧代码兼容,可以考虑使用,但要注意其性能瓶颈。
-
基于事件的异步模式(EAP)
:在实现 EAP 时,注意线程安全问题,避免不必要的状态保存。可以使用
CancellationToken来实现取消操作,避免保存额外的状态信息。 -
后台工作者模式
:对于简单的后台任务,使用
BackgroundWorker类可以快速实现进度报告和取消功能。但对于复杂的任务,可能需要考虑更灵活的模式。
代码维护建议
- 模块化设计 :将不同的异步操作封装成独立的模块,提高代码的可维护性和可测试性。例如,将异步委托调用的代码封装成一个独立的方法或类,方便复用和修改。
- 注释和文档 :在代码中添加详细的注释,说明每个异步操作的目的、参数和返回值。同时,编写相关的文档,记录不同异步模式的使用场景和注意事项。
-
错误处理
:在异步操作中,要注意错误处理。使用
try-catch块捕获异常,并通过合适的方式通知调用者。例如,在 EAP 中,可以通过事件参数的Error属性传递异常信息。
总结
异步编程模式在现代编程中起着至关重要的作用,不同的模式适用于不同的场景。通过深入了解使用 TPL 调用 APM、异步委托调用、基于事件的异步模式(EAP)和后台工作者模式的特点和适用场景,开发人员可以根据具体需求选择合适的模式,提高程序的性能和响应能力。同时,在实际应用中,要注意性能优化和代码维护,确保代码的质量和可扩展性。希望本文能帮助你更好地掌握异步编程模式,在实际项目中发挥更大的作用。
超级会员免费看
169万+

被折叠的 条评论
为什么被折叠?



