c# 异步编程async、await和Task

场景介绍

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框架中已有对应的实现,可以直接使用。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值