《Concurrency in C# Cookbook》--- 读书随记(1)
CHAPTER 1 Concurrency: An Overview
《Concurrency in C# Cookbook》
Asynchronous, Parallel, and Multithreaded ProgrammingAuthor: Stephen Cleary
如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的
Introduction to Concurrency
在继续之前,我想澄清一些我将在本书中使用的术语。这些是我自己的定义,我一直使用它们来消除不同编程技术的歧义。让我们从并发性开始
Concurrency
- Doing more than one thing at a time. 同一时间内做多个事情
当您需要应用程序在处理其他事情时完成一件事情时,您需要Concurrency。世界上几乎所有的软件应用程序都可以从并发中获益
Multithreading
- A form of concurrency that uses multiple threads of execution.
多线程是指字面上使用多个线程。多线程是并发的一种形式,但肯定不是唯一的一种。事实上,在现代应用程序中,直接使用低级线程类型几乎没有任何意义; 高级抽象比老式多线程更强大、更有效。因此,我将尽量减少对过时技术的报道。本书中的多线程处理方法都没有使用 Thread 或 BackoundWorker 类型; 它们已经被更好的替代方法所替代
但是不要认为多线程已经死了!多线程继续存在于线程池中,这是对排队工作这种形式的有效使用,可以根据需求自动调整自身。反过来,线程池又支持另一种重要的并发形式: 并行处理
Parallel processing
- Doing lots of work by dividing it up among multiple threads that run concurrently.
并行处理(或并行编程)使用多线程来最大限度地利用多处理器内核。现代 CPU 有多个内核,如果有很多工作要做,那么让一个内核完成所有工作而其他内核空闲是没有意义的。并行处理将工作分配给多个线程,每个线程可以在不同的内核上独立运行
并行处理是多线程的一种类型,而多线程是并发的一种类型。在现代应用程序中,还有另一种并发类型很重要,但许多开发人员并不熟悉: 异步编程
Asynchronous programming
- A form of concurrency that uses futures or callbacks to avoid unnecessary threads.
future(或promise)是一种表示将在未来完成的某种操作的类型。.NET 中一些现代的未来类型是 Task 和 Task < TResult > 。旧的异步 API 使用回调或事件而不是future。异步编程的核心思想是异步操作: 一些已经启动的操作将在一段时间后完成。当操作正在进行时,它不会阻塞原始线程; 启动操作的线程可以自由地执行其他工作。当操作完成时,它通知它的future,或者调用它的回调或事件来让应用程序知道操作已经完成
异步编程是一种强大的并发形式,但直到最近,它还需要极其复杂的代码。现代语言中的async和await支持使得异步编程几乎和同步(非并发)编程一样简单
另一种形式的并发是响应式编程并发。异步编程意味着应用程序将启动一个稍后将完成的操作。响应式编程与异步编程密切相关,但构建于异步事件而非异步操作之上。异步事件可能没有实际的“开始”,可能在任何时候发生,并且可能被引发多次
Reactive programming
- A declarative style of programming where the application reacts to events.
如果您认为应用程序是一个大型状态机,那么可以将应用程序的行为描述为通过在每个事件中更新其状态来响应一系列事件。这并不像听起来那样抽象或理论化; 现代框架使得这种方法在实际应用程序中非常有用。响应式编程不一定是并发的,但是它与并发密切相关,所以这本书涵盖了基础知识
通常,在编写并发程序时使用多种技术。大多数应用程序至少使用多线程(通过线程池)和异步编程。可以随意混合和匹配所有不同形式的并发,为应用程序的每个部分使用适当的工具
Introduction to Asynchronous Programming
异步编程有两个主要的好处。第一个好处是终端用户 GUI 程序: 异步编程支持响应性。每个人都使用过在工作时暂时锁定的程序; 异步程序在工作时可以保持对用户输入的响应。第二个好处是服务器端程序: 异步编程支持可伸缩性。服务器应用程序可以通过使用线程池进行某种程度的扩展,但是异步服务器应用程序通常可以比线程池更好地扩展数量级
异步编程的两个好处来自同一个基本方面:
异步编程释放线程。对于 GUI 程序,异步编程释放了 UI 线程; 这允许 GUI 应用程序保持对用户输入的响应。对于服务器应用程序,异步编程释放请求线程; 这允许服务器使用其线程来服务更多请求
现代异步 .NET 应用程序使用两个关键字: async和await。将 async 关键字添加到方法声明中,并执行一个双重用途: 它启用该方法中的 await 关键字,并向编译器发出信号,要求为该方法生成一个状态机,类似于屈服返回的工作方式。如果异步方法返回值,它可以返回 Task < TResult > ,如果不返回值,则返回 Task,或者返回任何其他“task-like”类型,如 ValueTask。此外,如果异步方法返回枚举中的多个值,它可以返回 IAsyncEnumerator < T > 或 IAsyncEnumerator < T > 。task-like类型表示futures; 它们可以在异步方法完成时通知调用代码
async Task DoSomethingAsync()
{
int value = 13;
// Asynchronously wait 1 second.
await Task.Delay(TimeSpan.FromSeconds(1));
value *= 2;
// Asynchronously wait 1 second.
await Task.Delay(TimeSpan.FromSeconds(1));
Trace.WriteLine(value);
}
异步方法开始同步执行,就像任何其他方法一样。在异步方法中,await 关键字对其参数执行异步等待。首先,它检查操作是否已经完成; 如果已经完成,则继续执行(同步)。否则,它将暂停异步方法并返回未完成的任务。当该操作在一段时间后完成时,异步方法将恢复执行
您可以将异步方法想象为具有几个同步部分,并通过 wait 语句进行分解。第一个同步部分在调用该方法的任何线程上执行,但是其他同步部分在哪里执行?答案有点复杂
当您await一个任务(最常见的场景)时,当await决定暂停该方法时,将捕获一个上下文。这是当前的 SynchronizationContext,除非它为 null,在这种情况下,上下文是当前的 TaskScheduler。该方法在捕获的上下文中继续执行。通常,这个上下文是 UI 上下文(如果您在 UI 线程上)或线程池上下文(大多数其他情况)。如果您有一个 ASP.NET Classic (pre-Core)应用程序,那么上下文也可以是 ASP.NET 请求上下文。ASP.NET Core 使用线程池上下文,而不是特殊的请求上下文
因此,在前面的代码中,所有同步部分都将尝试在原始上下文中恢复。如果从一个 UI 线程调用 DoSomething Async,它的每个同步部分将在该 UI 线程上运行; 但是如果从一个线程池线程调用它,它的每个同步部分将在任何线程池线程上运行
创建 Task 实例有两种基本方法。有些任务表示 CPU 必须执行的实际代码; 这些计算任务应该通过调用 Task.Run (或 TaskFactory.StartNew,如果您需要它们在特定的计划程序上运行)来创建
其他任务表示通知; 这些类型的基于事件的任务由 TaskCompletionSource < TResult > (或其快捷方式之一)创建。大多数 I/O 任务使用 TaskCompletionSource < TResult >
对于异步和等待,错误处理是自然的。在接下来的代码片段中,PossibleExceptionAsync 可能会抛出 NotSupportedException,但是 TrySomethingAsync 可以自然地捕获该异常。捕获的异常的堆栈跟踪得到了恰当的保存,并且没有被人为地包装在 TargetInvocationException 或 AggreateException 中:
async Task TrySomethingAsync()
{
try
{
await PossibleExceptionAsync();
}
catch (NotSupportedException ex)
{
LogException(ex);
throw;
}
}
当异步方法引发(或传播)异常时,异常被放置在其返回的 Task 上,并且 Task 完成。当等待 Task 时,await 操作符将检索该异常,并(重新)以保留其原始堆栈跟踪的方式抛出该异常。因此,如果 PossibleExceptionAsync 是一个异步方法,那么下面这样的代码就可以正常工作:
async Task TrySomethingAsync()
{
// The exception will end up on the Task, not thrown directly.
Task task = PossibleExceptionAsync();
try
{
// The Task's exception will be raised here, at the await.
await task;
}
catch (NotSupportedException ex)
{
LogException(ex);
throw;
}
}
当涉及到异步方法时,还有另一个重要的指导原则: 一旦开始使用异步,最好允许它在代码中增长。如果调用一个异步方法,您应该(最终)等待它返回的任务。抵制调用 Task.Wait、 Task < Tresult >.Result 或 GetWaiter().GetResult()的诱惑; 这样做可能会导致死锁
async Task WaitAsync()
{
// This await will capture the current context ...
await Task.Delay(TimeSpan.FromSeconds(1));
// ... and will attempt to resume the method here in that context.
}
void Deadlock()
{
// Start the delay.
Task task = WaitAsync();
// Synchronously block, waiting for the async method to complete.
task.Wait();
}
如果从 UI 或 ASP.NET 经典上下文调用此示例中的代码,则会发生死锁,因为这两种上下文一次只允许一个线程进入。Deadlock将调用 WaitAsync,从而开始延迟。Deadlock然后(同步地)等待该方法完成,阻塞上下文线程。当延迟完成时,await在捕获的上下文中恢复 WaitAsync 的尝试,但这是不可能的,因为上下文中已经有一个线程被阻塞,而且上下文一次只允许一个线程。可以通过两种方式防止死锁: 在 WaitAsync 中使用 ConfigureAwait(false)(这会导致await忽略其上下文) ,或者await对 WaitAsync 的调用(使Deadlock成为一个异步方法)
对于更完整的异步介绍,Microsoft 为异步提供的在线文档非常棒; 我建议至少阅读异步编程概述(http://bit.ly/async-prog)和基于任务的异步模式(TAP)概述(http://bit.ly/task-async-patt)。如果您想深入了解一下,还有 Async in Depth (http://bit.ly/async-indepth) 文档
Introduction to Parallel Programming
暂时跳过
Introduction to Reactive Programming (Rx)
暂时跳过
Introduction to Dataflows
暂时跳过
Introduction to Multithreaded Programming
线程是一个独立的执行者。每个进程都有多个线程,每个线程可以同时执行不同的任务。每个线程都有自己的独立堆栈,但与进程中的所有其他线程共享相同的内存。在某些应用程序中,有一个线程是特殊的。例如,用户界面应用程序有一个特殊的 UI 线程,控制台应用程序有一个特殊的主线程
每个 NET 应用程序都有一个线程池。线程池维护了许多工作线程,这些线程正在等待执行您让它们执行的任何工作。线程池负责在任何时候确定线程池中有多少个线程。您可以使用几十种配置设置来修改这种行为,但是我建议您不要管它; 线程池已经被仔细地调优,以覆盖绝大多数真实场景
您几乎不需要自己创建新线程。只有在需要 STA 线程用于 COM 互操作时才应该创建 Thread 实例
线程是一个低级抽象。线程池的抽象级别稍高一些; 当代码队列工作于线程池时,线程池本身将在必要时负责创建线程。本书所涉及的抽象更高: 并行和数据流处理队列根据需要作用于线程池。使用这些高级抽象的代码比使用低级抽象的代码更容易正确
Collections for Concurrent Applications
有几种集合类别对并发编程很有用: 并发集合和不可变集合。并发集合允许多个线程以安全的方式同时更新它们。大多数并发集合使用快照使一个线程能够枚举值,而另一个线程可能正在添加或删除值。并发集合通常比仅使用锁保护常规集合更有效
不可变集合有些不同。实际上不能修改不可变集合; 相反,若要修改不可变集合,请创建一个表示已修改集合的新集合。这听起来效率低得可怕,但是不可变的集合在集合实例之间共享尽可能多的内存,所以它没有听起来那么糟糕。不可变集合的好处是所有操作都是纯的,因此它们可以很好地与函数代码一起工作
Modern Design
大多数并发技术有一个相似的方面: 它们在本质上是functional的。我并不是指“他们完成了工作”,而是指基于复合函数的编程风格。如果采用functional思维,并发设计就不会那么复杂了
函数式编程的一个原则是纯粹性(即避免副作用)。解决方案的每一部分都接受一些值作为输入,并生成一些值作为输出。您应该尽可能避免让这些部分依赖于全局(或共享)变量或更新全局(或共享)数据结构。无论这个块是异步方法、并行任务、 Reactive 操作还是数据流块,都是如此。当然,您的计算迟早会产生影响,但是如果您能够用纯粹的部分处理处理,然后用结果执行更新,那么您会发现您的代码更加干净
函数式编程的另一个原则是不变性。不可变性意味着一段数据不能更改。不可变数据对并发程序有用的一个原因是,您永远不需要不可变数据的同步; 不可变数据不能更改的事实使同步成为不必要的。不可变数据还可以帮助您避免副作用
Summary of Key Technologies
.NET framework从一开始就对异步编程提供了一些支持。然而,异步编程在2012年之前一直很困难。NET 4.5(以及 C # 5.0和 VB 2012)引入了异步和等待关键字
任务并行库是在 .NET 4.0中引入的,它完全支持数据和任务并行。如今,它甚至可以在资源较少的平台上使用,比如移动电话。TPL 内置于 .NET 中
System.Reactive 团队一直在努力支持尽可能多的平台。System.Reactive 与异步和 wait 一样,为所有类型的应用程序(客户机和服务器)提供了好处。System.Reactive 可在 System.Reactive NuGet 包中获得
TPL 数据流库正式发布在 System.Threading.Tasks.Dataflow 的 NuGet 包中
大多数并发集合都内置在 NET 中; System.Threading.Channels NuGet 包中还有一些其他并发集合可用。System.Collections.ImmutableNuGet 包中提供了不可变集合
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?