场景介绍
WPF程序中,需要从几个网站下载内容。
同步方式
private void NormalExecute()
{
Percentage = 0;
var watch = Stopwatch.StartNew();
RunDownload();
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}";
}
private List<string> PrepWebsites()
{
var output = new List<string>();
Message = string.Empty;
output.Add("https://www.baidu.com");
output.Add("https://www.yahoo.net/");
output.Add("https://www.csdn.net/");
output.Add("https://www.douyin.com/");
output.Add("https://weixin.qq.com/");
Percentage = 0;
Maximum = output.Count;
return output;
}
private void RunDownload()
{
var websites = PrepWebsites();
foreach (var site in websites)
{
WebsiteModel result = DownloadWebsite(site);
ReportWebsiteInfo(result);
Percentage++;
}
}
private WebsiteModel DownloadWebsite(string websiteURL)
{
WebsiteModel output = new WebsiteModel();
WebClient client = new WebClient();
output.WebsiteUrl = websiteURL;
output.WebsiteData = client.DownloadString(websiteURL);
return output;
}
private void ReportWebsiteInfo(WebsiteModel data)
{
Message += $"{data.WebsiteUrl} downloaded: {data.WebsiteData.Length} characters long.{Environment.NewLine}";
}
当点击“同步”按钮时,会发现WPF程序大概卡了3秒钟,最终将结果一次性呈现出来:
异步方式
private async void AsyncExecute()
{
var watch = Stopwatch.StartNew();
// RunDownloadAsync(); 如果没有await,就会不等Download结束,就直接执行watch.Stop()了
await RunDownloadAsync(_cancellationTokenSource.Token);
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}{Environment.NewLine}";
}
/// <summary>
/// 在async函数中,如果没有返回值,也不要写void,而是写Task
/// </summary>
/// <returns></returns>
private async Task RunDownloadAsync()
{
var websites = PrepWebsites();
foreach (var site in websites)
{
WebsiteModel result = await Task.Run(() => DownloadWebsite(site));
ReportWebsiteInfo(result);
Percentage++;
}
}
点击异步按钮后,WPF界面可以随意拖动,并且进度条、结果等都是一条一条输出到UI中的。
与第一种同步方式相比,
- 借助于Task.Run,实现了异步
- 因为开启了另外一个线程,也就是把耗时的操作放到了另外一个线程去处理,所以不会阻塞UI线程
- 总的实现时间并没有比同步方式减少
并行方式
private async void ParalleAsyncExecute()
{
Percentage = 0;
var watch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}{Environment.NewLine}";
}
private async Task RunDownloadParallelAsync()
{
var websites = PrepWebsites();
List<Task> tasks = new List<Task>();
foreach (var site in websites)
{
var task = DownloadWebsiteAsync(site).ContinueWith(t =>
{
ReportWebsiteInfo(t.Result);
Percentage++;
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
}
private async Task<WebsiteModel> DownloadWebsiteAsync(string websiteURL)
{
WebsiteModel output = new WebsiteModel();
WebClient client = new WebClient();
output.WebsiteUrl = websiteURL;
output.WebsiteData = await client.DownloadStringTaskAsync(websiteURL);
return output;
}
#endregion
第二种异步方式中,每创建一个Task,都会直接await
而在并行方式中,我们先只创建task,然后放到list中,最后await Task.WhenAll(tasks);
这样就可以让所有task以并行的方式同时运行,这样即不阻塞UI,又能更快的执行任务。
Parallel
其实在第一种方式中,如果只是不想阻塞线程,直接写成下面即可,可以实现和第二种异步方式完全相同的效果
private async void NormalExecute()
{
Percentage = 0;
var watch = Stopwatch.StartNew();
await Task.Run(()=> RunDownload());
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}";
}
如果用上Parallel,也可以非常简单的实现第三种并行效果
private async void NormalExecute()
{
Percentage = 0;
var watch = Stopwatch.StartNew();
var websites = PrepWebsites();
await Task.Run(() =>
Parallel.ForEach(websites, site =>
{
WebsiteModel result = DownloadWebsite(site);
ReportWebsiteInfo(result);
Percentage++;
}));
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}";
}
取消
public class MainWindowViewModel : BindableBase
{
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
public MainWindowViewModel()
{
NormalCommand = new DelegateCommand(NormalExecute);
AsyncCommand = new DelegateCommand(AsyncExecute);
ParallelAsyncCommand = new DelegateCommand(ParalleAsyncExecute);
CancelCommand = new DelegateCommand(() => _cancellationTokenSource.Cancel());
}
private async void AsyncExecute()
{
var watch = Stopwatch.StartNew();
try
{
await RunDownloadAsync(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
Message += "Async download was canceled.\n";
}
watch.Stop();
var elapsedMS = watch.ElapsedMilliseconds;
Message += $"Total execution time: {elapsedMS}{Environment.NewLine}";
}
private async Task RunDownloadAsync(CancellationToken cancellationToken)
{
var websites = PrepWebsites();
foreach (var site in websites)
{
WebsiteModel result = await Task.Run(() => DownloadWebsite(site));
cancellationToken.ThrowIfCancellationRequested();
ReportWebsiteInfo(result);
Percentage++;
}
}
}
异步中的取消操作,通过CancellationTokenSource 来设置,当_cancellationTokenSource.Cancel()时,
两种常用的页面异步加载方式
实际开发工作中,经常需要启动一个页面,为防止加载耗时数据时造成的UI阻塞或卡顿,可以采用以下两种方式实现异步加载。
1、利用控件的Loaded事件
View.xaml
<UserControl x:Class="Test.Views.UploadView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Loaded">
<b:InvokeCommandAction Command="{Binding LoadedCommand}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
......
......
</UserControl>
ViewModel.cs
public class UploadViewModel : BindableBase
{
public UploadViewModel ()
{
LoadedCommand = new DelegateCommand(LoadAsync);
}
private async void LoadAsync()
{
await Task.Run(async () =>
{
// 一些耗时操作,比如计算、从数据库获取数据等等
}
}
}
2、利用构造函数
我们知道,在构造函数中,是不允许使用awati,构造函数本身也不支持async。
public class AsyncViewModel
{
public string Text { get; set; } = "I'm a boy";
public AsyncViewModel()
{
LoadData().Await(CompltedHandle, HandleError);
}
private void CompltedHandle()
{
Text = "Success in task";
}
private void HandleError(Exception ex)
{
Text = ex.Message;
}
private async Task LoadData()
{
await Task.Delay(1000);
Text = "Change in task";
}
}
public static class TaskExtension
{
public async static void Await(this Task task, Action compltedCallback, Action<Exception> errorCallback)
{
try
{
await task;
compltedCallback?.Invoke();
}
catch (Exception ex)
{
errorCallback?.Invoke(ex);
}
}
public async static void Await<T>(this Task<T> task, Action<T> compltedCallback, Action<Exception> errorCallback)
{
try
{
T result = await task;
compltedCallback?.Invoke(result);
}
catch (Exception ex)
{
errorCallback?.Invoke(ex);
}
}
}
可以利用扩展方法的方式来变相实现await效果。
注:Prism框架中已有对应的实现,可以直接使用。