[书籍]用UWP复习《C#并发编程经典实例》
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
private async void OnAddLinks(object sender, RoutedEventArgs e)
{
var dialog = new AddDownloadDialog();
await dialog.ShowAsync();
if (dialog.Downloads == null)
return;
…
…
}
基础的用法没什么好说的。
微软的文档提到“应将“‘Async’作为后缀添加到所编写的每个异步方法名称中。”,但即使没这样做VS和R#也没有提示。
2.2 同时开始一组任务并等待它们完成
private async Task<IEnumerable<Downloader>> AddNewDownloadAsync(IEnumerable<Uri> links, CancellationToken cancellationToken)
{
var downlodTasks = links.Select(Downloader.CreateAsync);
var downlodTasksArray = downlodTasks.ToArray();
var downloads = await Task.WhenAll(downlodTasksArray);
return downloads;
}
反正就是使用Task<TResult[]> WhenAll(params Task[] tasks)
。
2.3 一组任务中任一任务完成时的处理
Task<Downloader> Selector(Uri link) => Downloader.CreateAsync(link, cancellationToken);
var downlodTasks = links.Select(Selector);
var progressTasks = downlodTasks.Select(async t =>
{
var result = await t.ToObservable().Timeout(TimeSpan.FromSeconds(6));
await _mutex.WaitAsync(cancellationToken);
try
{
if (cancellationToken.IsCancellationRequested == false)
{
FinishedTasks++;
_downloads.Add(t.Result);
}
}
finally
{
_mutex.Release();
}
return result;
}).ToArray();
var downloads = await Task.WhenAll(progressTasks);
2.4 发出取消请求
由CancellationTokenSource发出取消请求,CancellationToken则让代码能够响应取消请求。
try
{
_cancellationTokenSource = new CancellationTokenSource();
await AddNewDownloadAsync(_cancellationTokenSource.Token);
}
catch (OperationCanceledException ex)
{
InAppNotification.Show("Task Paused:" + ex.Message, 5000);
}
catch (Exception ex)
{
ProgressControl.State = ProgressState.Faulted;
InAppNotification.Show("Task Error:" + ex.Message, 5000);
}
_cancellationTokenSource.Cancel();
上面代码演示了如何通过CancellationTokenSource发出取消请求,被取消的代码应该会抛出OperationCanceledException。也有可能被取消的代码还来不及响应取消就完成或报错了。
2.5 通过轮询响应取消请求
while (ReceivedBytes < TotalBytes)
{
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
var bytesReceived = random.Next(1024 * 1024);
ReceivedBytes += bytesReceived;
cancellationToken.ThrowIfCancellationRequested();
}
被取消的代码可以通过ThrowIfCancellationRequested()
抛出OperationCanceledException。也可以通过检查IsCancellationRequested再做其它处理,但抛出OperationCanceledException是标准处理方式。
如果再下一层代码里支持取消,则应该将CancellationToken传递给它,例如这里的Task.Delay。
2.6 超时后取消
var downlodTasks = links.Select(link =>
{
var cts = new CancellationTokenSource();
var token = cts.Token;
cts.CancelAfter(TimeSpan.FromSeconds(5));
return Downloader.CreateAsync(link, token);
});
var downlodTasksArray = downlodTasks.ToArray();
var downloads = await Task.WhenAll(downlodTasksArray);
CancellationTokenSource调用CancelAfter(TimeSpan delay)
或者使用构造函数CancellationTokenSource(TimeSpan delay)
设置取消前等待的时间间隔都可以实现超时后取消。
2.7 使用Rx实现超时
上面的方法实现超时其实相当于发出了一个取消请求,最终会抛出一个OperationCanceledException,有时会难以区分用户的取消操作和超时后被取消。我有时会用Rx来实现超时。
var result = await t.ToObservable().Timeout(TimeSpan.FromSeconds(6));
这段代码会抛出TimeoutException,更加有超时的感觉。但是CancellationTokenSource没有被取消,所以原本以为被取消的代码仍会继续偷偷摸摸地执行下去。
2.8 报告进度
public async Task StartDownloadAsync(IProgress<int> progress, CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
var random = new Random();
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
while (ReceivedBytes < TotalBytes)
{
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
var bytesReceived = random.Next(1024 * 1024);
ReceivedBytes += bytesReceived;
progress?.Report(bytesReceived);
cancellationToken.ThrowIfCancellationRequested();
}
}
}
var progress = new Progress<int>();
progress.ProgressChanged += (s, e) =>
{
DownloadedData?.Invoke(this, e);
OnPropertyChanged(nameof(Downloader));
};
_cancellationTokenSource = new CancellationTokenSource();
await Downloader.StartDownloadAsync(progress, _cancellationTokenSource.Token);
使用IProgress报告进度,使用Progress的event EventHandler ProgressChanged
接收进度。IProgress.Report(T value)
可以是异步的,所以T最好定义为一个不可变类型或者至少是值类型。
2.9 限制每次只开始5个下载
_semaphore = new SemaphoreSlim(5);
var tasks = dialog.Downloads.Select(async item =>
{
var model = new DownloaderModel { Downloader = item };
Downloads.Add(model);
model.DownloadedData += OnDownloadData;
await _semaphore.WaitAsync();
try
{
await model.StartDownloadAsync();
}
catch (OperationCanceledException)
{
//do nothing
}
finally
{
_semaphore.Release();
}
}).ToArray();
await Task.WhenAll(tasks);
虽然有几种方法实现,但SemaphoreSlim看着挺好理解的。
2.10 使用Rx的缓冲统计下载速度
private void OnDownloadData(object sender, int e)
{
_progress.Report(e);
}
当下载进度更新时使用IProgress
报告进度。
var progress = new Progress<int>();
_progress = progress;
var reports = Observable.FromEventPattern<int>(handler => progress.ProgressChanged += handler, handler => progress.ProgressChanged -= handler);
reports.Buffer(TimeSpan.FromSeconds(1)).Subscribe(async x =>
{
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
SpeedElement.Text = string.Format("{0} Bytes/S", x.Sum(s => s.EventArgs).ToString("N0"));
});
});
这段代码收集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元素通常如下:
await Task.Run(async () =>
{
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
Header.Text = "some message";
});
});
如果使用SynchronizationContext,则代码如下:
var synchronizationContext = SynchronizationContext.Current;
await Task.Run(() =>
{
synchronizationContext.Post(a =>
{
Header.Text = "some message";
}, null);
});
看起来SynchronizationContext确实更通用一些。
4. 延伸阅读
本书只介绍了使用技术,很少深入讲解内部机制,需要深入理解异步编程可以参考微软的官方文档:
异步编程
使用 Async 和 Await 的异步编程
异步概述
基于任务的异步模式 (TAP)
5. 源码
[书籍]用UWP复习《C#并发编程经典实例》的更多相关文章
- 《C#并发编程经典实例》笔记
1.前言 2.开宗明义 3.开发原则和要点 (1)并发编程概述 (2)异步编程基础 (3)并行开发的基础 (4)测试技巧 (5)集合 (6)函数式OOP (7)同步 1.前言 最近趁着项目的一段平稳期 ...
- 《C# 并发编程 · 经典实例》读书笔记
前言 最近在看<C# 并发编程 · 经典实例>这本书,这不是一本理论书,反而这是一本主要讲述怎么样更好的使用好目前 C#.NET 为我们提供的这些 API 的一本书,书中绝大部分是一些实例 ...
- 《C#并发编程经典实例》学习笔记—2.3 报告任务
问题 异步操作时,需要展示该操作的进度 解决方案 IProgress<T> Interface和Progress<T> Class 插一段话:读<C#并发编程经典实例&g ...
- 《C#并发编程经典实例》学习笔记—2.7 避免上下文延续
避免上下文延续 在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行. 为了避免在上下文中恢复运行,可让 await 调用 ConfigureAwait 方法 ...
- 《C#并发编程经典实例》学习笔记—3.1 数据的并行处理
问题 有一批数据,需要对每个元素进行相同的操作.该操作是计算密集型的,需要耗费一定的时间. 解决方案 常见的操作可以粗略分为 计算密集型操作 和 IO密集型操作.计算密集型操作主要是依赖于CPU计算, ...
- 《C#并发编程经典实例》学习笔记-第一章并发编程概述
并发编程的术语 并发 同时做多件事情 多线程 并发的一种形式,它采用多个线程来执行程序. 多线程是并发的一种形式,但不是唯一的形式. 并行处理 把正在执行的大量的任务分割成小块,分配给多个同时运行的线 ...
- 《C#并发编程经典实例》学习笔记-关于并发编程的几个误解
误解一:并发就是多线程 实际上多线程只是并发编程的一种形式,在C#中还有很多更实用.更方便的并发编程技术,包括异步编程.并行编程.TPL 数据流.响应式编程等. 误解二:只有大型服务器程序才需要考虑并 ...
- 并发编程概述--C#并发编程经典实例
优秀软件的一个关键特征就是具有并发性.过去的几十年,我们可以进行并发编程,但是难度很大.以前,并发性软件的编写.调试和维护都很难,这导致很多开发人员为图省事放弃了并发编程.新版.NET 中的程序库和语 ...
- c#并发编程经典实例文摘
第1章 并发编程概述 1.1 并发编程简介 并发: 多线程(包括并行处理) 异步编程(异步操作)程序启动一个操作,而该操作将会在一段时间后完成 响应时编程(异步事件)可以没有一个实际的开始,可以在任何 ...
随机推荐
- Mac上一条命令搭建web服务器
实际测试工作中偶尔会需要搭建Web服务器环境,由于Mac OS X自带了Apache和PHP环境,只需要简单的启动就可以. 开启Apache 开启Web服务器的方法有两种(默认启动端口号是80): 打 ...
- 2018(2017)美图java服务端笔试(回忆录)
选择题有几道,是比较基础的 填空题两道:一道是类似c语言的给出abc的值求 ++a+b+++c++ ,另一道是说出两个常见的垃圾回收算法 编程题 找出出现次数为1的数字然后改进(要求O(n)) 数据 ...
- MongoDB Sharding分片配置
Ps:mongod是mongodb实例,mongos被默认为为mongodb sharding的路由实例. 本文使用的mongodb版本为3.2.9,因此参考网址为:https://docs.mong ...
- oracle外部表
关于外部表的描述 正确描述 the create table as select statement can be used to upload data into a normal table in ...
- June.19 2018, Week 25th Tuesday
True love is visible not to the eyes but to the heart. 真爱不靠眼睛看,要用心感受. True love is visible not to th ...
- C语言 一个数学问题:求s=(a^m)!+(b^n)!
求s=(am)!+(bn)! //凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ #include<stdio.h> void main(){ i ...
- Linux /var/log下的各种日志文件详解
1)/var/log/secure:记录登录系统存取数据的文件;例如:pop3,ssh,telnet,ftp等都会记录在此. 2)/var/log/wtmp:记录登录这的信息记录,被编码过,所以必须以 ...
- Spark1.0.0 源码编译和部署包生成
问题导读:1.如何对Spark1.0.0源码编译?2.如何生成Spark1.0的部署包?3.如何获取包资源? Spark1.0.0的源码编译和部署包生成,其本质只有两种:Maven和SBT,只不过针对 ...
- 单片机与android手机通信(控制LED小灯亮灭)
1.单片机实验板功能设计 为验证数据通信内容,让单片机板上的四个按键与android手机客户端上的四个LED灯相互控制:为达到上述基本实验要求,采用单字符传输数据即可,硬件需设计两块相同的单片机电路板 ...
- Python单元测试框架 unittest详解
一 整体结构概览 unittest原名为PyUnit,是由java的JUnit衍生而来.对于单元测试,需要设置预先条件,对比预期结果和实际结果. TestCase :通过继承TestCase类,我们可 ...