《C# 并发编程经典实例》
简介
《C# 并发编程经典实例》适合具有.NET基础,希望学习最新(2014年)并发编程技术的开发人员阅读。本书侧重实操应用,对底层原理细节描述不是很多。
本书作者Stephen喜欢演讲和写作,在其个人网站http://stephencleary.com/上,有大量受欢迎的博客文章以及开源库和应用。
我于 2025 年 4 月开始学习此书,读书摘要记录如下。
《C#并发编程经典实例》 《Concurrency in C# Cookbook》 | ||
---|---|---|
检索 | 豆瓣读书 | |
封面 | ![]() | |
附件 |
译者序
本书译者为相银初老师,他在本书《译者序》章节有着这样一段话,想必大多数踏入职场工作过几年的朋友们,都会深以为然。
翻译中的一点感受
过去的十多年我一直在从事软件开发和设计工作。相信国内很多开发人员都和我一样,心中存在着一个疑惑:我国的软件人员很多(绝对数量不会比美国少),但为什么软件技术总体上落后欧美国家那么多?确定翻译《C#并发编程经典实例》这本书后,我一边仔细阅读原书,一边遵循作者的思路,逐渐发现作者思考问题的一个理念。这就是按软件的不同层次进行明确分工,我只负责我所实现的这个层次,底层技术是为上层服务的,我只负责选择和调用,不管内部的实现过程;同样,我负责的层次为更高一层的软件提供服务,供上层调用,也不需要上层关心我的内部实现。 由此想到,这正好反映出国内开发人员中的一个通病,即分工不够细、技术关注不够精。很多公司和团队在开发时都喜欢大包大揽,从底层到应用层全部自己实现;很多开发人员也热衷于“大而全”地学习技术,试图掌握软件开发中的各种技术,而不是精通某一方面。甚至流行这样一种观点,实现底层软件、写驱动的才是高级开发人员,做上层应用的人仅仅是“码农”。本书作者明确地反对了这种看法,书中强调如何利用好现成的库,而不是全部采用底层技术自己实现。利用现成的库开发出高质量的软件,对技术能力的考验并不低于开发底层库。
2014 年 10 月于深圳
Ch1
优秀软件的一个关键特征就是具有并发性。
1.1 并发编程简介
术语介绍
- 多线程:并发的一种形式,它采用多个线程来执行程序。
多线程是并发的一种形 式,但不是唯一的形式。实际上,直接使用底层线程类型在现代程序中基本不起作用。比起老式的多线程机制,采用高级的抽象机制会让程序功能更加强大、效率更高。但是,不要认为多线程已经彻底被淘汰了!因为线程池要求多线程继续存在。线程池存放任务的队列,这个队列能够根据需要自行调整。相应地,线程池产生了另一个重要的并发形式:并行处理。
- 并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。
并行处理是多线程的一种,而多线程是并发的一种。在现代程序中,还有一种非常重要但 很多人还不熟悉的并发类型:异步编程。
- 异步编程:并发的一种形式,它采用 future 模式或回调 (callback) 机制,以避免产生不必要的线程。
一个 future(或 promise) 类型代表一些即将完成的操作。在 .NET 中,新版 future 类型 有 Task 和 Task
异步编程是一种功能强大的并发形式,但直至不久前,实现异步编程仍需要特别复杂的代 码。VS2012 支持 async 和 await,这让异步编程变得几乎和同步 (非并发) 编程一样容易。
并发编程的另一种形式是响应式编程 (reactive programming)。异步编程意味着程序启动一 个操作,而该操作将会在一段时间后完成。响应式编程与异步编程非常类似,不过它是基于异步事件 (asynchronous event) 的,而不是异步操作 (asynchronous operation)。异步事件可以没有一个实际的“开始”,可以在任何时间发生,并且可以发生多次,例如用户输入。
- 响应式编程:一种声明式的编程模式,程序在该模式中对事件做出响应。
如果把一个程序看作一个大型的状态机,则该程序的行为便可视为它对一系列事件做出响 应,即每换一个事件,它就更新一次自己的状态。这听起来很抽象和空洞,但实际上并非如此。利用现代的程序框架,响应式编程已经在实际开发中广泛使用。响应式编程不一定 是并发的,但它与并发编程联系紧密,因此本书介绍了响应式编程的基础知识。
1.2 异步编程简介
异步编程有两大好处。
第一个好处是对于面向终端用户的 GUI 程序:异步编程提高了响应能力。
我们都遇到过在运行时会临时锁定界面的程序,异步编程可以使程序在执行任务时仍能响应用户的输入。
第二个好处是对于服务器端应用:异步编程实现了可扩展性。
服务器应用可以利用线程池满足其可扩展性,使用异步编程后,可扩展性通常可以提高一个数量级。
public async Task DoSomethingAsync()
{
int val = 13;
//异步方式等待 1 秒
await Task.Delay(TimeSpan.FromSeconds(1));
val *= 2;
//异步方式等待 1 秒
await Task.Delay(TimeSpan.FromSeconds(1));
Trace.WriteLine(val);
//Console.WriteLine(val);
}
一个 async 方法是由多个同步执行的程序块组成的,每个同步程序块之间由 await 语句分 隔。第一个同步程序块在调用这个方法的线程中运行,但其他同步程序块在哪里运行呢?情况比较复杂。
最常见的情况是,用 await 语句等待一个任务完成,当该方法在 await 处暂停时,就可以 捕捉上下文 (context)
。如果当前 SynchronizationContext
不为空,这个上下文就是当前 SynchronizationContext
。如果当前 SynchronizationContext
为空,则这个上下文为当前 TaskScheduler
。该方法会在这个上下文中继续运行。一般来说,运行 UI 线程时采用 UI 上 下文,处理 ASP.NET 请求时采用 ASP.NET 请求上下文,其他很多情况下则采用线程池上 下文。
因此,在上面的代码中,每个同步程序块会试图在原始的上下文中恢复运行。如果在 UI 线程中调用 DoSomethingAsync
,这个方法的每个同步程序块都将在此 UI 线程上运行。但 是,如果在线程池线程中调用,每个同步程序块将在线程池线程上运行。
要 避 免 这 种 错 误 行 为,可 以 在 await 中 使 用 ConfigureAwait
方 法,将 参 数 continueOnCapturedContext
设为 false。接下来的代码刚开始会在调用的线程里运行,在被 await 暂 停后,则会在线程池线程里继续运行。
async Task DoSomethingAsync()
{
int val = 13;
// 异步方式等待 1 秒
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
val *= 2;
// 异步方式等待 1 秒
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
Trace.WriteLine(val.ToString());
}
提示
最好的做法是,在核心库代码中一直使用 ConfigureAwait。在外围的用户界面代码中,只在需要时才恢复上下文。
关 键 字 await 不 仅 能 用 于 任 务,还 能 用 于 所 有 遵 循 特 定 模 式 的 awaitable
类 型。例 如,Windows Runtime API 定义了自己专用的异步操作接口。这些接口不能转化为 Task 类型,但确实遵循了可等待的 (awaitable) 模式
,因此可以直接使用 await。这种 awaitable 类型 在 Windows 应用商店程序中更加常见,但是在大多数情况下,await 使用 Task 或 Task
有两种基本的方法可以创建 Task 实例:
- 计算类:
Task.Run
(如需要按照特定的计划运行,则用TaskFactory. StartNew
) - I/O 型:TaskCompletionSource