经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
[书籍]用UWP复习《C#并发编程经典实例》
来源:cnblogs  作者:dino.c  时间:2018/9/25 20:41:00  对本文有异议

1. 简介

C#并发编程经典实例 是一本关于使用C#进行并发编程的入门参考书,使用“问题-解决方案-讨论”的模式讲解了以下这些概念:

  • 面向异步编程的async和await
  • 使用TPL(任务并行库)
  • 创建数据流管道的TPL Dataflow库
  • 基于LINQ的Reactive Extensions
  • 为并发代码编写单元测试
  • 并发方法之间的互操作
  • 不可变、线程安全和生产者/消费者集合
  • 并发代码中的取消功能支持
  • 支持异步的面向对象编程
  • 线程同步访问数据

我还挺喜欢这本书的,只有短短的170页却提供了大量的最佳实践,介绍了当时最新的C#平台并发开发技术,作为参考书时至今日依然很有推荐价值。不过篇幅所限,从入门知识到最佳实践之间往往缺乏过渡。例如第四章《数据流基础》,前一页还在介绍要安装哪个Nuget包才可以使用数据流,下一页突然讨论《链接数据流块》、《传递出错信息》,至于数据流有哪些类型各自的使用场景都没介绍到,于是我只好配合博客园上的这篇文章 TPL DataFlow初探 来学习数据流的知识。

2. 实现一个下载工具的UI

为什么这篇文章放在UWP板块下面?

这本书2015年在国内出版,读了这本书后感觉很有用。最近重读了这本书,试着用UWP复习一下书上的知识,除了有些Nuget包的名字变了其它内容都适用于UWP开发,最终成果是一个(十分阳春的)下载工具UI,所以就放在UWP板块下了。

2.1 基础的async/await

  1. private async void OnAddLinks(object sender, RoutedEventArgs e)
  2. {
  3. var dialog = new AddDownloadDialog();
  4. await dialog.ShowAsync();
  5. if (dialog.Downloads == null)
  6. return;
  7. }

基础的用法没什么好说的。
微软的文档提到“应将“‘Async’作为后缀添加到所编写的每个异步方法名称中。”,但即使没这样做VS和R#也没有提示。

2.2 同时开始一组任务并等待它们完成

  1. private async Task<IEnumerable<Downloader>> AddNewDownloadAsync(IEnumerable<Uri> links, CancellationToken cancellationToken)
  2. {
  3. var downlodTasks = links.Select(Downloader.CreateAsync);
  4. var downlodTasksArray = downlodTasks.ToArray();
  5. var downloads = await Task.WhenAll(downlodTasksArray);
  6. return downloads;
  7. }

反正就是使用Task<TResult[]> WhenAll(params Task[] tasks)

2.3 一组任务中任一任务完成时的处理

  1. Task<Downloader> Selector(Uri link) => Downloader.CreateAsync(link, cancellationToken);
  2. var downlodTasks = links.Select(Selector);
  3. var progressTasks = downlodTasks.Select(async t =>
  4. {
  5. var result = await t.ToObservable().Timeout(TimeSpan.FromSeconds(6));
  6. await _mutex.WaitAsync(cancellationToken);
  7. try
  8. {
  9. if (cancellationToken.IsCancellationRequested == false)
  10. {
  11. FinishedTasks++;
  12. _downloads.Add(t.Result);
  13. }
  14. }
  15. finally
  16. {
  17. _mutex.Release();
  18. }
  19. return result;
  20. }).ToArray();
  21. var downloads = await Task.WhenAll(progressTasks);

2.4 发出取消请求

由CancellationTokenSource发出取消请求,CancellationToken则让代码能够响应取消请求。

  1. try
  2. {
  3. _cancellationTokenSource = new CancellationTokenSource();
  4. await AddNewDownloadAsync(_cancellationTokenSource.Token);
  5. }
  6. catch (OperationCanceledException ex)
  7. {
  8. InAppNotification.Show("Task Paused:" + ex.Message, 5000);
  9. }
  10. catch (Exception ex)
  11. {
  12. ProgressControl.State = ProgressState.Faulted;
  13. InAppNotification.Show("Task Error:" + ex.Message, 5000);
  14. }
  1. _cancellationTokenSource.Cancel();

上面代码演示了如何通过CancellationTokenSource发出取消请求,被取消的代码应该会抛出OperationCanceledException。也有可能被取消的代码还来不及响应取消就完成或报错了。

2.5 通过轮询响应取消请求

  1. while (ReceivedBytes < TotalBytes)
  2. {
  3. await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
  4. var bytesReceived = random.Next(1024 * 1024);
  5. ReceivedBytes += bytesReceived;
  6. cancellationToken.ThrowIfCancellationRequested();
  7. }

被取消的代码可以通过ThrowIfCancellationRequested()抛出OperationCanceledException。也可以通过检查IsCancellationRequested再做其它处理,但抛出OperationCanceledException是标准处理方式。

如果再下一层代码里支持取消,则应该将CancellationToken传递给它,例如这里的Task.Delay。

2.6 超时后取消

  1. var downlodTasks = links.Select(link =>
  2. {
  3. var cts = new CancellationTokenSource();
  4. var token = cts.Token;
  5. cts.CancelAfter(TimeSpan.FromSeconds(5));
  6. return Downloader.CreateAsync(link, token);
  7. });
  8. var downlodTasksArray = downlodTasks.ToArray();
  9. var downloads = await Task.WhenAll(downlodTasksArray);

CancellationTokenSource调用CancelAfter(TimeSpan delay)或者使用构造函数CancellationTokenSource(TimeSpan delay)设置取消前等待的时间间隔都可以实现超时后取消。

2.7 使用Rx实现超时

上面的方法实现超时其实相当于发出了一个取消请求,最终会抛出一个OperationCanceledException,有时会难以区分用户的取消操作和超时后被取消。我有时会用Rx来实现超时。

  1. var result = await t.ToObservable().Timeout(TimeSpan.FromSeconds(6));

这段代码会抛出TimeoutException,更加有超时的感觉。但是CancellationTokenSource没有被取消,所以原本以为被取消的代码仍会继续偷偷摸摸地执行下去。

2.8 报告进度

  1. public async Task StartDownloadAsync(IProgress<int> progress, CancellationToken cancellationToken)
  2. {
  3. _cancellationToken = cancellationToken;
  4. var random = new Random();
  5. using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
  6. {
  7. while (ReceivedBytes < TotalBytes)
  8. {
  9. await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
  10. var bytesReceived = random.Next(1024 * 1024);
  11. ReceivedBytes += bytesReceived;
  12. progress?.Report(bytesReceived);
  13. cancellationToken.ThrowIfCancellationRequested();
  14. }
  15. }
  16. }
  1. var progress = new Progress<int>();
  2. progress.ProgressChanged += (s, e) =>
  3. {
  4. DownloadedData?.Invoke(this, e);
  5. OnPropertyChanged(nameof(Downloader));
  6. };
  7. _cancellationTokenSource = new CancellationTokenSource();
  8. await Downloader.StartDownloadAsync(progress, _cancellationTokenSource.Token);

使用IProgress报告进度,使用Progressevent EventHandler ProgressChanged接收进度。IProgress.Report(T value)可以是异步的,所以T最好定义为一个不可变类型或者至少是值类型。

2.9 限制每次只开始5个下载

  1. _semaphore = new SemaphoreSlim(5);
  2. var tasks = dialog.Downloads.Select(async item =>
  3. {
  4. var model = new DownloaderModel { Downloader = item };
  5. Downloads.Add(model);
  6. model.DownloadedData += OnDownloadData;
  7. await _semaphore.WaitAsync();
  8. try
  9. {
  10. await model.StartDownloadAsync();
  11. }
  12. catch (OperationCanceledException)
  13. {
  14. //do nothing
  15. }
  16. finally
  17. {
  18. _semaphore.Release();
  19. }
  20. }).ToArray();
  21. await Task.WhenAll(tasks);

虽然有几种方法实现,但SemaphoreSlim看着挺好理解的。

2.10 使用Rx的缓冲统计下载速度

  1. private void OnDownloadData(object sender, int e)
  2. {
  3. _progress.Report(e);
  4. }

当下载进度更新时使用IProgress报告进度。

  1. var progress = new Progress<int>();
  2. _progress = progress;
  3. var reports = Observable.FromEventPattern<int>(handler => progress.ProgressChanged += handler, handler => progress.ProgressChanged -= handler);
  4. reports.Buffer(TimeSpan.FromSeconds(1)).Subscribe(async x =>
  5. {
  6. await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  7. {
  8. SpeedElement.Text = string.Format("{0} Bytes/S", x.Sum(s => s.EventArgs).ToString("N0"));
  9. });
  10. });

这段代码收集ProgressChanged事件,并每一秒钟把收集到的事件作为一个集合发布。

3. 书中的其它建议

一旦你输入new Thread(),那就糟糕了,说明项目中的代码太过时了。

比起老式的多线程机制,采用高级的抽象机制会让程序功能更加强大、效率更高。事实上UWP好像只能使用线程池,不能直接访问及控制线程(因为习惯用Task没关心线程,也许有我不知道的方式),看起来微软希望开发者使用Task这个更合理的抽象而不是直接使用线程。

在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。

这是个常见的错误,幸好很多情况下R#都会提示这个错误。

基本的lock语句就可以很好地处理99%的情况了。

经常在Code Review时看到Monitor或ReaderWriterLockSlim之类的。但是,我明白的,比起直接用lock这样写比较帅气(但我还是会要求改过来)。

应该把lock语句使用的对象设为私有变量,并且永远不要暴露给非本类的方法。

lock一个属性,或者直接lock(this)都十分危险。我真的CodeReview过因为习惯性地lock(this)而产生死锁的代码。

另外锁对象的使用范围尽量小,不要在多个语句中使用同一个锁对象。

在UI线程上执行代码时,永远不要使用针对特定平台的类型。WPF、Silverlight、iOS、Android都有Dispatcher类,Windows应用商店平台使用CoreDispatcher、Windows Forms有ISynchronizeInvoke接口。不要在新写的代码中使用这些类型,就当它们不存在吧。使用这些类型会使代码无所谓绑定到某个特定平台上。SynchronizationContext是通用的,基于上述类型的抽象类。

在UWP中,在线程中调用UI元素通常如下:

  1. await Task.Run(async () =>
  2. {
  3. await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  4. {
  5. Header.Text = "some message";
  6. });
  7. });

如果使用SynchronizationContext,则代码如下:

  1. var synchronizationContext = SynchronizationContext.Current;
  2. await Task.Run(() =>
  3. {
  4. synchronizationContext.Post(a =>
  5. {
  6. Header.Text = "some message";
  7. }, null);
  8. });

看起来SynchronizationContext确实更通用一些。

4. 延伸阅读

本书只介绍了使用技术,很少深入讲解内部机制,需要深入理解异步编程可以参考微软的官方文档:
异步编程
使用 Async 和 Await 的异步编程
异步概述
基于任务的异步模式 (TAP)

5. 源码

Progress-Control-Sample

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号