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. 源码

Progress-Control-Sample

[书籍]用UWP复习《C#并发编程经典实例》的更多相关文章

  1. 《C#并发编程经典实例》笔记

    1.前言 2.开宗明义 3.开发原则和要点 (1)并发编程概述 (2)异步编程基础 (3)并行开发的基础 (4)测试技巧 (5)集合 (6)函数式OOP (7)同步 1.前言 最近趁着项目的一段平稳期 ...

  2. 《C# 并发编程 · 经典实例》读书笔记

    前言 最近在看<C# 并发编程 · 经典实例>这本书,这不是一本理论书,反而这是一本主要讲述怎么样更好的使用好目前 C#.NET 为我们提供的这些 API 的一本书,书中绝大部分是一些实例 ...

  3. 《C#并发编程经典实例》学习笔记—2.3 报告任务

    问题 异步操作时,需要展示该操作的进度 解决方案 IProgress<T> Interface和Progress<T> Class 插一段话:读<C#并发编程经典实例&g ...

  4. 《C#并发编程经典实例》学习笔记—2.7 避免上下文延续

    避免上下文延续 在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行. 为了避免在上下文中恢复运行,可让 await 调用 ConfigureAwait 方法 ...

  5. 《C#并发编程经典实例》学习笔记—3.1 数据的并行处理

    问题 有一批数据,需要对每个元素进行相同的操作.该操作是计算密集型的,需要耗费一定的时间. 解决方案 常见的操作可以粗略分为 计算密集型操作 和 IO密集型操作.计算密集型操作主要是依赖于CPU计算, ...

  6. 《C#并发编程经典实例》学习笔记-第一章并发编程概述

    并发编程的术语 并发 同时做多件事情 多线程 并发的一种形式,它采用多个线程来执行程序. 多线程是并发的一种形式,但不是唯一的形式. 并行处理 把正在执行的大量的任务分割成小块,分配给多个同时运行的线 ...

  7. 《C#并发编程经典实例》学习笔记-关于并发编程的几个误解

    误解一:并发就是多线程 实际上多线程只是并发编程的一种形式,在C#中还有很多更实用.更方便的并发编程技术,包括异步编程.并行编程.TPL 数据流.响应式编程等. 误解二:只有大型服务器程序才需要考虑并 ...

  8. 并发编程概述--C#并发编程经典实例

    优秀软件的一个关键特征就是具有并发性.过去的几十年,我们可以进行并发编程,但是难度很大.以前,并发性软件的编写.调试和维护都很难,这导致很多开发人员为图省事放弃了并发编程.新版.NET 中的程序库和语 ...

  9. c#并发编程经典实例文摘

    第1章 并发编程概述 1.1 并发编程简介 并发: 多线程(包括并行处理) 异步编程(异步操作)程序启动一个操作,而该操作将会在一段时间后完成 响应时编程(异步事件)可以没有一个实际的开始,可以在任何 ...

随机推荐

  1. Mac上一条命令搭建web服务器

    实际测试工作中偶尔会需要搭建Web服务器环境,由于Mac OS X自带了Apache和PHP环境,只需要简单的启动就可以. 开启Apache 开启Web服务器的方法有两种(默认启动端口号是80): 打 ...

  2. 2018(2017)美图java服务端笔试(回忆录)

    选择题有几道,是比较基础的 填空题两道:一道是类似c语言的给出abc的值求 ++a+b+++c++  ,另一道是说出两个常见的垃圾回收算法 编程题 找出出现次数为1的数字然后改进(要求O(n)) 数据 ...

  3. MongoDB Sharding分片配置

    Ps:mongod是mongodb实例,mongos被默认为为mongodb sharding的路由实例. 本文使用的mongodb版本为3.2.9,因此参考网址为:https://docs.mong ...

  4. oracle外部表

    关于外部表的描述 正确描述 the create table as select statement can be used to upload data into a normal table in ...

  5. 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 ...

  6. C语言 一个数学问题:求s=(a^m)!+(b^n)!

    求s=(am)!+(bn)! //凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ #include<stdio.h> void main(){ i ...

  7. Linux /var/log下的各种日志文件详解

    1)/var/log/secure:记录登录系统存取数据的文件;例如:pop3,ssh,telnet,ftp等都会记录在此. 2)/var/log/wtmp:记录登录这的信息记录,被编码过,所以必须以 ...

  8. Spark1.0.0 源码编译和部署包生成

    问题导读:1.如何对Spark1.0.0源码编译?2.如何生成Spark1.0的部署包?3.如何获取包资源? Spark1.0.0的源码编译和部署包生成,其本质只有两种:Maven和SBT,只不过针对 ...

  9. 单片机与android手机通信(控制LED小灯亮灭)

    1.单片机实验板功能设计 为验证数据通信内容,让单片机板上的四个按键与android手机客户端上的四个LED灯相互控制:为达到上述基本实验要求,采用单字符传输数据即可,硬件需设计两块相同的单片机电路板 ...

  10. Python单元测试框架 unittest详解

    一 整体结构概览 unittest原名为PyUnit,是由java的JUnit衍生而来.对于单元测试,需要设置预先条件,对比预期结果和实际结果. TestCase :通过继承TestCase类,我们可 ...