跳转至

《C# 并发编程经典实例》

简介

《C# 并发编程经典实例》适合具有.NET基础,希望学习最新(2014年)并发编程技术的开发人员阅读。本书侧重实操应用,对底层原理细节描述不是很多。

本书作者Stephen喜欢演讲和写作,在其个人网站http://stephencleary.com/上,有大量受欢迎的博客文章以及开源库和应用。

我于 2025 年 4 月开始学习此书,读书摘要记录如下。

《C#并发编程经典实例》
《Concurrency in C# Cookbook》
检索 豆瓣读书
封面 Snipaste_2025-04-10_09-53-18
附件

译者序

本书译者为相银初老师,他在本书《译者序》章节有着这样一段话,想必大多数踏入职场工作过几年的朋友们,都会深以为然。

翻译中的一点感受

过去的十多年我一直在从事软件开发和设计工作。相信国内很多开发人员都和我一样,心中存在着一个疑惑:我国的软件人员很多(绝对数量不会比美国少),但为什么软件技术总体上落后欧美国家那么多?确定翻译《C#并发编程经典实例》这本书后,我一边仔细阅读原书,一边遵循作者的思路,逐渐发现作者思考问题的一个理念。这就是按软件的不同层次进行明确分工,我只负责我所实现的这个层次,底层技术是为上层服务的,我只负责选择和调用,不管内部的实现过程;同样,我负责的层次为更高一层的软件提供服务,供上层调用,也不需要上层关心我的内部实现。 由此想到,这正好反映出国内开发人员中的一个通病,即分工不够细、技术关注不够精。很多公司和团队在开发时都喜欢大包大揽,从底层到应用层全部自己实现;很多开发人员也热衷于“大而全”地学习技术,试图掌握软件开发中的各种技术,而不是精通某一方面。甚至流行这样一种观点,实现底层软件、写驱动的才是高级开发人员,做上层应用的人仅仅是“码农”。本书作者明确地反对了这种看法,书中强调如何利用好现成的库,而不是全部采用底层技术自己实现。利用现成的库开发出高质量的软件,对技术能力的考验并不低于开发底层库。

2014 年 10 月于深圳

Ch1

优秀软件的一个关键特征就是具有并发性。

1.1 并发编程简介

术语介绍

  • 多线程:并发的一种形式,它采用多个线程来执行程序。

多线程是并发的一种形 式,但不是唯一的形式。实际上,直接使用底层线程类型在现代程序中基本不起作用。比起老式的多线程机制,采用高级的抽象机制会让程序功能更加强大、效率更高。但是,不要认为多线程已经彻底被淘汰了!因为线程池要求多线程继续存在。线程池存放任务的队列,这个队列能够根据需要自行调整。相应地,线程池产生了另一个重要的并发形式:并行处理。

  • 并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。

并行处理是多线程的一种,而多线程是并发的一种。在现代程序中,还有一种非常重要但 很多人还不熟悉的并发类型:异步编程。

  • 异步编程:并发的一种形式,它采用 future 模式或回调 (callback) 机制,以避免产生不必要的线程。

一个 future(或 promise) 类型代表一些即将完成的操作。在 .NET 中,新版 future 类型 有 Task 和 Task。在 老 式 异 步 编 程 API 中,采 用 回 调 或 事 件 (event), 而 不 是 future。异步编程的核心理念是异步操作:启动了的操作将会在一段时间后完成。这个操作 正在执行时,不会阻塞原来的线程。启动了这个操作的线程,可以继续执行其他任务。当 操作完成时,会通知它的 future,或者调用回调函数,以便让程序知道操作已经结束。

异步编程是一种功能强大的并发形式,但直至不久前,实现异步编程仍需要特别复杂的代 码。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