IO 绑定异步/Await 任务示例,带取消选项





5.00/5 (3投票s)
关于如何使用 IO 绑定异步/Await 进程的简短示例
引言
本文展示了一个小示例,说明如何在基于 WPF 的简单应用程序中使用 Async/Await 模式,以及用户如何通过 CancellationToken
来 取消 当前启动的 线程。
Using the Code
我们将首先展示演示应用程序的工作方式,然后我们将以 逐步的方式 详细介绍技术实现细节。
用户界面
应用程序非常简单。用户填写文本文件所在的完整路径,单击 Get 按钮,文本文件的内容将加载到主屏幕中(如果找到),否则将显示一个错误消息框。为了证明文本文件的处理(读取)是 async
的,用户可以在处理输入文件时单击 "+" 按钮。这将在主屏幕中显示一个整数值,该值将增加 1。在处理文件的同时,主屏幕中会显示一个百分比处理消息。最后,为了展示 CancellationTokens
的使用,当用户“点击” Cancel 按钮时,正在运行的线程(任务)将被中断,并且将向用户显示取消消息!
在进入代码之前...
备注 1:Regions 的使用...
依我之见,将相似的代码分组到 regions 中总是一个好主意,我通常使用以下 regions
- Private Storage:变量声明
- C'tor:构造函数代码
- Private Interface:所有
private
方法,细分为UIEventhandlers
和ApplicationLogic
- Public Interface:所有从外部可访问的代码(+ 最终的
Protected
和Internal
部分,如果需要的话...)。
备注 2:在 UI 代码隐藏中编写代码
为了简单起见...此示例中的所有事件处理和应用程序逻辑都已在 main-form 的 代码隐藏 中编写。虽然这对于演示目的来说是可以的,但在生产编码中应该省略。相反,所有应用程序和事件处理逻辑都应该从 UI 代码隐藏中提取出来,并放入所谓的 View-Model 类中,采用 MVVM(Model-View-ViewModel)模式。
解释代码隐藏
Private Storage
#region Private Storage
private int _counter;
CancellationTokenSource _cts;
#endregion Private Storage
当用户单击 + 按钮时,将使用 _counter
变量来更新计数器。这只是为了向读者表明,UI 界面 在后台线程运行时(任务在用户激活 Get
按钮后执行 read-async)仍然是 响应的。
获取文件内容事件处理方法
private async void buttonGetFile_Click(object sender, RoutedEventArgs e)
{
try
{
_cts = new CancellationTokenSource();
textBlockResult.Text = string.Empty;
labelPlus.Content = string.Empty;
labelProgress.Content = string.Empty;
buttonGetFile.IsEnabled = false;
textBlockResult.Text = await GetFileContentAsync(textBoxFileName.Text, _cts.Token);
}
catch (OperationCanceledException exCancel)
{
MessageBox.Show(exCancel.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
buttonGetFile.IsEnabled = true;
}
}
此方法将调用一个方法,该方法以 async
方式从文件读取数据。在调用 GetFileContentAsync(...)
(读取磁盘上文件的内容)时,将 _cts
的 CancellationTokenSource.Token
属性作为参数发送。如果请求取消,则 Token
属性将传播取消消息。添加一个 catch
块,如果用户选择取消文件读取操作,则显示一条消息。当此 IO 绑定的后台线程运行时,UI 仍然对用户输入做出响应(单击 + 按钮来证明这一点)。
在执行后台线程期间,UI 保持响应
private void buttonPlus_Click(object sender, RoutedEventArgs e)
{
labelPlus.Content = _counter++.ToString();
}
在后台线程执行期间(当用户提供了本地磁盘上物理文件的正确 URL 并通过点击 UI 中的 Get 按钮启动了 IO 绑定检索过程时),UI 保持响应,用户可以单击 + 按钮,该按钮将增加计数器。
async 方法的实现
private async Task<string> GetFileContentAsync(string fileName, CancellationToken ct)
{
// mimic long running process ...
for(int i = 0; i <= 100; i++)
{
if (!ct.IsCancellationRequested)
{
// mimic some long running process ...
await Task.Delay(50);
// update ui, we use dispatch to update the ui thread
this.Dispatcher.Invoke(() => SetProgress(i.ToString(), fileName));
}
}
using (StreamReader reader = new StreamReader(fileName))
{
if (!ct.IsCancellationRequested)
{
string fileContent = await reader.ReadToEndAsync();
return fileContent;
}
throw new OperationCanceledException
($"File-read-async of {fileName} has been canceled by user !");
}
}
private void SetProgress(string progress, string fileName)
{
labelProgress.Content = $"file {fileName} - {progress} % processed ...";
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
{
if (_cts != null)
_cts.Cancel();
}
代码非常简单,该方法提供了一个 fileName
和 CancellationToken
作为输入参数,将返回一个 Task of string 作为返回值。一旦 Task
完成,控制权将返回给 调用方法 (buttonGetFile_Click
),结果将显示在 UI 中。由于 async
执行上下文,UI 将保持响应,以便用户在后台进程运行时可以执行其他操作... 如果用户点击 Cancel 按钮,则正在进行的线程将被取消。备注:由于 async 读取过程不需要很长时间即可完成,因此我通过引入延迟来模拟一个长时间运行的进程。还要注意,我们会在每个时间间隔通知 UI 线程。我们需要执行 this.Dispatcher.Invoke(...)
方法,因为我们正在运行的后台进程在与 UI 线程不同的线程上运行。
关注点
请注意,上面提到的示例是一个 IO 绑定的 async 进程,这与 CPU 绑定的 async 进程相反,可能需要以不同的方式处理,更具体地说,我们应该(与 CPU 绑定相反)尝试在执行 IO 密集型任务时将线程返回给线程池。通过这种方式,线程可以被 windows-scheduler 重新使用以处理不同的操作。为了简单起见,我没有在这个简单的示例中包含这种行为,但请查看(除其他外)下一个 URL 关于这个概念:return-task-threads-back-to-the-thread-pool。