<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Haoran Liao | 廖浩然</title><description>Stay hungry, stay foolish</description><link>https://iaohr9.github.io</link><item><title>并行程序设计复习题库</title><link>https://iaohr9.github.io/blog/%E5%B9%B6%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E5%A4%8D%E4%B9%A0%E9%A2%98%E5%BA%93</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E5%B9%B6%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E5%A4%8D%E4%B9%A0%E9%A2%98%E5%BA%93</guid><description>并行程序设计复习题库，覆盖填空、判断、选择和大题，并附答案、解释与课本 OCR 来源。</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;并行程序设计复习题库&lt;/h1&gt;
&lt;h2&gt;题量概览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;填空题：98&lt;/li&gt;
&lt;li&gt;判断题：72&lt;/li&gt;
&lt;li&gt;选择题：72&lt;/li&gt;
&lt;li&gt;大题：43&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%B8%80%E7%AB%A0-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97&quot;&gt;第一章 为什么要并行计算&lt;/a&gt;：47 题&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%BA%8C%E7%AB%A0-%E5%B9%B6%E8%A1%8C%E7%A1%AC%E4%BB%B6%E5%92%8C%E8%BD%AF%E4%BB%B6&quot;&gt;第二章 并行硬件和软件&lt;/a&gt;：47 题&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%B8%89%E7%AB%A0-mpi-%E8%BF%9B%E8%A1%8C%E5%88%86%E5%B8%83%E5%BC%8F%E5%86%85%E5%AD%98%E7%BC%96%E7%A8%8B&quot;&gt;第三章 MPI 进行分布式内存编程&lt;/a&gt;：47 题&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E5%9B%9B%E7%AB%A0-pthreads-%E8%BF%9B%E8%A1%8C%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E7%BC%96%E7%A8%8B&quot;&gt;第四章 Pthreads 进行共享内存编程&lt;/a&gt;：47 题&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%BA%94%E7%AB%A0-%E7%94%A8-openmp-%E8%BF%9B%E8%A1%8C%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E7%BC%96%E7%A8%8B&quot;&gt;第五章 用 OpenMP 进行共享内存编程&lt;/a&gt;：48 题&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E5%85%AD%E7%AB%A0-cuda-%E5%BC%82%E6%9E%84%E7%BC%96%E7%A8%8B&quot;&gt;第六章 CUDA 异构编程&lt;/a&gt;：49 题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第一章 为什么要并行计算&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;1-填空-1&lt;/h3&gt;
&lt;p&gt;并发强调多个任务在同一________内交替执行，而并行强调多个任务在同一________真正同时运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 时间段；时刻&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料用“同一时间段内交替执行”和“同一时刻真正同时运行”区分并发和并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-2&lt;/h3&gt;
&lt;p&gt;分布式程序把任务拆分到多台物理机器上，并通过________通信协调完成共同目标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 网络&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把分布式定义为多台物理机器通过网络通信协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-3&lt;/h3&gt;
&lt;p&gt;任务并行把________任务分配给不同处理单元；数据并行把________操作应用到不同数据子集。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 不同；相同&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料分别定义任务并行和数据并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-4&lt;/h3&gt;
&lt;p&gt;并行程序中核之间的协调主要包括通信、________和同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 负载均衡&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料列出通信、负载均衡、同步三类协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4；并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-5&lt;/h3&gt;
&lt;p&gt;课程材料中，分布式内存编程主要对应 ________，共享内存编程主要对应 Pthreads 和 OpenMP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在并行系统处写明分布式：MPI；共享：Pthread、OpenMP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-6&lt;/h3&gt;
&lt;p&gt;峰值性能常按核心数、每核频率和每周期________操作数相乘估算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 浮点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出峰值性能 = 核心数、每核频率、每周期浮点操作数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-7&lt;/h3&gt;
&lt;p&gt;继续单纯提高 CPU 频率会让集成电路变热，并最终导致________问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 不可靠&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料指出晶体管传递速度加快会使集成电路变热并最终导致不可靠。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-填空-8&lt;/h3&gt;
&lt;p&gt;在批改试卷例子中，4 个助教每人改 25 份试卷属于________并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 数据&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把每人改 25 份试卷作为数据并行例子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本填空-1&lt;/h3&gt;
&lt;p&gt;书中把并行系统按内存访问方式分为共享内存系统和________内存系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 分布式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明，共享内存系统中各核心原则上可读写每个内存位置；分布式内存系统中每个核心有私有内存，并通过网络发送消息等方式显式通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;课本小结指出，由于物理限制，传统处理器性能提升速度大幅下降，芯片制造商转向在一个芯片上集成多个传统处理器，即________集成电路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 多核&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结把处理器性能提升放缓与多核集成电路的出现直接联系起来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;在课本全局求和伪代码中，初始化数组 x 后、各核心计算部分和前，需要加入一个同步点，即 ________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; Synchronize_cores()&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明如果某些核心还在初始化 x，其他核心就开始计算部分和会出错，所以需要在初始化和部分求和之间同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;树状结构全局求和中，如果核心数是 2 的幂，配对核心可通过当前核心编号与位掩码做按位________得到。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 异或&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本练习说明，每一阶段通过反转编号中与 bitmask 非零位对应的位得到配对核心编号，可用按位异或实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.24&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;共享内存系统中，各核心通过访问共享________位置来协调行动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 内存&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结把共享内存系统定义为各核心共享一个大的内存池，并通过访问共享内存位置协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;分布式内存系统中，每个核心有自己的________内存，核心通过网络发送消息协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 私有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结区分共享内存和分布式内存：分布式内存中每个核心都有自己的私有内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;MIMD 的中文含义是多________流多数据流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 指令&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本把能相互独立运行的核心系统称为多指令流多数据流系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;SIMD 的中文含义是单指令流多________流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 数据&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本把所有核心执行相同指令、处理各自数据流的系统称为单指令流多数据流系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;1-判断-1&lt;/h3&gt;
&lt;p&gt;单处理器继续单纯提高主频会带来发热和可靠性问题，因此继续依靠晶体管速度提升性能不可行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料指出，晶体管传递速度加快会使集成电路变热并最终导致不可靠，所以需要利用晶体管密度增加来放置多个相对简单的处理器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-判断-2&lt;/h3&gt;
&lt;p&gt;所有并发程序都一定是并行程序或分布式程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料表述是所有并行和分布式程序都是并发的；并发只要求多个任务在同一时间段内交替执行，不必同一时刻真正同时运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-判断-3&lt;/h3&gt;
&lt;p&gt;数据并行是把相同操作应用到不同数据子集上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料将数据并行定义为每个处理单元执行相同操作，但处理不同的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-判断-4&lt;/h3&gt;
&lt;p&gt;任务并行要求每个处理单元执行完全相同的操作，只是数据不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 这是数据并行的描述；任务并行是把不同任务或功能分配给不同处理单元。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-判断-5&lt;/h3&gt;
&lt;p&gt;并行程序中，多个核独立工作时相对简单，但核之间协调会增加复杂度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料明确说各核独立工作时并行程序接近串行程序，但核之间通常需要协调，协调包含通信、负载均衡和同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4；并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-判断-6&lt;/h3&gt;
&lt;p&gt;课程材料把 MPI 归入分布式内存编程，把 Pthreads 和 OpenMP 归入共享内存编程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料在并行系统处列出：分布式对应 MPI，共享对应 Pthread、OpenMP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本判断-1&lt;/h3&gt;
&lt;p&gt;在 SIMD 系统中，不可能让一个核心执行加法、另一个核心同时执行乘法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本用加法和乘法示例说明 SIMD 核心必须执行相同指令；若需要不同指令，只能让一个核心执行时另一个空闲，再交换执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.20&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;课本认为普通串行程序通常不能自动利用多核，翻译程序也不太可能承担所有串行到并行的转换工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 第1章小结明确指出，普通串行程序通常无法利用多核，编译/翻译程序不太可能完成全部转换，因此软件开发者需要学习并行程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;只要各核心独立工作，编写并行程序通常就和编写串行程序基本相同；复杂性主要在需要协调时出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在全局求和例子后说明，核心独立工作时并行程序接近串行程序；一旦需要协调，事情会变复杂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.18&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;并行和分布式之间没有硬性的区别，但并行程序中的任务通常耦合更紧密。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结明确指出并行和分布式之间没有硬性区别，并行程序任务通常更紧密耦合。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;并发程序在任何时刻都必须有多个任务真正同时执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本区分并发、并行和分布式：并发是任意时刻有多个任务在进行，不等同于真正同时执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;本书使用 C 语言和 MPI、Pthreads、OpenMP、CUDA 四个 API 学习并行系统编程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结列出将使用 C 语言和四个 API：MPI、Pthreads、OpenMP、CUDA。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;1-选择-1&lt;/h3&gt;
&lt;p&gt;为什么现代并行系统倾向于多核处理器，而不是继续主要提高单核主频？&lt;/p&gt;
&lt;p&gt;A. 因为编译器无法生成串行代码
B. 因为继续加快晶体管速度会带来发热和可靠性问题，而晶体管密度仍可用于放置多个核心
C. 因为内存容量已经无法增加
D. 因为分布式系统不需要通信&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把单处理器主频提升受限和多核处理器出现联系起来：晶体管速度继续加快不可行，但密度增加可以用于并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-选择-2&lt;/h3&gt;
&lt;p&gt;峰值浮点性能的常见估算式是下面哪一个？&lt;/p&gt;
&lt;p&gt;A. 核心数 x 每核频率 x 每周期浮点操作数
B. 内存容量 x 缓存行大小
C. 网络带宽 / 延迟
D. 线程数 + 进程数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料列出的峰值性能计算项为核心数、每核频率、每周期浮点操作数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3；并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-选择-3&lt;/h3&gt;
&lt;p&gt;下列哪项最符合“并行”的定义？&lt;/p&gt;
&lt;p&gt;A. 多个任务在同一时间段内交替执行，看起来同时运行
B. 多个任务在同一时刻真正同时运行，通常依赖多个核心或计算资源
C. 多个任务必须分布在多台物理机器上
D. 只有一个任务在一个核心上执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料区分并发、并行、分布式：并行强调同一时刻真正同时运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-选择-4&lt;/h3&gt;
&lt;p&gt;下列哪项最符合“分布式”的定义？&lt;/p&gt;
&lt;p&gt;A. 同一进程中的多个线程共享内存
B. 将任务拆分到多台物理机器上，通过网络通信协调完成共同目标
C. 同一条指令作用于多个数据
D. 单核通过流水线提高吞吐量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料将分布式定义为任务拆分到多台物理机器，并通过网络通信协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-选择-5&lt;/h3&gt;
&lt;p&gt;4 个助教分别批改 25 份试卷，这属于哪种并行方式？&lt;/p&gt;
&lt;p&gt;A. 任务并行
B. 数据并行
C. 流水线并行
D. 不属于并行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料用批改试卷举例：4 个助教每人改 25 份试卷是数据并行；按题型分工才是任务并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-选择-6&lt;/h3&gt;
&lt;p&gt;并行程序中核之间协调主要包括哪些方面？&lt;/p&gt;
&lt;p&gt;A. 通信、负载均衡、同步
B. 编译、链接、清理
C. 加密、压缩、解压
D. 绘图、渲染、显示&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料直接列出协调过程主要分为通信、负载均衡、同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4；并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本选择-1&lt;/h3&gt;
&lt;p&gt;课本对 MPI、Pthreads、OpenMP、CUDA 的适用系统描述，哪项正确？&lt;/p&gt;
&lt;p&gt;A. MPI 用于分布式内存 MIMD；Pthreads 用于共享内存 MIMD；OpenMP 可用于共享内存 MIMD/SIMD；CUDA 用于 NVIDIA GPU
B. MPI 只用于共享内存 SIMD
C. Pthreads 只用于 GPU
D. CUDA 只用于分布式内存 MIMD 集群&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本明确把 MPI、Pthreads、OpenMP 和 CUDA 分别放到相应的系统类型和编程对象中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;在课本全局求和例子中，第一阶段每个核心计算自己的部分和主要属于哪类并行？&lt;/p&gt;
&lt;p&gt;A. 数据并行
B. 任务并行
C. 流水线并行
D. 串行执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本练习明确把每个核心累加其分配计算值的第一部分称为数据并行例子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.24&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;树状结构全局求和相比“所有核心发送给主核心”的直接方案，主要降低了哪个核心的汇总压力？&lt;/p&gt;
&lt;p&gt;A. 核心0/主核心
B. 最后一个核心
C. 所有空闲核心
D. GPU 设备核心&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明直接方案让主核心完成最终求和，而树状配对让不同核心分阶段合并结果，减少主核心接收和加法数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.17；课本OCR-并行程序设计导论 原书第2版 p.24&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;下列哪项是分布式内存系统协调核心工作的典型方式？&lt;/p&gt;
&lt;p&gt;A. 通过网络发送消息
B. 所有核心隐式读写同一个寄存器
C. 禁用所有通信
D. 只依靠编译器自动向量化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明分布式内存系统中核心通过在网络上发送消息协调行动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;课本把 Pthreads 主要用于哪类系统编程？&lt;/p&gt;
&lt;p&gt;A. 共享内存 MIMD 系统
B. 分布式内存 SIMD 系统
C. 只含 GPU 的系统
D. 单核 SISD 系统&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结说明 Pthreads 用于为共享内存 MIMD 系统编程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;课本把 MPI 主要用于哪类系统编程？&lt;/p&gt;
&lt;p&gt;A. 分布式内存 MIMD 系统
B. 共享内存 SIMD 系统
C. 单线程串行系统
D. 只含统一内存的 GPU&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第1章小结说明 MPI 用于分布式内存 MIMD 系统的编程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;1-大题-1&lt;/h3&gt;
&lt;p&gt;简述为什么需要构建并行系统。要求从单处理器性能提升受限、多核处理器出现、程序员需要利用并行硬件三个角度回答。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 单处理器过去主要依赖晶体管密度增加和速度提升来提高性能，但继续提高速度会带来发热和可靠性问题；与此同时，晶体管密度仍在增加，因此可以在单芯片上放置多个相对简单的处理器，形成多核处理器。硬件已经提供并行资源后，程序必须把计算拆分到多个核心或多台机器上，才能继续获得性能提升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案按资料中的逻辑组织：频率提升受限 -&gt; 利用晶体管密度做多核 -&gt; 需要编写并行程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.3；课程要点-并行程序设计第2版 para 4；课程要点-并行程序设计第2版 para 5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-大题-2&lt;/h3&gt;
&lt;p&gt;比较并发、并行和分布式，并说明三者的包含关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 并发强调多个任务在同一时间段内交替执行，看起来像同时运行；并行强调多个任务在同一时刻真正同时运行，依赖多个核心或多个计算资源；分布式强调任务分布在多台物理机器上，并通过网络通信协调完成目标。资料给出的关系是：所有并行和分布式程序都是并发的，但并发不一定并行，也不一定分布式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 核心区别在于“同一时间段”“同一时刻”和“多台物理机器/网络通信”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4；课程要点-并行程序设计第2版 para 7&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-大题-3&lt;/h3&gt;
&lt;p&gt;比较任务并行和数据并行，并用“批改试卷”的例子说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 任务并行把不同任务或功能分配给不同处理单元，例如助教 A 批选择题、助教 B 批填空题、助教 C 批问答题。数据并行把相同操作应用到不同数据子集上，例如 4 个助教每人批 25 份试卷。二者都可能需要通信、负载均衡和同步来协调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料直接给出了任务并行、数据并行定义和批改试卷例子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.4；并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本大题-1&lt;/h3&gt;
&lt;p&gt;依据课本，说明为什么 GPU 既不是纯 SIMD，也不是纯 MIMD。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; GPU 的给定核心上的数据通路可以使用 SIMD 并行，但当代 GPU 可以在单个核心上运行多个指令流；典型 GPU 还有几十个核心，这些核心也能执行独立指令流。因此 GPU 同时具有 SIMD 和 MIMD 的特征，不能简单归为纯 SIMD 或纯 MIMD。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本直接强调 GPU 不是纯 SIMD 系统，也不是纯 MIMD 系统，并给出核心内和多核心层面的原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.37&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;根据课本全局求和例子，说明通信、负载均衡和同步分别在哪里出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 通信出现在一个或多个核心把当前部分和发送给另一个核心时；负载均衡出现在第一阶段给每个核心分配大致相同数量、且工作量接近的计算值时；同步出现在必须等数组 x 初始化完成后才能开始部分求和，以及树状归约每一阶段必须等待前一阶段结果可用时。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在全局求和例子后逐项说明了通信、负载平衡和同步，并在伪代码处给出 `Synchronize_cores()` 的必要性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.18；课本OCR-并行程序设计导论 原书第2版 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;推导并比较直接全局求和和树状全局求和中核心0的接收/加法次数。假设核心数 p 是 2 的幂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 直接方案中，其他 p-1 个核心都把部分和发送给核心0，所以核心0需要完成 p-1 次接收和 p-1 次加法。树状结构中每一阶段问题规模减半，核心0只在每一层接收并合并一次，因此需要 log2(p) 次接收和 log2(p) 次加法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本练习要求推导核心0在原始伪代码和树状结构全局和中的接收与加法数量，树状配对过程见课本图示和位掩码说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.17；课本OCR-并行程序设计导论 原书第2版 p.24&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;为什么课本把“良好的程序开发技术”放在并行程序中特别强调？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 并行程序除了普通程序的算法和实现问题，还要处理核心之间通信、负载均衡、同步、任务划分和调试等问题。课本指出并行程序通常非常复杂，因此更需要良好的程序开发技术，以避免把协调问题隐藏在难以定位的错误中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 第1章小结和练习前说明并行程序通常复杂，且编写时需要协调通信、负载平衡和同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.22；课本OCR-并行程序设计导论 原书第2版 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二章 并行硬件和软件&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;2-填空-1&lt;/h3&gt;
&lt;p&gt;冯诺依曼架构困境指处理器速度和________访问速度之间差距越来越大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 内存&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 CPU 和主存分离导致 CPU 大量时间等待数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-2&lt;/h3&gt;
&lt;p&gt;Cache 利用两类局部性：时间局部性和________局部性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 空间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明程序员不能显式控制 Cache Line，需要利用时间局部性和空间局部性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-3&lt;/h3&gt;
&lt;p&gt;写回策略会先把被修改的 Cache Line 标记为________，等换出 Cache 后再写回主存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 脏的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料对写回的定义是标记 Cache Line 为脏，换出时再写主存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-4&lt;/h3&gt;
&lt;p&gt;Flynn 分类法按照同时管理的________流数目和数据流数目分类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 指令&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Flynn 分类法按指令流和数据流数目分类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.7&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-5&lt;/h3&gt;
&lt;p&gt;SIMD 可以抽象为一个控制单元和多个________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; ALU&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 SIMD 有一个控制单元和多个 ALU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-6&lt;/h3&gt;
&lt;p&gt;MIMD 系统的两类主要形式是共享内存系统和________内存系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 分布式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料说明 MIMD 系统主要类型为共享内存和分布式内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-7&lt;/h3&gt;
&lt;p&gt;效率 E 的常用公式是 E = S / ________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; p&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明效率表示 S 接近线性加速比的程度，所以除以 p。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.16&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-填空-8&lt;/h3&gt;
&lt;p&gt;固定问题规模、增加核数并希望效率维持不变，称为________扩展性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 强&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义强扩展性为固定问题规模，增加核数 P，效率维持不变。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本填空-1&lt;/h3&gt;
&lt;p&gt;SPMD 程序不是在每个核心上运行不同程序，而是由同一个可执行程序在不同进程/线程上运行，通常依靠________区分各自工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 编号或秩&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在并行软件部分说明主要关注 SPMD：同一程序在多个进程/线程上运行，再由各自身份决定执行哪部分任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.46&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;课本小结中，CPU 和内存分离限制指令执行速度的问题称为________瓶颈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 冯·诺依曼&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章小结说明，CPU 和内存分离被称为冯·诺依曼瓶颈，因为它限制了指令执行速度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;虚拟地址和物理地址的对应关系存储在主存中的页表；为减少页表访问，CPU 使用页表缓存，即________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 转换查找缓冲区（TLB）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明页表在主存中，直接查页表可能导致额外内存访问，因此 CPU 有转换查找缓冲区存储最近使用的页表条目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;课本把由处理器和编译器控制、程序员通常不直接控制的低层次并行分为指令级并行和________级并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 线程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本将 ILP 和 TLP 作为低层次并行讨论，并指出它们通常由处理器和编译器控制，而不是程序员直接控制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;操作系统通过在执行程序之间快速切换，即使没有多个核心，也能产生多个作业同时运行的________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 假象&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章小结说明，多任务操作系统通过快速切换产生多个作业同时运行的假象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;指令级并行的两种主要类型是流水线和________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 多发射&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章小结列出 ILP 两种主要类型：流水线和多发射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;Flynn 分类法区分系统可同时管理的________流数量和数据流数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 指令&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 Flynn 分类法区分系统可以同时管理的指令流数量和数据流数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;如果所有处理器具有相同体系结构，该系统称为________系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 同构&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明所有处理器具有相同体系结构的系统是同构系统；处理器功能不同则是异构系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;2-判断-1&lt;/h3&gt;
&lt;p&gt;冯诺依曼架构困境是指处理器速度和内存访问速度差距增大，CPU 大量时间等待数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料把 CPU 和主存分离、CPU 等待数据作为冯诺依曼架构困境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-判断-2&lt;/h3&gt;
&lt;p&gt;Cache 通常一次只从内存读取一个标量数据，程序员可以显式控制每个 Cache Line 进入 Cache。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明通常一次读取一个 Cache Line，且程序员不能显式控制 Cache Line 放入 Cache，需要利用时间局部性和空间局部性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-判断-3&lt;/h3&gt;
&lt;p&gt;空间局部性是指访问某个内存位置后，接下来很可能访问附近位置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料直接给出空间局部性的定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-判断-4&lt;/h3&gt;
&lt;p&gt;SIMD 系统可以抽象为一个控制单元和多个 ALU，同一条指令广播给多个 ALU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料对 SIMD 的说明正是一个控制单元和多个 ALU，控制单元广播同一条指令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-判断-5&lt;/h3&gt;
&lt;p&gt;MIMD 系统与 SIMD 系统一样要求所有处理单元同步执行同一条指令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MIMD 支持多个指令流在多个数据流上操作，通常是异步的；同步同指令是 SIMD 的特点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-判断-6&lt;/h3&gt;
&lt;p&gt;伪共享发生在多个核心访问同一个 Cache Line 中的不同变量时，可能造成性能急剧下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把伪共享定义为多个核心访问同一 Cache Line 中不同变量而触发无意义一致性开销。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.13&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本判断-1&lt;/h3&gt;
&lt;p&gt;伪共享不会导致错误结果，但会因为多余主存访问而破坏程序性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在伪共享小节明确指出，伪共享不会导致不正确的结果，但会使对主存的访问超出必要次数，从而破坏性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.45&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;如果程序员能够看到硬件并行性并修改源代码利用它，课本就把这种硬件视为并行硬件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在并行硬件小结中给出这个操作性定义：程序员可见并可通过源代码利用的并行性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;在课本假设中，同构 MIMD 程序计时通常应使用 CPU 时间，而不是挂钟时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明应使用挂钟时间，因为程序可能在等待消息等状态下仍是活动的，即使核心空闲；CPU 时间不能反映这种等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;线程由进程启动，通常比进程需要更少的相关数据和信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章小结说明，进程有许多相关数据和信息，线程由进程启动，不需要那么多相关数据和信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;缓存一致性的单位通常是单个变量，因此不会出现伪共享。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明缓存一致性的单位是缓存行，不是单个变量，这正是伪共享产生的原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;异构 CPU-GPU 系统中，本书通常在 CPU 上启动程序，再由 CPU 把计算工作分配给 GPU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章假设部分说明，在异构 CPU-GPU 系统上，将在 CPU 上启动程序，CPU 将在 GPU 上启动计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;2-选择-1&lt;/h3&gt;
&lt;p&gt;资料中列出的缓解冯诺依曼架构困境的三类改进措施是？&lt;/p&gt;
&lt;p&gt;A. Cache、虚拟内存、低层次并行
B. MPI、Pthreads、OpenMP
C. 写直达、写回、写合并
D. 主机、设备、网格&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料明确列出 Cache、虚拟内存和低层次并行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-选择-2&lt;/h3&gt;
&lt;p&gt;下列关于写直达和写回的说法哪项正确？&lt;/p&gt;
&lt;p&gt;A. 写直达修改 Cache 后立刻写回主存；写回先把 Cache Line 标为脏，换出时再写主存
B. 写回每次修改后立刻写主存
C. 写直达只适用于寄存器
D. 二者都与主存一致性无关&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料分别定义了写直达和写回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-选择-3&lt;/h3&gt;
&lt;p&gt;Flynn 分类法主要根据什么划分体系结构？&lt;/p&gt;
&lt;p&gt;A. 同时管理的指令流数目和数据流数目
B. 处理器品牌和主频
C. 是否支持 C 语言
D. Cache Line 大小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Flynn 分类法按能够同时管理的指令流数目和数据流数目分类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.7&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-选择-4&lt;/h3&gt;
&lt;p&gt;下列哪项是共享内存系统的典型特点？&lt;/p&gt;
&lt;p&gt;A. 多个处理单元共享同一块主内存空间，可以直接访问和通信
B. 每个处理单元只能通过显式消息访问别人的内存
C. 只能运行 MPI
D. 没有 Cache 一致性问题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料将共享内存系统定义为多个处理单元共享同一块主内存空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-选择-5&lt;/h3&gt;
&lt;p&gt;若程序有 10% 串行部分，根据资料对阿姆达尔定律的说明，理论最高加速比约为多少？&lt;/p&gt;
&lt;p&gt;A. 2
B. 4
C. 10
D. 与核心数无限线性增长&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; C&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料给出串行部分为 10% 时，最高加速比只有 10。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.18&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-选择-6&lt;/h3&gt;
&lt;p&gt;强扩展性与弱扩展性的关键区别是？&lt;/p&gt;
&lt;p&gt;A. 强扩展固定问题规模，弱扩展随核数增加而按比例增加问题规模
B. 强扩展只用于 GPU，弱扩展只用于 CPU
C. 强扩展不考虑效率，弱扩展不考虑核数
D. 强扩展必须用 MPI，弱扩展必须用 OpenMP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义强扩展性为固定问题规模增加核数，弱扩展性为核数增加时问题规模也相同规模增加以维持效率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本选择-1&lt;/h3&gt;
&lt;p&gt;课本列出的 Foster 并行程序设计方法四步，正确顺序是？&lt;/p&gt;
&lt;p&gt;A. 划分、通信、凝聚或聚合、映射
B. 映射、划分、通信、凝聚
C. 通信、映射、划分、凝聚
D. 凝聚、映射、通信、划分&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本引用 Foster 方法论，依次为划分、通信、凝聚或聚合、映射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.62&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;课本说评估互连网络常看的两个整体指标是什么？&lt;/p&gt;
&lt;p&gt;A. 等分宽度或等分带宽
B. 源代码行数和编译时间
C. 寄存器个数和页大小
D. 线程名和进程号&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结中说通常通过等分宽度或等分带宽评估网络能支持多少同步通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;下列哪项最符合课本对分布式内存消息传递的描述？&lt;/p&gt;
&lt;p&gt;A. 至少包含发送函数和接收函数；send 可能阻塞或缓存，receive 通常阻塞直到收到消息
B. 所有进程直接读写同一变量，不需要通信
C. 只能通过共享全局数组通信
D. 接收函数通常在消息到达前立即返回&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在消息传递小结中描述 send/receive 的常见行为：send 可能阻塞或复制到内部存储，receive 最常见行为是阻塞。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.70&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;下列哪项属于线程级并行而不是指令级并行？&lt;/p&gt;
&lt;p&gt;A. 硬件多线程
B. 流水线
C. 多发射
D. 单条指令译码&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本将硬件多线程与 TLP 联系起来，而流水线和多发射属于 ILP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;课本对线性加速比的理想描述是？&lt;/p&gt;
&lt;p&gt;A. p 个进程/线程时并行程序运行速度是串行程序的 p 倍
B. p 个进程时运行时间增加 p 倍
C. 效率一定为 0
D. 只要加线程就无限快&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本性能小结说明，p 个进程/线程且每核心不超过一个时，理想是并行程序速度为串行程序的 p 倍，称为线性加速比。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.70&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;强可扩展性的定义更接近哪项？&lt;/p&gt;
&lt;p&gt;A. 问题规模不变，增加进程/线程数仍能保持效率
B. 问题规模必须按线程数同比增加
C. 只适用于单核程序
D. 只看编译时间是否降低&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本区分强/弱可扩展性：问题规模保持不变可保持效率称为强可扩展性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;2-大题-1&lt;/h3&gt;
&lt;p&gt;结合 `sum += z[i]` 这类数组求和循环，解释时间局部性和空间局部性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 时间局部性体现在 `sum` 被反复读取和更新，CPU 可以把它保留在寄存器或 Cache 中，避免每次从主存加载。空间局部性体现在数组 `z` 连续存储，访问 `z[0]` 时可能把包含后续元素的 Cache Line 一起读入，之后访问 `z[1]`、`z[2]` 等更容易命中 Cache。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案直接来自资料对 `float z[1000]` 求和示例的解释。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-大题-2&lt;/h3&gt;
&lt;p&gt;比较 SIMD 和 MIMD，并说明它们适合的并行模式或系统类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; SIMD 用一个控制单元向多个 ALU 广播同一条指令，让不同 ALU 处理不同数据，适合规则的数据并行，但复杂分支会降低效率。MIMD 支持多个独立处理单元在多个数据流上运行不同指令，通常异步，是目前使用较多的形式，可进一步分为共享内存系统和分布式内存系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在 SIMD/MIMD 小节分别说明了控制单元、ALU、同步/异步、共享内存和分布式内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.8；课程要点-计算机体系结构-量化研究方法-第六版-第四章 para 4；课程要点-计算机体系结构-量化研究方法-第六版-第四章 para 5&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-大题-3&lt;/h3&gt;
&lt;p&gt;解释加速比、效率、阿姆达尔定律和可扩展性之间的关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 加速比衡量并行程序相对串行程序的变快程度，线性加速比是 S=P。效率 E=S/P，表示实际加速比接近线性加速比的程度。阿姆达尔定律说明只要存在串行部分，加速比就有上限。可扩展性描述增加处理器数量时性能能否持续提升；强扩展固定问题规模，弱扩展在增加核数时也增加问题规模。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在性能评估页依次解释了加速比、效率、阿姆达尔定律和强/弱扩展性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.16；并行复习 p.17；并行复习 p.18；并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本大题-1&lt;/h3&gt;
&lt;p&gt;用课本直方图例子解释为什么直接共享 `bin_counts[b]++` 会出现竞争条件，以及一种避免思路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 如果两个进程或线程被分配到属于同一个桶 b 的数据元素，它们都会执行 `bin_counts[b]++`。若 `bin_counts` 是共享数组，这两个更新可能交错执行，导致丢失更新，形成竞争条件。避免思路是把 `bin_counts` 划分为各进程/线程的局部计数，例如使用 `loc_bin_counts`，最后再合并局部结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 Foster 方法论的直方图示例中指出，共享 `bin_counts[b]++` 会导致竞争条件，而按进程/线程划分 `loc_bin_counts` 可规避共享更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.64&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;解释为什么按行遍历二维数组通常比按列遍历更符合缓存局部性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; C 语言二维数组按行连续存储。按行遍历时，读入一个缓存行后会连续使用同一行中相邻元素，空间局部性好；按列遍历时，相邻访问跨越较大地址间隔，已载入的同一缓存行很快被逐出，缓存未命中更多。课本示例中第一对嵌套循环显著快于第二对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本用 MAX=1000 的二维数组循环示例说明，两种嵌套循环因缓存未命中次数不同而有明显性能差异。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.31&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;说明课本中 MIMD 系统、混合系统、异构系统和同构系统的关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MIMD 系统执行多个独立指令流，每个指令流可有自己的数据流。较大的 MIMD 系统常是混合系统，即多个较小共享内存节点通过互连网络连接。若处理器功能不同，如 CPU 与 GPU 组合，就是异构系统；若所有处理器具有相同体系结构，就是同构系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章小结在 MIMD 系统段落中依次定义了这些系统类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;按课本说明，如何较可靠地测量并行程序运行时间？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 应在源代码中调用计时器，记录挂钟时间；在同构 MIMD 系统中，通常先同步进程/线程再启动计时器，停止计时后取所有进程/线程中的最大运行时间；由于系统可变性，应对同一数据集多次运行，通常取最短时间；为减少可变性，通常每个核心不超过一个进程或线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本性能小结明确说明计时要用挂钟时间、并行时间要考虑同步和最大运行时间，并建议多次运行取较稳定结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第三章 MPI 进行分布式内存编程&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;3-填空-1&lt;/h3&gt;
&lt;p&gt;MPI 程序通常用 ________ 编译，用 mpiexec 执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; mpicc&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料给出 `mpicc -o hello hello.c` 和 `mpiexec -n 4 ./hello`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-2&lt;/h3&gt;
&lt;p&gt;MPI_Init 必须在所有其他 MPI 函数调用________执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 之前&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MPI_Init 必须在所有其他 MPI 函数调用之前执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.20&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-3&lt;/h3&gt;
&lt;p&gt;MPI_Comm_rank 用于获取当前进程在通信子中的________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; rank&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料给出 MPI_Comm_rank 的作用和示例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.21&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-4&lt;/h3&gt;
&lt;p&gt;MPI_Send 的 count 参数表示发送的数据项个数，而不是________数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 字节&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料参数表明确 count 是数据项个数，不是字节数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.21&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-5&lt;/h3&gt;
&lt;p&gt;MPI_Recv 可用通配符 MPI_ANY_SOURCE 匹配任意________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 消息来源进程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 source 可用 MPI_ANY_SOURCE 匹配任意来源。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-6&lt;/h3&gt;
&lt;p&gt;MPI 点对点消息匹配主要看 source、tag 和________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; comm&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 MPI_Recv 与 MPI_Send 匹配需要 source、tag、comm。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-7&lt;/h3&gt;
&lt;p&gt;MPI_Reduce 将多个进程的数据按指定归约操作聚合，并把结果返回到指定________进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 根&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MPI_Reduce 将结果返回到指定根进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.25&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-填空-8&lt;/h3&gt;
&lt;p&gt;MPI_Allgather 可以理解为 MPI_Gather 加上 ________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI_Bcast&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料直接写出 `MPI_Allgather = MPI_Gather + MPI_Bcast`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.30&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本填空-1&lt;/h3&gt;
&lt;p&gt;MPI_Init 的目的之一是定义包含启动时所有进程的通信域，称为________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI_COMM_WORLD&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 MPI_Init 会进行必要设置，其中一个目的就是定义包含用户启动程序所有进程的通信域 MPI_COMM_WORLD。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.78&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;从程序员视角看，分布式内存系统由通过网络连接的核心-________对组成，相关存储器只有该核心可直接访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 存储&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章开头把分布式内存系统描述为通过网络连接的核心-存储对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.75&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;MPI 点对点消息通常包含数据和“信封”信息，信封中包括目的进程、发送进程、标签、通信域和________信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 消息大小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本解释 MPI 发送进程会组装消息，给数据加上信封信息，其中包含目的、源、标签、通信域和消息大小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.83&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;MPI_Bcast 是把单个源进程的数据发送到通信域中的________进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 所有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在广播小节说明，source_proc 进程把 data_p 引用的内容发送给通信器中的所有进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.94&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;MPI 是一个可以从 C 或 Fortran 程序中调用的________库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结第一句说明 MPI 是一个函数库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.116&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;许多系统使用 ________ 编译 MPI 程序，使用 mpiexec 运行 MPI 程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; mpicc&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结说明许多系统使用 mpicc 编译、mpiexec 运行 MPI 程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.116&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;C MPI 程序应包含 ________ 头文件以获得 MPI 函数原型、类型和宏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; mpi.h&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结说明 C MPI 程序应该包含 mpi.h 头文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.116&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;MPI_Comm_rank 获取进程在通信域中的________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 序列号或 rank&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在通信域、MPI_Comm_size 和 MPI_Comm_rank 小节说明该函数获取当前进程编号。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.78&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;3-判断-1&lt;/h3&gt;
&lt;p&gt;MPI 面向分布式内存，主要角色是进程，并且本质上是一个函数库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在第三章开头说明 MPI 是分布式内存的，角色主要是进程，实际上是一个函数库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-判断-2&lt;/h3&gt;
&lt;p&gt;MPI_Init 可以在任意 MPI 通信函数之后调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MPI_Init 必须在所有其他 MPI 函数调用之前执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.20&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-判断-3&lt;/h3&gt;
&lt;p&gt;MPI_Recv 的 count 参数表示最多接收的数据项数量，而不是字节数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; MPI_Recv 参数表中 count 被解释为最多接收的数据项数量，不是字节数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-判断-4&lt;/h3&gt;
&lt;p&gt;MPI_ANY_SOURCE 和 MPI_ANY_TAG 可以在发送方和接收方都使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料明确指出这两个通配符只能在接收方使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-判断-5&lt;/h3&gt;
&lt;p&gt;MPI_Send 一定永远阻塞，和消息大小无关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MPI_Send 是否阻塞取决于消息大小是否超过阈值，小消息可能进入内部缓冲区并立即返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.23&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-判断-6&lt;/h3&gt;
&lt;p&gt;MPI 集合通信按 tag 匹配，点对点通信只按调用顺序匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明点对点通信通过 tag 和通信子匹配，集合通信只通过通信子和调用顺序匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.26&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本判断-1&lt;/h3&gt;
&lt;p&gt;根据课本经验，在调用 MPI_Init 之前不应该调用其他 MPI 函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 MPI_Init 和 MPI_Finalize 小节说明，按经验在 MPI_Init 之前不应该调用其他 MPI 函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.77&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;大多数 MPI 程序采用 SPMD 方式：只编译一个程序，再根据进程序列号分支执行不同操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 SPMD 小节说明，只编译单一程序，进程0和其他进程可根据 rank 执行不同代码，这是 MPI 程序常见方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.79&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;MPI_Reduce 这类集合通信存在的价值之一，是把全局和等通信优化负担交给 MPI 实现而不是每个应用程序员。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明全局和有很多可能树结构，期望每个 MPI 程序都写最优函数不合理，因此 MPI_Reduce 把优化负担放到 MPI 实现开发者身上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.90&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;MPI_Finalize 应在程序完成 MPI 使用后调用，用于清理 MPI 运行环境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 MPI_Init/MPI_Finalize 小节说明 MPI_Init 做设置，MPI_Finalize 用于结束 MPI。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.77；课本OCR-并行程序设计导论 原书第2版 p.116&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;MPI 程序中，进程0和其他进程必须编译成不同的可执行文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 SPMD 小节强调只编译单一程序，进程0和其他进程根据 rank 执行不同分支。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.79&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;MPI_Send 和 MPI_Recv 通常称为点对点通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结区分点对点通信和集合通信，MPI_Send/MPI_Recv 属于点对点通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.117&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;3-选择-1&lt;/h3&gt;
&lt;p&gt;编译 MPI 程序的典型命令是哪一个？&lt;/p&gt;
&lt;p&gt;A. gcc -pthread hello.c -o hello
B. mpicc -o hello hello.c
C. nvcc hello.cu -o hello
D. clang -fopenmp hello.c -o hello&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料给出 MPI 编译示例 `mpicc -o hello hello.c`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.19&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-选择-2&lt;/h3&gt;
&lt;p&gt;MPI_Comm_size 的作用是？&lt;/p&gt;
&lt;p&gt;A. 获取通信子中的进程总数
B. 获取当前进程编号
C. 发送一条消息
D. 销毁通信子&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义 MPI_Comm_size 为获取通信子中总共有多少个进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.21&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-选择-3&lt;/h3&gt;
&lt;p&gt;MPI_Send 中的 count 参数表示什么？&lt;/p&gt;
&lt;p&gt;A. 要发送的数据项个数
B. 要发送的字节数
C. 目标进程编号
D. 消息标签&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; MPI_Send 参数表中 count 是要发送的数据项个数，不是字节数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.21&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-选择-4&lt;/h3&gt;
&lt;p&gt;MPI_Recv 与 MPI_Send 成功匹配至少需要哪些字段一致？&lt;/p&gt;
&lt;p&gt;A. source、tag、comm，并且类型和缓冲区容量满足要求
B. 只需要 count 相等
C. 只需要两个进程在同一机器上
D. 只需要 root 相同&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 source、recv_tag/send_tag、recv_comm/send_comm，并补充类型和缓冲区容量条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-选择-5&lt;/h3&gt;
&lt;p&gt;MPI_Reduce 和 MPI_Allreduce 的主要区别是？&lt;/p&gt;
&lt;p&gt;A. MPI_Reduce 只有根进程获得结果；MPI_Allreduce 让所有进程获得结果
B. MPI_Allreduce 只能求最大值
C. MPI_Reduce 不属于集合通信
D. 二者参数完全一样&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 MPI_Reduce 的结果返回到指定根进程，MPI_Allreduce 是希望所有进程都获得归约结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.25；并行复习 p.26&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-选择-6&lt;/h3&gt;
&lt;p&gt;关于 MPI_Scatter 和 MPI_Gather，下列说法正确的是？&lt;/p&gt;
&lt;p&gt;A. Scatter 从根进程分发数据，Gather 把各进程数据聚集到根进程
B. Scatter 和 Gather 都只用于计时
C. Gather 只能在每个进程上产生全局结果
D. Scatter 不属于集合通信&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料把 MPI_Scatter 解释为把 root 中数据分给不同进程，把 MPI_Gather 解释为把所有进程数据聚集到根进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.28；并行复习 p.29&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本选择-1&lt;/h3&gt;
&lt;p&gt;MPI_Reduce 与 MPI_Allreduce 的区别，哪项符合课本小结？&lt;/p&gt;
&lt;p&gt;A. MPI_Reduce 把全局操作结果存储在单个指定进程上；MPI_Allreduce 把结果存储在通信域中所有进程上
B. MPI_Reduce 只能广播，MPI_Allreduce 只能发送点对点消息
C. MPI_Allreduce 不属于集合通信
D. 二者都要求只有 0 号进程调用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结指出，MPI_Reduce 的结果在单个指定进程上，MPI_Allreduce 的结果在所有进程上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.117&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;在课本对 MPI_Send 语义的说明中，发送进程缓存消息时会发生什么？&lt;/p&gt;
&lt;p&gt;A. MPI 系统把消息数据和信封存到内部存储中，MPI_Send 可以返回
B. 接收方必须已经打印消息
C. 发送进程一定永久阻塞
D. 消息标签会被忽略&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明发送进程可能缓存消息，MPI 系统把数据和信封存到内部存储中，调用即可返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.83&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;课本为什么说 MPI_Allgather 可能比手写 Gather+Bcast 更合适？&lt;/p&gt;
&lt;p&gt;A. MPI 可以使用更合适的通信模式，例如避免简单串接两个树形通信
B. Allgather 不需要任何通信
C. Allgather 只能由 0 号进程调用
D. Gather+Bcast 不是 MPI 函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在矩阵-向量乘法例子中说 Gather 后再 Bcast 可能涉及两种树形通信，MPI 提供独立的 Allgather，可能用更好的模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.100&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;MPI_Comm_size 的作用是？&lt;/p&gt;
&lt;p&gt;A. 获取通信域中的进程数
B. 获取本机 CPU 主频
C. 同步所有 GPU 线程
D. 销毁通信域&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本通信域小节给出 MPI_Comm_size 用于获取通信域中的进程数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.78&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;MPI_Bcast 属于哪类通信？&lt;/p&gt;
&lt;p&gt;A. 集合通信
B. 单机文件 I/O
C. 线程互斥
D. CUDA 核函数调用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本把广播列为集合通信函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.94；课本OCR-并行程序设计导论 原书第2版 p.117&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;MPI 中参数混叠指什么？&lt;/p&gt;
&lt;p&gt;A. 把同一个实际参数传给输入缓冲区和输出缓冲区等本不应重叠的参数
B. 多个进程有相同 rank
C. 消息标签为负数
D. 通信域为空&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结指出，在 MPI_Reduce 等函数中将相同实际参数传给输入和输出缓冲区称为参数混叠，MPI 明确禁止输出参数与另一个参数混叠。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.117&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;3-大题-1&lt;/h3&gt;
&lt;p&gt;解释 MPI_Send 和 MPI_Recv 的阻塞语义，并说明大消息双向先 Send 后 Recv 为什么可能死锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI_Recv 一定阻塞，如果消息未到达会等待。MPI_Send 对小消息可能复制到内部缓冲区后返回；对较大消息，如果接收端尚未发起匹配的 MPI_Recv，发送端可能阻塞。若两个进程都先执行大消息 MPI_Send，再执行 MPI_Recv，两个进程都会等待对方先接收，无法到达接收阶段，形成死锁。可以使用 MPI_Sendrecv 或调整收发顺序避免。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案结合复习资料对 Send/Recv 语义的描述和参考资料 2 对大消息死锁原因及 Sendrecv 替代方案的说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.23；参考资料2 para 16；参考资料2 para 17；参考资料2 para 41；参考资料2 para 42&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-大题-2&lt;/h3&gt;
&lt;p&gt;用 MPI 并行梯形积分时，如何划分计算并汇总结果？说明会用到哪些 MPI 函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 基本思路是把积分区间划分为若干梯形，每个进程计算自己负责的一部分局部面积，再用 MPI_Reduce 按求和操作把局部结果汇总到 0 号进程，最后由 0 号进程输出总积分值。程序需要 MPI_Init/MPI_Finalize 管理环境，MPI_Comm_rank 获取当前进程编号，MPI_Comm_size 获取进程数，MPI_Reduce 汇总局部积分结果，MPI_Wtime 可用于计时。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料给出梯形积分公式和 MPI_Reduce 用法；参考资料 2 也列出梯形法中通过 MPI_Reduce 汇总到进程 0。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.23；并行复习 p.25；并行复习 p.31；参考资料2 para 90；参考资料2 para 92&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-大题-3&lt;/h3&gt;
&lt;p&gt;比较 MPI 点对点通信和集合通信，说明匹配规则和调用要求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 点对点通信如 MPI_Send/MPI_Recv 发生在发送方和接收方之间，通过 source、tag、comm 等字段匹配，接收方还可使用 MPI_ANY_SOURCE 和 MPI_ANY_TAG。集合通信涉及通信子中的所有进程，所有进程必须调用相同的集合函数，参数需一致，匹配主要依赖通信子和调用顺序，不能用 MPI_Reduce 与 MPI_Recv 相匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在消息匹配和集合通信与点对点通信部分直接给出这些规则。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.22；并行复习 p.25；并行复习 p.26&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本大题-1&lt;/h3&gt;
&lt;p&gt;结合课本，说明 MPI_Scatter、MPI_Gather 与 MPI_Allgather 的用途和限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI_Scatter 可由根进程把数组块分发给各进程；MPI_Gather 可把各进程的局部数组收集到根进程；MPI_Allgather 可理解为先 Gather 再 Bcast，让所有进程获得收集后的向量。课本提醒 Scatter/Gather 示例对块分布且每个块大小相同的向量工作正常，因此使用时要注意数据分布和每进程块大小限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本给出了 Scatter、Gather 和 Allgather 的函数语义，并指出 Gather/Scatter 示例对相同大小块分布有使用限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.96；课本OCR-并行程序设计导论 原书第2版 p.98；课本OCR-并行程序设计导论 原书第2版 p.100&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;用课本梯形法则例子说明 Foster 方法论在 MPI 并行化中的应用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 先划分任务：计算单个梯形面积和计算面积总和；再识别通信：局部面积需要汇总到总和；然后凝聚：把多个相邻梯形分配给同一进程形成较大的局部任务；最后映射：把局部梯形块映射到各进程，每个进程计算局部积分，再通过归约通信汇总。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 MPI 梯形法则中用 Foster 方法论分析任务、通信、聚合和映射，并以图示说明单个梯形面积与总和之间的通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.85；课本OCR-并行程序设计导论 原书第2版 p.90&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;说明 MPI_Reduce、MPI_Allreduce、MPI_Bcast、MPI_Scatter 和 MPI_Gather 的核心区别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; MPI_Reduce 把全局操作结果放到单个指定进程；MPI_Allreduce 把结果放到所有进程；MPI_Bcast 从单个源进程把数据发送给所有进程；MPI_Scatter 从根进程把不同数据块分散给各进程；MPI_Gather 把各进程数据块收集到根进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第3章小结和集合通信小节分别给出这些集合通信函数的用途。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.94；课本OCR-并行程序设计导论 原书第2版 p.96；课本OCR-并行程序设计导论 原书第2版 p.98；课本OCR-并行程序设计导论 原书第2版 p.117&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;为什么发送零字节消息仍可能有非零运行时间？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 即使 count 为 0，通信仍需要函数调用、进入 MPI 运行时、构造和处理消息信封、进行进程匹配与同步等控制操作。这些固定开销与消息数据字节数无关，因此零字节消息运行时间仍可能非零。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本练习提出 count 为 0 的发送成本问题；前文对消息信封和发送语义的说明支持固定控制开销的解释。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.83；课本OCR-并行程序设计导论 原书第2版 p.124&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第四章 Pthreads 进行共享内存编程&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;4-填空-1&lt;/h3&gt;
&lt;p&gt;pthread_create 的线程入口函数类型必须是 `void* -&gt; ________`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; void*&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料函数原型注释写明线程入口函数必须是 void* 到 void*。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.35&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-2&lt;/h3&gt;
&lt;p&gt;pthread_join 会阻塞调用它的线程，直到目标线程正常________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 返回&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 pthread_join 阻塞调用线程直到目标线程正常返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.35&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-3&lt;/h3&gt;
&lt;p&gt;竞争条件是多个线程同时读取某个共享变量并操作，变量值与线程执行________有关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 顺序&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义竞争条件时强调共享变量的值与线程执行顺序有关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-4&lt;/h3&gt;
&lt;p&gt;互斥锁保证同一时刻只有________个线程可以进入临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义互斥锁保证同一时刻只有一个线程进入临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-5&lt;/h3&gt;
&lt;p&gt;sem_wait 在信号量值大于 0 时会将其减________并继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 sem_wait 若信号量值 &gt; 0，减一并继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.39&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-6&lt;/h3&gt;
&lt;p&gt;路障要求一组线程在某一点集合，等________线程都到达后再继续。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 所有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义路障为等所有线程到达后统一继续。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.39&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-7&lt;/h3&gt;
&lt;p&gt;条件变量通常需要搭配________锁一起使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 互斥&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明条件变量需要搭配互斥锁一起使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.40&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-填空-8&lt;/h3&gt;
&lt;p&gt;线程安全版本的 strtok 是 ________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; strtok_r&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明可以使用线程安全版本 strtok_r。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.46&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本填空-1&lt;/h3&gt;
&lt;p&gt;Pthreads 程序启动线程常用函数是 pthread_create，停止或等待线程完成常用函数是________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; pthread_join&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 Pthreads 小结说明，pthread_create 和 pthread_join 分别用于启动和停止一个线程函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.166&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;课本把观察或调试时可能暂时消失的并发错误称为________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; Heisenbugs&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明，附加调试器或添加 printf 可能改变线程相对时间，使错误似乎消失，这类错误称为 Heisenbugs。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.139&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;忙等待实现栅栏时，如果要重复使用计数器，必须处理计数器________问题，否则第二个栅栏可能失效或挂起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 重置&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明第一个栅栏完成后 counter 等于 thread_count；若不能正确重置，第二个栅栏不会阻塞，或重置时导致线程挂起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.149&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;Pthreads 条件变量总是与一个________锁相关联。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 互斥&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本介绍条件变量时明确说条件变量总是与一个互斥锁相关联。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.151&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;从程序员角度看，共享内存系统是所有核心都可以访问所有________位置的系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 内存&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第4章开头回顾共享内存系统的定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.127&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;互斥锁用于保证对临界区的________访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 互斥&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 mutex 是 mutual exclusion 的缩写，用于限制对临界区的访问，使一次只有一个线程访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.142&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;二元信号量只使用两个值：________和1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 0&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明二元信号量只使用 0 和 1；0 对应锁定，1 对应解锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.146&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;栅栏是一种同步点，在所有线程到达之前，没有线程可以________栅栏继续前进。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 越过&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本定义栅栏为线程都处于同一位置的同步点，在所有线程到达前没有线程可以越过。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.148&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;4-判断-1&lt;/h3&gt;
&lt;p&gt;Pthreads 适合共享内存系统，同一台机器上的多个线程可以通过访问共享变量通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Pthreads 属于共享内存编程，本章重点包括线程之间如何通信，即访问共享变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.34&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-判断-2&lt;/h3&gt;
&lt;p&gt;pthread_join 的作用是强制停止目标线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 pthread_join 是等待线程结束的同步机制，不会停止线程，而是阻塞调用它的线程直到目标线程返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.35&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-判断-3&lt;/h3&gt;
&lt;p&gt;临界区通常是更新共享变量的代码段，一次只能由一个线程进入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把临界区定义为一次只能由一个线程在内执行的代码段，通常用于更新共享变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-判断-4&lt;/h3&gt;
&lt;p&gt;忙等待线程在等待条件时不会消耗 CPU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明忙等待线程不停检查条件，期间没有做有用工作，只是在空转 CPU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-判断-5&lt;/h3&gt;
&lt;p&gt;信号量与互斥锁最大的区别之一是信号量不属于某个线程，并且可以允许多个线程进入临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料明确列出这一差异。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.38&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-判断-6&lt;/h3&gt;
&lt;p&gt;pthread_cond_wait 等待时会释放互斥锁，被唤醒后会重新加锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把 pthread_cond_wait 分解为释放锁、等待信号、被唤醒后重新加锁三个动作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.41&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本判断-1&lt;/h3&gt;
&lt;p&gt;pthread_create 本身不要求传递线程编号，也不要求为线程分配固定序列号。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本指出，给线程分配序列号只是书中使用的一种方便惯例，并不是调用 pthread_create 的要求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.132&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;忙等待保护临界区会反复测试条件并消耗 CPU 周期，在比核心更多线程时性能可能严重下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本指出忙等待线程会在测试上旋转、消耗 CPU 周期；表格和事件序列说明线程数超过核心数时性能可能灾难性下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.140；课本OCR-并行程序设计导论 原书第2版 p.144&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;pthread_cond_wait 阻塞线程时不会释放互斥锁，因此其他线程无法进入临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本强调 pthread_cond_wait 会解锁关联互斥锁并阻塞；如果不解锁，只有一个线程能进入栅栏，程序会挂起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.151；课本OCR-并行程序设计导论 原书第2版 p.152&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;互斥锁会指定线程按编号顺序进入临界区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明有了互斥锁后，线程进入临界区的顺序多多少少是随机的，由系统安排。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.143&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;条件变量允许一个线程暂停执行，直到某个事件或条件发生。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本条件变量小节直接给出这个定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.151&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;线程安全表示一个代码块由多个线程运行时仍能正常工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第2章和第4章均给出线程安全定义：多个线程运行时可以正常工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.70&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;4-选择-1&lt;/h3&gt;
&lt;p&gt;启动 Pthreads 线程的函数是？&lt;/p&gt;
&lt;p&gt;A. pthread_create
B. pthread_join
C. pthread_mutex_lock
D. sem_post&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料把 pthread_create 标为启动线程，并给出函数原型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.34；并行复习 p.35&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-选择-2&lt;/h3&gt;
&lt;p&gt;下列哪组函数最直接对应互斥量的常见生命周期？&lt;/p&gt;
&lt;p&gt;A. pthread_mutex_init / pthread_mutex_lock / pthread_mutex_unlock / pthread_mutex_destroy
B. MPI_Init / MPI_Send / MPI_Recv / MPI_Finalize
C. cudaMalloc / cudaMemcpy / cudaFree
D. omp_get_thread_num / omp_get_num_threads&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料连续列出互斥量初始化、上锁、释放锁、销毁函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36；并行复习 p.37&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-选择-3&lt;/h3&gt;
&lt;p&gt;当线程数大于可用 CPU 核心数时，忙等待相比互斥量可能出现什么问题？&lt;/p&gt;
&lt;p&gt;A. 性能严重下降甚至变差
B. 自动变成分布式内存
C. 消除所有同步开销
D. 保证矩阵乘法一定正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明线程数大于可用核心时，忙等待同步性能严重下降甚至变差，而互斥量较稳定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.37；并行复习 p.38&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-选择-4&lt;/h3&gt;
&lt;p&gt;路障的含义是？&lt;/p&gt;
&lt;p&gt;A. 一组线程在某一点集合，等所有线程到达后再继续
B. 只允许一个线程永久运行
C. 把所有线程销毁
D. 把共享变量复制到每个进程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料定义路障为让一组线程在某一点集合，等所有线程都到达后统一继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.39&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-选择-5&lt;/h3&gt;
&lt;p&gt;为什么 `y[0]` 到 `y[7]` 被不同线程分别修改仍可能很慢？&lt;/p&gt;
&lt;p&gt;A. 它们可能位于同一个 cache line，导致伪共享
B. 它们不在内存中
C. 整数不能被线程修改
D. pthread_join 会取消这些更新&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料解释这些元素可能占据同一个 cache line，每次写导致整个 cache line 失效/同步，形成伪共享。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.44；并行复习 p.45&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-选择-6&lt;/h3&gt;
&lt;p&gt;多线程环境中，`strtok` 可能出错的主要原因是？&lt;/p&gt;
&lt;p&gt;A. 它内部使用共享的静态状态，线程之间可能互相覆盖上下文
B. 它不能分割字符串
C. 它只能在 MPI 中使用
D. 它自动加锁导致死锁&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 strtok 的静态函数指针变量被共享，线程 1 可能用错线程 0 留下的上下文；可用 strtok_r。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.45；并行复习 p.46&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本选择-1&lt;/h3&gt;
&lt;p&gt;关于 sem_wait 与 sem_post，哪项符合课本描述？&lt;/p&gt;
&lt;p&gt;A. sem_wait 在信号量非零时减一并继续，若为 0 则阻塞；sem_post 增加信号量并可能唤醒等待线程
B. sem_wait 总是立即返回且不修改信号量
C. sem_post 会减少信号量
D. 信号量不能用于互斥&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明把二元信号量初始化为 1 可作为互斥锁使用；sem_wait 负责等待并减一，sem_post 负责增加信号量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.146&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;课本建议提高 Pthreads 求和性能的关键做法是什么？&lt;/p&gt;
&lt;p&gt;A. 让每个线程先用私有变量累计局部和，最后只进一次临界区合并
B. 让每次循环迭代都加锁更新全局和
C. 用 printf 改变线程时序
D. 增加忙等待循环次数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本指出无论如何保护临界区都会序列化该段代码，因此应减少进入临界区次数；程序4.5 使用每线程私有 my_sum 后再合并。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.141&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;关于条件变量的 signal 与 broadcast，哪项符合课本描述？&lt;/p&gt;
&lt;p&gt;A. signal 唤醒一个阻塞线程，broadcast 唤醒所有阻塞线程
B. signal 会销毁条件变量
C. broadcast 只适用于一个线程
D. 二者都不需要互斥锁&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本给出 pthread_cond_signal 和 pthread_cond_broadcast 的语义：前者解除一个阻塞线程，后者解除所有阻塞线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.151&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;pthread_mutex_init 的第二个参数通常传入什么以使用默认属性？&lt;/p&gt;
&lt;p&gt;A. NULL
B. MPI_COMM_WORLD
C. thread_count
D. cudaFree&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明不会使用 pthread_mutex_init 的第二个参数，传入 NULL 使用默认属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.142&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;下列哪项是 sem_wait 在信号量为 0 时的行为？&lt;/p&gt;
&lt;p&gt;A. 阻塞调用线程
B. 增加信号量
C. 销毁信号量
D. 创建新线程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明如果信号量为 0，执行 sem_wait 的线程将阻塞。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.146&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;下列哪个函数用于唤醒条件变量上所有被阻塞的线程？&lt;/p&gt;
&lt;p&gt;A. pthread_cond_broadcast
B. pthread_cond_wait
C. pthread_mutex_destroy
D. pthread_join&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 pthread_cond_broadcast 将解除所有被阻塞的线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.151&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;4-大题-1&lt;/h3&gt;
&lt;p&gt;分析在循环内部对全局变量加锁累加，为什么性能可能很差；给出更好的思路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 如果每次循环迭代都在 `global_sum += ...` 前后加锁，那么临界区执行次数等于迭代次数，锁和解锁会发生大量次。其他线程需要等待，调度器还可能频繁唤醒和切换线程，CPU 周期消耗在同步和调度上，而不是有效计算。更好的思路是每个线程先用私有局部变量累计自己的部分，最后只进入一次临界区把局部结果合并到全局变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 参考资料 3 直接分析了 Pthreads 求平方和的两个代码片段：循环内锁导致 N 次 lock/unlock，局部和再合并能减少同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 参考资料3 para 2；参考资料3 para 36；参考资料3 para 37；参考资料3 para 38&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-大题-2&lt;/h3&gt;
&lt;p&gt;比较忙等待、互斥量、信号量和条件变量的适用点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 忙等待通过反复检查条件来同步，能控制顺序但浪费 CPU，线程数超过核心数时性能可能很差。互斥量保证同一时刻只有一个线程进入临界区，适合保护共享变量更新，但进入顺序由系统调度决定。信号量适合一个线程等待另一个线程完成某些操作，也可以允许一定数量线程进入。条件变量用于等待某个条件成立，通常与互斥锁搭配，等待时释放锁，被唤醒后重新加锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案分别来自忙等待、互斥量、信号量、条件变量小节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.36；并行复习 p.37；并行复习 p.38；并行复习 p.39；并行复习 p.40；并行复习 p.41&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-大题-3&lt;/h3&gt;
&lt;p&gt;解释伪共享的形成过程，并给出至少两种避免思路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 伪共享发生在多个线程修改不同变量，但这些变量落在同一个 Cache Line 中。某个线程修改自己的变量时，会使其他核心中同一 Cache Line 的副本失效；另一个线程再修改邻近变量时又要重新获取该 Cache Line，导致缓存行反复失效/同步，性能急剧下降。避免思路包括给结构体成员填充或对齐，让不同线程频繁写的变量落在不同 Cache Line；或者让每个线程先在私有局部变量中累加，最后再少量写回共享内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料和参考资料 3 均解释了不同变量同处一个 cache line 导致性能下降，以及私有局部变量累加的改法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.44；并行复习 p.45；参考资料3 para 162；参考资料3 para 166；参考资料3 para 172&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本大题-1&lt;/h3&gt;
&lt;p&gt;课本为什么强调线程安全？以静态局部变量为例说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 线程安全指一个代码块由多个线程运行时仍能正常工作。普通 C 局部变量从线程自己的栈分配，通常是私有的；但静态局部变量跨调用保持存在，在多个线程调用同一函数时实际上会被共享，可能导致意外的共享状态和竞争。因此，串行程序中可用的函数放到多线程程序中不一定安全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在线程安全小节说明，静态局部变量会在调用函数的线程之间有效共享，这是串行函数在多线程程序中可能出错的重要原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.50；课本OCR-并行程序设计导论 原书第2版 p.70&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;比较忙等待、互斥锁和信号量在控制线程顺序或临界区访问上的差异。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 忙等待通过反复测试共享标志控制顺序，简单但浪费 CPU，且受优化和调度影响；互斥锁适合保护临界区，只保证互斥，不保证线程按编号顺序进入；信号量可表示资源计数或事件通知，二元信号量能像互斥锁一样使用，也能用于生产者-消费者式的消息到达通知。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本分别讨论忙等待、自旋、互斥锁随机进入临界区、以及用信号量让线程发送消息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.140；课本OCR-并行程序设计导论 原书第2版 p.143；课本OCR-并行程序设计导论 原书第2版 p.146；课本OCR-并行程序设计导论 原书第2版 p.147&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;为什么用互斥锁数组直接改造线程消息传递示例仍可能崩溃？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 如果线程0远领先于线程1，它可能在线程1设置给线程0的消息指针前，就锁住自己的互斥锁并执行打印；此时 messages[my_rank] 仍是 NULL，解除空指针引用会崩溃。问题不只是互斥访问，而是还需要表达“消息已经到达”的同步条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本用两个线程的事件顺序说明，单纯在写和读周围加互斥锁仍可能让线程读取 NULL 指针。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.145；课本OCR-并行程序设计导论 原书第2版 p.146&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;为什么条件变量等待通常应该放在 while 循环中？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 线程可能因为 signal/broadcast 之外的事件解除阻塞，即虚假唤醒；即使确实被唤醒，条件也可能已被其他线程改变。因此等待返回后应重新检查条件，不满足就再次等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本明确说明虚假唤醒存在，`pthread_cond_wait` 通常应放在 while 循环中；单线程唤醒时继续前也应确认条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.152&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第五章 用 OpenMP 进行共享内存编程&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;5-填空-1&lt;/h3&gt;
&lt;p&gt;OpenMP 的 pragma 总是以 `#pragma ________` 开始。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; omp&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 OpenMP 的 pragma 总是以 `# pragma omp` 开始。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.47&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-2&lt;/h3&gt;
&lt;p&gt;`#pragma omp parallel` 并行块末尾有一个隐式________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; barrier&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料提醒并行代码块末尾存在隐式 barrier。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.47&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-3&lt;/h3&gt;
&lt;p&gt;OpenMP 中，在 parallel 指令之前声明的变量默认是________变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 共享&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料总结并行指令之前声明的变量默认共享。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.49&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-4&lt;/h3&gt;
&lt;p&gt;OpenMP 的 reduction 子句会为每个线程创建归约变量的________副本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 私有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 OpenMP 自动为每个线程创建 variable 的私有副本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.50&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-5&lt;/h3&gt;
&lt;p&gt;`#pragma omp parallel for` 后面只能跟 ________ 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; for&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料明确该指令后面只能跟 for 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.50&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-6&lt;/h3&gt;
&lt;p&gt;如果第 i 次迭代依赖第 i-1 次迭代结果，就存在数据________，不能直接并行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 依赖&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料用 a[i]=a[i-1]+1 说明数据依赖性冲突。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.51&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-7&lt;/h3&gt;
&lt;p&gt;schedule(static, 1) 在资料中表现为按线程轮转分配每次________个迭代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料用 12 次迭代、3 个线程、chunk size 为 1 展示轮转调度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.54&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-填空-8&lt;/h3&gt;
&lt;p&gt;atomic 通常只保护紧跟其后的________条共享变量更新语句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 atomic 只保护接下来一条 C 语言赋值语句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.58；并行复习 p.60&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本填空-1&lt;/h3&gt;
&lt;p&gt;OpenMP 的 reduction 子句会为每个线程创建归约变量的________变量，运行时再按归约运算合并结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 私有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本解释 `reduction(+: global_result)` 时说明，OpenMP 为每个线程创建私有变量来存储线程结果，然后合并。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.185&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;OpenMP 的 `parallel for` 指令通过在线程之间分配循环的________来并行化 for 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 迭代&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 parallel for 指令分叉线程执行结构化块，并通过在线程之间分配 for 循环迭代来实现并行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.186&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;schedule 子句中，`runtime` 类型表示调度方式在运行时由________变量确定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 环境&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 runtime 调度是在运行时根据环境变量确定的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;课本 OpenMP 任务示例中，`taskwait` 作为任务的________，强迫当前任务等待子任务完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 栅栏&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 Fibonacci 示例说明 taskwait 指令作为任务的一个栅栏，等待子任务完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.214&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;OpenMP 程序通常通过 `#pragma omp ________` 创建一组线程执行结构化块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; parallel&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 OpenMP 入门示例使用 `#pragma omp parallel` 分叉线程执行 Hello。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.175&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;OpenMP 归约子句 `reduction(+:x)` 中的 `+` 表示归约________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 运算符&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本解释 reduction 时说明加号表示归约运算符是加法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.185&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;OpenMP `parallel for` 指令后面的结构化块必须是一个________循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; for&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 parallel for 后的结构化块必须是 for 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.186&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;OpenMP schedule 子句的一般形式是 `schedule(&amp;#x3C;type&gt;[, &amp;#x3C;________&gt;])`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; chunksize&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本给出 schedule 子句形式为 `schedule(&amp;#x3C;type&gt;[, &amp;#x3C;chunksize&gt;])`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;5-判断-1&lt;/h3&gt;
&lt;p&gt;OpenMP 比 Pthreads 更高层，常用于对已有串行程序进行并行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 pthread 更底层，需要程序员详细定义线程行为；OpenMP 抽象更高，甚至是为已有串行程序并行化设计。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.46&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-判断-2&lt;/h3&gt;
&lt;p&gt;C/C++ 编译器如果不认识某个 `#pragma`，通常会忽略它而不报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在 OpenMP 编译部分说明 pragma 用于给编译器建议或指示，不认识会忽略。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.47&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-判断-3&lt;/h3&gt;
&lt;p&gt;`#pragma omp parallel` 并行块末尾存在隐式 barrier。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明并行代码块末尾会有隐式 barrier，等线程组所有线程完成后主线程继续。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.47&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-判断-4&lt;/h3&gt;
&lt;p&gt;在 `#pragma omp parallel` 之前声明的变量默认在并行区中是私有变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料总结：parallel 指令之前声明的变量默认共享，并行代码块内部新声明的变量是私有的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.49&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-判断-5&lt;/h3&gt;
&lt;p&gt;OpenMP reduction 子句适合 `+`、`*`、`max` 等操作，资料中特别提醒减法 `-` 不适合作为普通归约操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 op 是 `+`、`*`、`max` 等，并注明减法不行且给出反例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.49&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-判断-6&lt;/h3&gt;
&lt;p&gt;`#pragma omp atomic` 通常只保护紧跟在后面的一条共享变量更新语句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 atomic 只能保护接下来一条 C 语言赋值语句形成的临界区，只能保护一个共享变量的单一更新操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.58；并行复习 p.60&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本判断-1&lt;/h3&gt;
&lt;p&gt;`#pragma omp parallel for` 后的结构化块必须是 for 循环，OpenMP 不会直接并行化 while 或 do-while 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 parallel for 注意事项中说明，OpenMP 只对 for 循环并行化，不直接并行化 while 或 do-while。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.187&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;只给 `global_result += Local_trap(...)` 加 critical 会使 Local_trap 调用本身也串行执行，虽正确但性能差。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本指出如果整个 `global_result += Local_trap(...)` 是临界区，那么 Local_trap 调用一次只能由一个线程执行，实际上让并行化失去意义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.184&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;OpenMP 中 `single` 常用于 task 示例，因为它可以防止每个线程都重复创建同一组任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明如果没有 single，每个函数调用将由所有线程执行；single 保证只有一个线程调用每个函数并创建任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.220&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;OpenMP 的 reduction 子句会为每个线程创建归约变量的私有副本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 OpenMP 为每个线程创建私有变量保存结果，再合并。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.185&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;dynamic 和 guided 调度都属于循环执行时再分配迭代的调度方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 schedule 子句说明 dynamic 或 guided 的迭代在循环执行时分配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;task 指令创建的子任务一定会在当前语句返回前完成，因此不需要 taskwait。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 Fibonacci 示例说明 task 最终会运行，但当前任务可能提前返回当前值，因此需要 taskwait 等待子任务完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.214&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;5-选择-1&lt;/h3&gt;
&lt;p&gt;创建指定线程数的 OpenMP 并行区应使用哪种写法？&lt;/p&gt;
&lt;p&gt;A. #pragma omp parallel num_threads(thread_count)
B. pthread_create(&amp;#x26;thread,...)
C. MPI_Comm_size(...)
D. kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid, block&gt;&gt;&gt;()&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料给出 `#pragma omp parallel num_threads(thread_count)`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.47&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-选择-2&lt;/h3&gt;
&lt;p&gt;在 OpenMP 梯形积分中，如果多个线程要合并到 `global_result`，下列哪种机制可以保护共享写？&lt;/p&gt;
&lt;p&gt;A. #pragma omp critical
B. MPI_ANY_TAG
C. cudaFree
D. pthread_join&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在梯形积分处说明 OpenMP 使用 `#pragma omp critical` 互斥访问合并结果的代码块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.48；并行复习 p.49&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-选择-3&lt;/h3&gt;
&lt;p&gt;`#pragma omp parallel for` 后面通常只能跟什么结构？&lt;/p&gt;
&lt;p&gt;A. for 循环
B. while 循环
C. switch 语句
D. 任意函数定义&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料明确指出该指令后面只能跟 for 循环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.50&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-选择-4&lt;/h3&gt;
&lt;p&gt;下面哪个循环不能直接并行化的主要原因是数据依赖？`for (int i=1;i&amp;#x3C;n;i++){ a[i]=a[i-1]+1; }`&lt;/p&gt;
&lt;p&gt;A. 第 i 次迭代依赖第 i-1 次迭代结果
B. 循环变量 i 是私有的
C. 数组 a 太大
D. for 循环不能使用 `&amp;#x3C;`&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料用这个例子说明第 i 次迭代使用 a[i-1]，存在数据依赖，不能直接并行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.51；参考资料4 para 14&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-选择-5&lt;/h3&gt;
&lt;p&gt;当循环迭代耗时差异很大时，哪类调度通常更适合负载均衡？&lt;/p&gt;
&lt;p&gt;A. dynamic 或 guided
B. 只用默认 static 大块划分
C. 完全不调度
D. 只使用 atomic&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 dynamic/guided 在循环执行时动态分配，更适合负载不均；static 开销小但可能不均衡。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.54；并行复习 p.55；并行复习 p.56&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-选择-6&lt;/h3&gt;
&lt;p&gt;默认缺省调度在资料中被概括为哪种方式？&lt;/p&gt;
&lt;p&gt;A. schedule(static, n/thread_count)
B. schedule(dynamic, 1)
C. schedule(guided, 1)
D. runtime 随机&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明默认缺省调度实际上就是 `schedule(static, n/thread_count)`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.55&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本选择-1&lt;/h3&gt;
&lt;p&gt;OpenMP schedule 子句中，dynamic 或 guided 的共同特点是什么？&lt;/p&gt;
&lt;p&gt;A. 迭代在循环执行时分配，线程完成当前迭代集后可向运行时系统请求更多迭代
B. 所有迭代必须在编译期完全分配
C. 只能用于 while 循环
D. 只用于设置线程数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 schedule 子句说明，dynamic 和 guided 都是在循环执行时分配迭代，线程完成当前迭代集后可请求更多迭代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;OpenMP schedule(static) 与 dynamic/guided 的关键区别是什么？&lt;/p&gt;
&lt;p&gt;A. static 可在循环执行前分配迭代；dynamic/guided 在循环执行时分配迭代
B. static 只能用于 while 循环
C. dynamic/guided 不会产生调度开销
D. static 必须由环境变量决定&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 schedule 子句说明 static 在循环执行前分配，dynamic/guided 在循环执行时分配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;课本练习中用 Monte Carlo 方法估计 pi，OpenMP 并行版本最自然应对哪个计数使用 reduction？&lt;/p&gt;
&lt;p&gt;A. 落在圆内的点数 number_in_circle
B. 线程编号
C. 随机数种子数组地址
D. printf 次数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本编程作业要求使用归约子句来找到投掷在圆圈内的飞镖总数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.225&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;OpenMP `critical` 指令的作用是？&lt;/p&gt;
&lt;p&gt;A. 使线程对后续结构化代码块互斥访问
B. 创建 MPI 进程
C. 释放 CUDA 统一内存
D. 改变 Cache 行大小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 critical 指令使线程对下面的结构化代码块互斥访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.181&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;OpenMP `static` 调度的基本含义是？&lt;/p&gt;
&lt;p&gt;A. 迭代可在循环执行前分配给线程
B. 线程完成当前块后动态请求更多块
C. 调度由环境变量决定
D. 由编译器完全忽略循环&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 schedule 子句说明 static 中迭代可以在循环执行前分配给线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.197&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;OpenMP `taskwait` 最接近哪种同步语义？&lt;/p&gt;
&lt;p&gt;A. 当前任务等待其子任务完成
B. 所有 MPI 进程退出
C. 只同步一个 CUDA warp
D. 销毁线程池&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 taskwait 指令作为任务的栅栏，强迫任务等待子任务完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.214&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;5-大题-1&lt;/h3&gt;
&lt;p&gt;为什么资料中的第一个 OpenMP pi 值估计并行版本有问题？如何修改？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 问题在于 `factor` 在并行区外定义，默认共享，并且每次迭代的正负号依赖前一次迭代，存在数据依赖和共享变量竞争。修改思路是把 `factor` 设为 private，并在每次迭代中由 k 的奇偶性独立计算 `factor=1.0` 或 `-1.0`，同时对 `sum` 使用 `reduction(+:sum)`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在 pi 值估计部分给出了错误版本、原因和更新后的 private(factor)+reduction 写法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.52；并行复习 p.53&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-大题-2&lt;/h3&gt;
&lt;p&gt;比较 static、dynamic、guided 调度，并说明负载不均衡循环应如何选择。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; static 在循环开始前分配固定块，调度开销最小，但如果迭代耗时差异大，会导致某些线程过重。dynamic 把迭代分成小块，线程完成后动态领取新块，负载均衡好但有同步调度开销。guided 也是动态调度，但开始块大、后期块逐渐减小，适合大规模迭代。若迭代工作量随 i 增大而显著变化，默认 static 大块划分可能让后段线程过重，应考虑 dynamic 或 guided。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案来自循环调度、schedule 子句和调度方式区别部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.53；并行复习 p.54；并行复习 p.55；并行复习 p.56；参考资料4 para 28；参考资料4 para 29&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-大题-3&lt;/h3&gt;
&lt;p&gt;比较 OpenMP 中 critical、atomic 和锁，并说明嵌套 critical 为什么可能死锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; atomic 适合保护一条简单的共享变量更新语句，只保护紧跟其后的单一更新。critical 可以保护更一般的代码段；未命名 critical 默认使用同一全局锁，命名 critical 可区分不同临界区。锁通常更适合互斥数据结构。若在一个未命名 critical 内再次进入未命名 critical，线程已持有外层锁又尝试获取同一把锁，会自己等待自己而死锁；即使命名不同，也可能出现两个线程分别持有不同锁并等待对方锁的交叉死锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 复习资料在 atomic、critical、锁和嵌套 critical 死锁处均给出说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.58；并行复习 p.59；并行复习 p.60；参考资料4 para 86；参考资料4 para 92&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本大题-1&lt;/h3&gt;
&lt;p&gt;用课本 OpenMP 梯形法例子说明 `global_result += my_result` 为什么是临界区，并给出两种修正方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 多个线程都会读写共享变量 `global_result`，且更新顺序会影响结果；课本时间表展示了两个线程分别计算 1 和 2 时可能最终只保存其中一个更新。因此该语句是临界区。修正方法一是用 `#pragma omp critical` 让每次只有一个线程更新；方法二是使用 `reduction(+: global_result)`，让每个线程写私有副本，最后由 OpenMP 合并。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本先用时间表展示 `global_result += my_result` 的竞争条件，随后给出 critical 和 reduction 两种 OpenMP 改法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.180；课本OCR-并行程序设计导论 原书第2版 p.181；课本OCR-并行程序设计导论 原书第2版 p.185&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本大题-2&lt;/h3&gt;
&lt;p&gt;课本中 OpenMP task/taskwait 的作用是什么？为什么 task 往往需要配合 single 使用？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; task 指令把函数调用或递归子问题创建为可由线程执行的任务；taskwait 像该任务的栅栏，强迫当前任务等待子任务完成后再继续使用结果。课本还说明，如果在 parallel 区中直接写多个函数调用，每个线程都会执行这些调用；single 可保证只有一个线程负责创建任务，再由线程组执行任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本 Fibonacci 和函数调用示例分别解释了 taskwait 对子任务的等待，以及 single 防止所有线程重复创建同一批任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.214；课本OCR-并行程序设计导论 原书第2版 p.220&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;为什么 OpenMP 的 `parallel for` 不能无条件用于所有 for 循环？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; OpenMP 只能直接并行化迭代次数可确定的 for 循环，不能直接并行化 while 或 do-while；更重要的是，循环迭代之间不能存在会改变结果的数据依赖。例如后一轮依赖前一轮结果的循环，直接并行化会破坏语义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本在 parallel for 注意事项中说明了 for 循环形式和迭代次数限制，前文也用数据依赖例子说明不能直接并行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.187&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;根据课本练习，说明如何用 OpenMP 改写计数排序，以及主要性能风险。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 计数排序对每个元素 a[i] 计算列表中小于它的元素个数，再写入 temp[count]。外层 i 循环的各次计算相对独立，可用 parallel for 分配给线程；每个线程使用私有 count 和循环变量。风险在于算法本身是 O(n^2)，并且对相等元素的稳定性判断、temp 写入位置和内存访问模式都要正确处理，实际性能还要与串行 Count_sort 和 qsort 比较。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本编程作业给出 Count_sort 串行代码，并要求实现并行版本、比较其与串行 Count_sort 和 qsort 的性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.225；课本OCR-并行程序设计导论 原书第2版 p.226&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;课本练习中，矩阵-向量乘法为什么可能出现伪共享？两种缓解方式是什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 如果多个线程写向量 y 中相邻元素，而这些元素落在同一缓存行上，不同处理器之间会不断使对方缓存行失效，即使线程写的是不同 y 元素，也会发生伪共享。缓解方式一是在 y 中填充，使不同线程同时写的元素不共享缓存行；方式二是让每个线程在私有存储中计算自己的 y 片段，完成后再复制到共享 y。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本练习要求分析 8000x8000 矩阵-向量乘法中的伪共享，并提出填充 y 或使用线程私有 y 片段两种修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.223；课本OCR-并行程序设计导论 原书第2版 p.224&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第六章 CUDA 异构编程&lt;/h2&gt;
&lt;h3&gt;填空题&lt;/h3&gt;
&lt;h3&gt;6-填空-1&lt;/h3&gt;
&lt;p&gt;CUDA 中 CPU 端称为 host，GPU 端称为________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; device&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料写明 CPU(host) 负责管理调度，GPU(device) 负责大规模并行计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.64&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-2&lt;/h3&gt;
&lt;p&gt;CUDA C 程序由 Host Code 和 ________ Code 两部分组成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; Device&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 Host Code 和 Device Code。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.64；并行复习 p.65&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-3&lt;/h3&gt;
&lt;p&gt;申请 GPU 显存使用 ________，释放 GPU 显存使用 cudaFree。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; cudaMalloc&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 cudaMalloc 和 cudaFree。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.65&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-4&lt;/h3&gt;
&lt;p&gt;CUDA kernel 调用语法是 `kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;gridDim, ________&gt;&gt;&gt;(args);`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; blockDim&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料给出 `kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;gridDim, blockDim&gt;&gt;&gt;(args);`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-5&lt;/h3&gt;
&lt;p&gt;一维全局线程编号常写作 `blockDim.x * blockIdx.x + ________`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; threadIdx.x&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料向量加法 kernel 示例使用该表达式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-6&lt;/h3&gt;
&lt;p&gt;CUDA 线程组织三层是 Thread、Block 和 ________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; Grid&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 Thread、Block、Grid 三层。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-7&lt;/h3&gt;
&lt;p&gt;一个 warp 通常包含 ________ 个线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 32&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明一个 warp 是 32 个线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67；并行复习 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-8&lt;/h3&gt;
&lt;p&gt;`__syncthreads()` 是________级线程同步屏障。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; block&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 `__syncthreads()` 适用于 block 级线程同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-9&lt;/h3&gt;
&lt;p&gt;Unified Memory 让 CPU 和 GPU 使用同一________访问内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 指针&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Unified Memory 让 CPU 和 GPU 使用同一指针访问内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-填空-10&lt;/h3&gt;
&lt;p&gt;共享内存 bank conflict 会导致访问被________化，从而降低性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 串行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 bank conflict 时 CUDA 会串行化访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本填空-1&lt;/h3&gt;
&lt;p&gt;CUDA 程序中，threadIdx.x 给出线程在其线程块中的相对索引；blockIdx.x 给出________在网格中的索引。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 线程块&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 threadIdx.x 是线程在块中的索引，blockIdx.x 是块在网格中的索引。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.235&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-填空-1&lt;/h3&gt;
&lt;p&gt;从主机角度看，CUDA 核函数调用通常是________的；若主机要使用设备结果，可调用 cudaDeviceSynchronize。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 异步&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结说明核函数在设备上启动后，设备和主机可以同时执行；主机可通过 cudaDeviceSynchronize 阻塞等待设备完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-填空-2&lt;/h3&gt;
&lt;p&gt;CUDA 统一内存最常用的分配函数是________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; cudaMallocManaged&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结给出 `cudaMallocManaged(void** ptr, size_t size)` 用于分配统一内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.275&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-填空-3&lt;/h3&gt;
&lt;p&gt;CUDA 中线程块集合称为________。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 网格&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结说明由核函数启动的线程块集合称为网格。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-填空-1&lt;/h3&gt;
&lt;p&gt;CUDA 线程可通过 threadIdx、blockIdx、blockDim 和 ________ 获取网格信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; gridDim&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第6章小结列出线程可检查的四个结构：threadIdx、blockIdx、blockDim、gridDim。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-填空-2&lt;/h3&gt;
&lt;p&gt;CUDA 中线程束中的线程以________方式执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; SIMD&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结说明线程束是线程块中线程的子集，线程束中的线程以 SIMD 方式执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.275&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-填空-3&lt;/h3&gt;
&lt;p&gt;CUDA 原子操作 `atomicAdd(xp, y)` 会把 y 加到 ________ 指向的内存位置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; xp&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本给出 atomicAdd 语义：将 y 添加到 xp 指向的内存位置，并返回原始值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.276&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-填空-4&lt;/h3&gt;
&lt;p&gt;共享内存被划分为多个________，若同一 warp 多线程访问同一库不同位置会串行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 库&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明共享内存被划分为库，访问不同库可并行，访问同一库不同位置必须串行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.262；课本OCR-并行程序设计导论 原书第2版 p.277&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;判断题&lt;/h3&gt;
&lt;h3&gt;6-判断-1&lt;/h3&gt;
&lt;p&gt;CUDA 是异构编程模型，CPU 作为 host 管理与调度，GPU 作为 device 负责大规模并行计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料明确给出 host/device 分工和通过 PCIe 通信的描述。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.64；并行复习 p.65&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-判断-2&lt;/h3&gt;
&lt;p&gt;Host Code 运行在 GPU 上，Device Code 运行在 CPU 上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Host Code 运行在 CPU 上，Device Code 运行在 GPU 上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.64；并行复习 p.65&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-判断-3&lt;/h3&gt;
&lt;p&gt;CUDA kernel 必须返回 void，且用 `__global__` 等关键字标记。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料把 kernel 定义为用特殊 CUDA 关键字定义的 C 函数，并列出必须返回 void。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.66&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-判断-4&lt;/h3&gt;
&lt;p&gt;CUDA kernel 调用默认是异步的，调用后控制权会立刻返回 CPU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 CUDA 核函数与 OpenMP 隐式路障不同，是异步执行，调用完成后控制权立刻返回 CPU。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-判断-5&lt;/h3&gt;
&lt;p&gt;CUDA 的线程组织层次是 Grid、Block、Thread；一个 kernel 调用会启动一个 grid。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料列出 Thread、Block、Grid 三层，并说明每次 kernel 调用相当于启动一个 grid。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-判断-6&lt;/h3&gt;
&lt;p&gt;`__syncthreads()` 会同步整个 grid 中的所有线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 `__syncthreads()` 在一个 block 内所有线程之间建立同步屏障，适用于 block 级线程同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本判断-1&lt;/h3&gt;
&lt;p&gt;CUDA 核函数执行是异步的，主机线程在启动核函数后通常会继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本指出普通 C 函数与 CUDA 核函数的重要区别之一是核函数执行异步，主机不会像普通函数调用那样一直等待函数体执行完才返回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.233&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-判断-1&lt;/h3&gt;
&lt;p&gt;不能把主机栈上变量的地址直接传给 CUDA 核函数来模拟引用传递，因为该地址在设备上无效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本统一内存小结明确提醒，指针若指向主机栈中内存位置，不能传给核函数；实现引用传递效果可用 cudaMallocManaged。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.275&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-判断-2&lt;/h3&gt;
&lt;p&gt;如果多个线程访问同一共享内存库中的不同位置，CUDA 可以总是并行完成这些访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本共享内存库说明，若多个线程访问单个库中的不同内存位置，访问必须串行化；只有访问不同库，或读取同一库同一位置广播，才能避免该类串行化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.262&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-判断-1&lt;/h3&gt;
&lt;p&gt;默认情况下，设备上一次只运行一个核函数；若主机先启动 Kernel_a 再启动 Kernel_b，二者按启动顺序执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本第6章小结说明默认情况下设备上一次只运行一个核函数，主机按顺序启动的核函数按顺序执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-判断-2&lt;/h3&gt;
&lt;p&gt;线程束洗牌允许线程读取同一线程束中另一个线程的寄存器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 正确&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明线程束洗牌允许线程读取属于同一线程束中另一个线程的寄存器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.276&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-判断-3&lt;/h3&gt;
&lt;p&gt;`__syncthreads()` 可以同步普通网格中的所有线程块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本明确说明 `__syncthreads()` 只同步一个线程块中的线程，不能同步普通网格中的线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.260&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;选择题&lt;/h3&gt;
&lt;h3&gt;6-选择-1&lt;/h3&gt;
&lt;p&gt;Host Code 的主要职责不包括哪一项？&lt;/p&gt;
&lt;p&gt;A. 控制程序流程
B. 数据分配和传输
C. 调用 GPU
D. 作为每个 GPU 线程执行的 kernel&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; D&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Host Code 运行在 CPU 上，负责控制流程、数据分配传输和调用 GPU；kernel 属于 Device Code。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.65&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-选择-2&lt;/h3&gt;
&lt;p&gt;一维 CUDA kernel 中常见的全局线程编号计算式是？&lt;/p&gt;
&lt;p&gt;A. blockDim.x * blockIdx.x + threadIdx.x
B. MPI_Comm_rank + MPI_Comm_size
C. omp_get_thread_num() + pthread_join()
D. gridDim.x / blockDim.x&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在向量加法 kernel 示例中使用 `int tid = blockDim.x * blockIdx.x + threadIdx.x;`。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-选择-3&lt;/h3&gt;
&lt;p&gt;资料中一个 warp 通常包含多少个线程？&lt;/p&gt;
&lt;p&gt;A. 8
B. 16
C. 32
D. 1024&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; C&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明一个 warp 是 32 个线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.67；并行复习 p.68&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-选择-4&lt;/h3&gt;
&lt;p&gt;下列哪种分支更容易在同一个 warp 内造成分支分化？&lt;/p&gt;
&lt;p&gt;A. if (threadIdx.x % 2) ... else ...
B. if (blockIdx.x % 2) ... else ...
C. 所有线程执行完全相同路径
D. 没有 if/else 的循环&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 `threadIdx.x` 是同一个 warp 内不同线程编号，不同线程走不同分支会导致分支分化；`blockIdx.x` 示例不会。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.69&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-选择-5&lt;/h3&gt;
&lt;p&gt;CUDA 存储层次中，哪一类内存由一个线程块内线程共享，位于片上？&lt;/p&gt;
&lt;p&gt;A. 共享内存
B. 本地内存
C. 全局内存
D. 常量内存&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料表格和后文说明共享内存是 GPU 片上内存，由一个线程块内所有线程共享。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.70；并行复习 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-选择-6&lt;/h3&gt;
&lt;p&gt;统一内存的主要优点和代价是？&lt;/p&gt;
&lt;p&gt;A. 简化编程，但底层仍需迁移数据，性能可能低于显式内存管理
B. 完全消除 CPU-GPU 通信
C. 只能在一个线程块内访问
D. 要求程序员手写每次 PCIe 传输&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料说明 Unified Memory 让 CPU/GPU 使用同一指针并自动迁移数据，简化编程但可能牺牲性能，高性能计算中手动管理仍更常用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.71&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本选择-1&lt;/h3&gt;
&lt;p&gt;关于 CUDA 统一内存，哪项符合课本描述？&lt;/p&gt;
&lt;p&gt;A. cudaMallocManaged 分配的指针可在主机和设备上使用，但统一内存核函数可能比显式区分主机/设备内存更慢
B. 统一内存要求程序员手动写每次 cudaMemcpy
C. 统一内存只能被 CPU 使用
D. 统一内存保证所有 GPU 程序最快&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 cudaMallocManaged 的指针可在主机和设备上使用，简化编程；但统一内存核函数可能比显式处理设备内存和主机内存更慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.240&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-选择-1&lt;/h3&gt;
&lt;p&gt;课本用 atomicAdd 优化讨论说明，减少调用 atomicAdd 的线程数量主要为了什么？&lt;/p&gt;
&lt;p&gt;A. 降低全局求和等操作的原子更新开销
B. 让每个线程打印更多文本
C. 取消所有共享内存
D. 使核函数变成同步调用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本梯形法性能讨论指出，在更快 GPU 上，通过编程减少调用 atomicAdd 的线程数量是值得的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.264；课本OCR-并行程序设计导论 原书第2版 p.276&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-选择-2&lt;/h3&gt;
&lt;p&gt;CUDA 双调排序单线程块版本中，为什么每个阶段后需要 `__syncthreads()`？&lt;/p&gt;
&lt;p&gt;A. 不同线程的 compare-swap 存在阶段依赖，若阶段顺序交错会得到错误列表
B. 因为 printf 必须排序
C. 因为所有内存都是主机栈内存
D. 因为 blockIdx.x 不能被读取&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本用 4 元素蝶式交换事件序列说明，如果线程0下一阶段比较先于线程1上一阶段比较执行，最终列表可能不有序，因此需要同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.270；课本OCR-并行程序设计导论 原书第2版 p.271&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-选择-1&lt;/h3&gt;
&lt;p&gt;若调用 `My_kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;4,256&gt;&gt;&gt;(x,y,n)`，blockDim.x 等于多少？&lt;/p&gt;
&lt;p&gt;A. 256
B. 4
C. 1024
D. 0&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本小结用该调用说明 gridDim.x=4，blockDim.x=256。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-选择-2&lt;/h3&gt;
&lt;p&gt;CUDA 中用于等待设备执行完成的主机函数是？&lt;/p&gt;
&lt;p&gt;A. cudaDeviceSynchronize
B. pthread_join
C. MPI_Finalize
D. omp_get_wtime&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明主机若想使用设备结果，可以调用 cudaDeviceSynchronize 并阻塞到设备完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.274&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本概念-选择-3&lt;/h3&gt;
&lt;p&gt;下列哪项是共享内存库冲突的典型情形？&lt;/p&gt;
&lt;p&gt;A. 同一 warp 多个线程访问同一库中的不同地址
B. 多个线程读取同一库同一位置并广播
C. 每个线程访问不同库
D. 主机调用 cudaFree&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明多个线程访问同一库中不同内存位置时必须串行化；读取同一位置可广播。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.262&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;大题&lt;/h3&gt;
&lt;h3&gt;6-大题-1&lt;/h3&gt;
&lt;p&gt;说明一个简单 CUDA 向量加法程序的 host-device 基本流程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; Host 端先准备输入数据并用 cudaMalloc 申请 GPU 显存，然后用 cudaMemcpyHostToDevice 将数据从 CPU 拷贝到 GPU。随后调用 `__global__` kernel，kernel 中每个线程通过 `blockDim.x * blockIdx.x + threadIdx.x` 计算自己的全局编号并处理一个或一段数据。kernel 调用后可用 cudaDeviceSynchronize 等待 GPU 完成，再用 cudaMemcpyDeviceToHost 把结果拷回 CPU，最后 cudaFree 释放 GPU 显存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 答案综合资料中的 cudaMalloc/cudaMemcpy/cudaFree、kernel、线程编号和同步说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.65；并行复习 p.66；并行复习 p.67；课程要点-CUDA体系结构 para 4；课程要点-CUDA体系结构 para 5；课程要点-CUDA体系结构 para 6&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-大题-2&lt;/h3&gt;
&lt;p&gt;解释 warp 分支分化和 divergent loop 为什么会降低性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一个 warp 中的线程按 SIMD 模式执行，同一时刻只能执行同一条指令。如果同一个 warp 内线程因为 `threadIdx.x` 等条件进入不同分支，硬件会先执行一条路径并屏蔽另一部分线程，再执行另一条路径并屏蔽前一部分线程，等价于串行化不同路径。divergent loop 也是类似：循环次数不同的线程在每一轮中只有尚未结束的线程继续执行，其他线程被屏蔽，导致有效并行度下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料在分支分化和 divergent loop 小节直接解释了两次路径执行和循环轮次屏蔽。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.68；并行复习 p.69；课程要点-CUDA体系结构 para 11；课程要点-CUDA体系结构 para 12&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-大题-3&lt;/h3&gt;
&lt;p&gt;概述 CUDA 内存模型，并解释共享内存 bank conflict 及一种避免方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; CUDA 存储层次包括寄存器、本地内存、共享内存、全局内存和常量内存。寄存器属于线程且在片上；本地内存虽然名字叫本地，但大数组等可能存放在片外；共享内存位于片上，由一个 block 内线程共享；全局内存和常量内存可被所有线程访问。共享内存被划分为多个 memory bank，如果同一 block 内多个线程在同一时钟周期访问同一 bank 的不同地址，就会发生 bank conflict，访问被串行化、性能下降。避免方法包括让不同线程访问不同 bank，或通过 padding 改变地址映射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 资料的 CUDA 内存模型表格和共享内存 bank conflict 小节给出这些点；体系结构 DOCX 也标注了 GPU memory structures 对应课本页码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 并行复习 p.70；并行复习 p.71；并行复习 p.72；课程要点-计算机体系结构-量化研究方法-第六版-第四章 para 14；课程要点-CUDA体系结构 para 15；课程要点-CUDA体系结构 para 16&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本大题-1&lt;/h3&gt;
&lt;p&gt;说明 CUDA 中 warp 分歧和 `__syncthreads()` 的两个重要限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 一个 warp 当前通常包含 32 个线程，并以单指令流多数据流方式运行；同一 warp 内线程走不同 if-else 分支时会发生分歧，分支路径需要分别执行，影响效率。`__syncthreads()` 的限制有两点：第一，块中所有线程都必须执行到该调用，否则部分线程会永久等待；第二，它只同步同一个线程块内的线程，不能同步普通网格中的不同线程块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本分别在 warp 小节和 `__syncthreads()` 警告中说明了分歧、所有线程都必须调用、以及只同步一个块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.253；课本OCR-并行程序设计导论 原书第2版 p.259；课本OCR-并行程序设计导论 原书第2版 p.260&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-大题-1&lt;/h3&gt;
&lt;p&gt;解释 CUDA 统一内存的便利性和限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 统一内存通过 cudaMallocManaged 分配，返回的指针在主机和设备上都有效，系统会在主机和设备之间自动复制更新的数据，简化了显式 cudaMemcpy 管理。但统一内存不能让设备访问主机栈变量地址；在某些设备上主机和设备不能同时访问同一统一内存；性能也可能比显式区分主机/设备内存更慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本统一内存段落说明了 cudaMallocManaged 的语义、主机栈指针限制和统一内存可能较慢的注意事项。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.240；课本OCR-并行程序设计导论 原书第2版 p.275&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-大题-2&lt;/h3&gt;
&lt;p&gt;比较 CUDA 线程束洗牌和共享内存在树形求和中的作用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 两者都用于快速实现树结构全局求和，减少昂贵的全局原子更新。线程束洗牌允许同一 warp 内线程读取其他线程寄存器中的值，可用 `__shfl_down_sync` 做树形合并；共享内存方案把中间结果存入块内共享内存，再用块内同步保证各 warp 结果可见。共享内存方案还要注意 bank conflict。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本说明 CUDA 提供线程束洗牌和共享内存两种快速树结构操作替代方法，并给出共享内存同步和库冲突分析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.261；课本OCR-并行程序设计导论 原书第2版 p.262；课本OCR-并行程序设计导论 原书第2版 p.276&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6-课本补充-大题-3&lt;/h3&gt;
&lt;p&gt;为什么并行双调排序在小输入上不一定比 qsort 快，但在大输入上可能明显更快？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 小输入时 GPU 启动、同步和并行算法额外步骤的开销占比高，课本 2048 个整数实验中 qsort 比单块并行双调排序更快或相近；大输入时大量线程块和线程能充分利用 GPU 并行度，课本 2,097,152 个整数实验中并行双调排序比串行 qsort 快 40 多倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt; 课本表 6.18 和 6.19 比较了小列表和大列表上 qsort、串行双调排序和并行双调排序的运行时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;来源：&lt;/strong&gt; 课本OCR-并行程序设计导论 原书第2版 p.273&lt;/p&gt;
&lt;hr&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>并行程序设计复习笔记</title><link>https://iaohr9.github.io/blog/%E5%B9%B6%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E5%A4%8D%E4%B9%A0%E7%AC%94%E8%AE%B0</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E5%B9%B6%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E5%A4%8D%E4%B9%A0%E7%AC%94%E8%AE%B0</guid><description>并行程序设计导论复习笔记，整理并行系统分类、MPI、Pthreads、OpenMP、缓存与性能等重点。</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;第 1 章 为什么需要并行计算&lt;/h1&gt;
&lt;h2&gt;1.2 为什么需要建立并行系统&lt;/h2&gt;
&lt;h3&gt;单核性能提升受限&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;过去单处理器性能提升主要依赖晶体管密度增加。&lt;/li&gt;
&lt;li&gt;晶体管变小可以提高速度，但会带来更高功耗和散热压力。&lt;/li&gt;
&lt;li&gt;风冷散热逐渐接近极限，继续单纯提高单核速度变得困难。&lt;/li&gt;
&lt;li&gt;因此，提高计算能力的重要方向转向&lt;strong&gt;并行化&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多核处理器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多核处理器&lt;/strong&gt;：在一个芯片上集成多个相对简单、完整的处理器核心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单核系统&lt;/strong&gt;：传统只有一个 CPU 核心的处理器系统。&lt;/li&gt;
&lt;li&gt;普通串行程序通常不能自动利用多核，程序员需要学习并行编程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;并行化的两种基本方式&lt;/h3&gt;
&lt;p&gt;| 类型 | 含义 | 例子 |
|---|---|---|
| 任务并行 | 把不同任务分配给不同核心 | 一个核心读文件，一个核心处理数据，一个核心写结果 |
| 数据并行 | 把数据分给不同核心，各核心执行相同或类似操作 | 把数组分段，每个核心求一段和 |&lt;/p&gt;
&lt;h3&gt;并行程序的协调问题&lt;/h3&gt;
&lt;p&gt;| 问题 | 含义 |
|---|---|
| 通信 | 核心之间交换部分结果或状态 |
| 负载平衡 | 尽量让各核心工作量接近，避免部分核心空闲 |
| 同步 | 某些阶段需要等待其他核心完成后才能继续 |&lt;/p&gt;
&lt;h2&gt;1.3 并行系统的分类&lt;/h2&gt;
&lt;h3&gt;按内存访问方式分类&lt;/h3&gt;
&lt;h4&gt;共享内存系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;多个核心共享同一块内存。&lt;/li&gt;
&lt;li&gt;原则上，每个核心都可以读写每个内存位置。&lt;/li&gt;
&lt;li&gt;核心可通过共享变量进行协调。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;核心0   ┐
核心1   ├── 共享内存
...    │
核心p-1 ┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分布式内存系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;每个核心或节点有自己的私有内存。&lt;/li&gt;
&lt;li&gt;核心之间通常不能直接访问彼此内存。&lt;/li&gt;
&lt;li&gt;需要通过网络发送消息进行通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;核心0 -- 内存0     ┐
核心1 -- 内存1     ├── 网络
...               │
核心p-1 -- 内存p-1 ┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;按指令流和数据流分类&lt;/h3&gt;
&lt;p&gt;这类分类通常称为 &lt;strong&gt;Flynn 分类法&lt;/strong&gt;。它从两个维度描述并行系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指令流（instruction stream）&lt;/strong&gt;：处理器执行的指令序列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据流（data stream）&lt;/strong&gt;：指令作用的数据序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据“指令流数量”和“数据流数量”，Flynn 分类法一共分为四类：&lt;/p&gt;
&lt;p&gt;| 类型 | 全称 | 指令流 | 数据流 | 含义 | 常见例子 |
|---|---|---|---|---|---|
| SISD | Single Instruction Single Data | 单 | 单 | 一个处理器顺序执行一条指令流，处理一个数据流 | 传统单核串行计算机 |
| SIMD | Single Instruction Multiple Data | 单 | 多 | 多个处理单元执行同一条指令，但作用在不同数据上 | 向量处理器、GPU 的部分模型 |
| MISD | Multiple Instruction Single Data | 多 | 单 | 多条指令流作用在同一个数据流上；现实中很少作为通用模型 | 少数容错、冗余计算模型 |
| MIMD | Multiple Instruction Multiple Data | 多 | 多 | 多个处理器或核心可以独立执行不同指令，并处理不同数据 | 多核 CPU、集群 |&lt;/p&gt;
&lt;p&gt;本书主要关注 &lt;strong&gt;SIMD&lt;/strong&gt; 和 &lt;strong&gt;MIMD&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SIMD&lt;/strong&gt; 更像“同一条指令同时作用在多份数据上”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIMD&lt;/strong&gt; 更像“多个独立处理器各自执行自己的指令流”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MISD&lt;/strong&gt; 理论上存在，但在通用并行程序设计中很少作为主要模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 1 章小结&lt;/h2&gt;
&lt;p&gt;| API | 主要用途 |
|---|---|
| MPI | 分布式内存 MIMD 系统 |
| Pthreads | 共享内存 MIMD 系统 |
| OpenMP | 共享内存系统，重点用于 MIMD |
| CUDA | NVIDIA GPU 编程 |&lt;/p&gt;
&lt;h3&gt;并发、并行、分布式&lt;/h3&gt;
&lt;p&gt;| 概念 | 重点 |
|---|---|
| 并发 | 多个任务在同一时间段内推进，不一定真正同时执行 |
| 并行 | 多个任务实际同时执行，任务通常耦合更紧密 |
| 分布式 | 多个任务分布在不同机器或节点上，通过网络协作 |&lt;/p&gt;
&lt;h1&gt;第 2 章 并行硬件与并行软件&lt;/h1&gt;
&lt;h2&gt;2.1 背景知识&lt;/h2&gt;
&lt;h3&gt;2.1.1 冯·诺依曼体系结构&lt;/h3&gt;
&lt;h3&gt;基本组成&lt;/h3&gt;
&lt;p&gt;经典冯·诺依曼体系结构包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主存&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;：也叫处理器或核心&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互连网络&lt;/strong&gt;：连接 CPU 和主存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;主存由一组存储位置组成。&lt;/li&gt;
&lt;li&gt;每个位置有：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;地址&lt;/strong&gt;：用于访问该位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：保存指令或数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CPU&lt;/h3&gt;
&lt;p&gt;| 部件 | 作用 |
|---|---|
| 控制单元 | 决定执行哪条指令 |
| 数据通路 | 执行实际指令 |
| 寄存器 | 保存数据和程序状态 |
| 程序计数器 | 保存下一条将执行指令的地址 |&lt;/p&gt;
&lt;h3&gt;冯·诺依曼瓶颈&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU 和主存分离。&lt;/li&gt;
&lt;li&gt;指令和数据必须通过互连网络传输。&lt;/li&gt;
&lt;li&gt;CPU 执行速度可能远快于内存访问速度，导致 CPU 等待内存。&lt;/li&gt;
&lt;li&gt;互连网络限制了程序访问指令和数据的速率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU = 工厂&lt;/li&gt;
&lt;li&gt;主存 = 仓库&lt;/li&gt;
&lt;li&gt;互连 = 道路&lt;/li&gt;
&lt;li&gt;如果运输速度跟不上生产速度，道路就成为瓶颈。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1.2 进程、多任务和线程&lt;/h3&gt;
&lt;h3&gt;进程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进程&lt;/strong&gt;：正在执行的程序实例。&lt;/p&gt;
&lt;p&gt;进程通常包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可执行机器语言程序&lt;/li&gt;
&lt;li&gt;内存块：代码、调用栈、堆等&lt;/li&gt;
&lt;li&gt;资源描述符：如文件描述符&lt;/li&gt;
&lt;li&gt;安全信息：可访问哪些资源&lt;/li&gt;
&lt;li&gt;状态信息：是否可运行、是否等待资源、寄存器内容等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多任务&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多任务操作系统&lt;/strong&gt;支持多个程序在同一时间段内运行。&lt;/li&gt;
&lt;li&gt;单核系统也可通过&lt;strong&gt;时间片&lt;/strong&gt;轮流执行不同进程，让它们看起来同时运行。&lt;/li&gt;
&lt;li&gt;如果进程等待资源，会进入&lt;strong&gt;阻塞态&lt;/strong&gt;，此时操作系统可以运行其他进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线程&lt;/strong&gt;：进程内部的执行单元，用于把程序划分为多个相对独立的任务。是 CPU 资源调度的单元&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一进程内的线程共享大部分资源。&lt;/li&gt;
&lt;li&gt;线程切换通常比进程切换更快。&lt;/li&gt;
&lt;li&gt;每个线程需要自己的程序计数器和调用栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程共享与独有内容：&lt;/p&gt;
&lt;p&gt;| 类型 | 内容 |
|---|---|
| 共享 | 可执行文件、进程内存、大部分资源、I/O 设备 |
| 独有 | 程序计数器、调用栈 |&lt;/p&gt;
&lt;p&gt;图 2.2：有三个线程的进程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-01.BeXan0Vd_L1Vdk.webp&quot; alt=&quot;Diagram 01&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2.2 冯·诺依曼模型的改进&lt;/h2&gt;
&lt;p&gt;本节介绍三类改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存&lt;/li&gt;
&lt;li&gt;虚拟内存&lt;/li&gt;
&lt;li&gt;低级别并行性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2.1 缓存基础&lt;/h3&gt;
&lt;h3&gt;缓存的作用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;缓存用于缓解冯·诺依曼瓶颈。&lt;/li&gt;
&lt;li&gt;思想：
&lt;ul&gt;
&lt;li&gt;一次传输一块数据或指令，而不是单个数据项。&lt;/li&gt;
&lt;li&gt;把常用数据和指令放在更靠近 CPU 的高速存储中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;CPU 缓存&lt;/strong&gt;：CPU 能以比访问主存更快速度访问的一组内存位置。缓存速度快，但容量通常远小于主存。&lt;/p&gt;
&lt;h3&gt;局部性&lt;/h3&gt;
&lt;p&gt;| 类型 | 含义 | 例子 |
|---|---|---|
| 时间局部性 | 最近访问过的内容，近期可能再次访问 | 循环中反复使用变量 |
| 空间局部性 | 访问某位置后，附近位置可能很快被访问 | 顺序遍历数组 |&lt;/p&gt;
&lt;p&gt;数组例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;float z[1000];
sum = 0.0;
for (i = 0; i &amp;#x3C; 1000; i++)
    sum += z[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组连续存储，访问 &lt;code&gt;z[i]&lt;/code&gt; 后通常接着访问 &lt;code&gt;z[i+1]&lt;/code&gt;，符合空间局部性。&lt;/p&gt;
&lt;h3&gt;缓存行&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存行 / 缓存块&lt;/strong&gt;：内存和缓存之间传输的基本单位。&lt;/li&gt;
&lt;li&gt;一次缓存缺失通常会把包含目标数据的一整行读入缓存。&lt;/li&gt;
&lt;li&gt;如果一个缓存行保存 &lt;code&gt;z[0]&lt;/code&gt; 到 &lt;code&gt;z[15]&lt;/code&gt;，那么第一次访问 &lt;code&gt;z[0]&lt;/code&gt; 后，后续访问 &lt;code&gt;z[1]&lt;/code&gt; 到 &lt;code&gt;z[15]&lt;/code&gt; 可能直接命中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多级缓存&lt;/h3&gt;
&lt;p&gt;| 层级 | 特点 |
|---|---|
| L1 | 最小、最快、最接近 CPU |
| L2 | 比 L1 大，速度较慢 |
| L3 | 更大，通常更慢 |&lt;/p&gt;
&lt;p&gt;CPU 访问数据时通常按 &lt;code&gt;L1 -&gt; L2 -&gt; L3 -&gt; 主存&lt;/code&gt; 的顺序查找。&lt;/p&gt;
&lt;h3&gt;缓存命中与缺失&lt;/h3&gt;
&lt;p&gt;| 概念 | 含义 |
|---|---|
| 缓存命中 hit | 所需数据或指令在缓存中 |
| 缓存缺失 miss | 所需数据或指令不在缓存中，需要访问更慢层级 |&lt;/p&gt;
&lt;p&gt;例子：&lt;code&gt;L1&lt;/code&gt; 缺失但 &lt;code&gt;L2&lt;/code&gt; 命中。&lt;/p&gt;
&lt;p&gt;缓存缺失可能导致 CPU 暂停，等待数据从主存或低层缓存取回。&lt;/p&gt;
&lt;h3&gt;写策略&lt;/h3&gt;
&lt;p&gt;| 策略 | 做法 | 特点 |
|---|---|---|
| 写直达 | 写缓存时同时写主存 | 主存更新及时，写入开销较大 |
| 写回 | 先只写缓存并标记为脏，替换时再写回主存 | 写入较快，但需要维护脏数据 |&lt;/p&gt;
&lt;h3&gt;2.2.2 缓存映射&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;缓存映射问题&lt;/strong&gt;：主存中的一个数据块被读入缓存时，应该放在缓存的哪个位置？&lt;/p&gt;
&lt;h3&gt;三种映射方式&lt;/h3&gt;
&lt;p&gt;| 方式 | 含义 | 特点 |
|---|---|---|
| 全相联缓存 | 主存块可放入缓存任意位置 | 灵活，硬件复杂 |
| 直接映射缓存 | 每个主存块只能放入唯一缓存行 | 简单，但冲突多 |
| n 路组相联缓存 | 每个主存块可放入某组中的 n 个位置之一 | 折中方案 |&lt;/p&gt;
&lt;h3&gt;直接映射例子&lt;/h3&gt;
&lt;p&gt;主存有 16 个块，缓存有 4 行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;缓存行索引 = 主存块索引 mod 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0, 4, 8, 12 -&gt; 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1, 5, 9, 13 -&gt; 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2, 6, 10, 14 -&gt; 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3, 7, 11, 15 -&gt; 3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两路组相联例子&lt;/h3&gt;
&lt;p&gt;缓存 4 行，分成两组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组 0：缓存行 &lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;组 1：缓存行 &lt;code&gt;2&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;组索引 = 主存块索引 mod 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;偶数主存块：可放入缓存行 &lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;奇数主存块：可放入缓存行 &lt;code&gt;2&lt;/code&gt; 或 &lt;code&gt;3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;表 2.1：16 个主存块分配到 4 个缓存行&lt;/h3&gt;
&lt;p&gt;| 主存索引 | 全相联 | 直接映射 | 两路组相联 |
|---:|---|---:|---|
| 0 | 0、1、2 或 3 | 0 | 0 或 1 |
| 1 | 0、1、2 或 3 | 1 | 2 或 3 |
| 2 | 0、1、2 或 3 | 2 | 0 或 1 |
| 3 | 0、1、2 或 3 | 3 | 2 或 3 |
| 4 | 0、1、2 或 3 | 0 | 0 或 1 |
| 5 | 0、1、2 或 3 | 1 | 2 或 3 |
| 6 | 0、1、2 或 3 | 2 | 0 或 1 |
| 7 | 0、1、2 或 3 | 3 | 2 或 3 |
| 8 | 0、1、2 或 3 | 0 | 0 或 1 |
| 9 | 0、1、2 或 3 | 1 | 2 或 3 |
| 10 | 0、1、2 或 3 | 2 | 0 或 1 |
| 11 | 0、1、2 或 3 | 3 | 2 或 3 |
| 12 | 0、1、2 或 3 | 0 | 0 或 1 |
| 13 | 0、1、2 或 3 | 1 | 2 或 3 |
| 14 | 0、1、2 或 3 | 2 | 0 或 1 |
| 15 | 0、1、2 或 3 | 3 | 2 或 3 |&lt;/p&gt;
&lt;h3&gt;缓存替换策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当多个主存块可映射到多个缓存位置时，需要决定替换哪一行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LRU&lt;/strong&gt;：Least Recently Used，最近最少使用。&lt;/li&gt;
&lt;li&gt;思想：优先逐出最久没有被访问的缓存行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2.3 缓存和程序：示例&lt;/h3&gt;
&lt;h3&gt;核心结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU 缓存由硬件控制，程序员不能直接决定缓存内容。&lt;/li&gt;
&lt;li&gt;但程序员可以利用局部性写出更缓存友好的代码。&lt;/li&gt;
&lt;li&gt;C 语言二维数组按&lt;strong&gt;行优先&lt;/strong&gt;存储，因此按行访问通常比按列访问快。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;行优先存储&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;A[4][4]&lt;/code&gt; 在内存中按行连续存储：&lt;/p&gt;
&lt;p&gt;| 缓存行 | A 的元素 |
|---:|---|
| 0 | &lt;code&gt;A[0][0]&lt;/code&gt; &lt;code&gt;A[0][1]&lt;/code&gt; &lt;code&gt;A[0][2]&lt;/code&gt; &lt;code&gt;A[0][3]&lt;/code&gt; |
| 1 | &lt;code&gt;A[1][0]&lt;/code&gt; &lt;code&gt;A[1][1]&lt;/code&gt; &lt;code&gt;A[1][2]&lt;/code&gt; &lt;code&gt;A[1][3]&lt;/code&gt; |
| 2 | &lt;code&gt;A[2][0]&lt;/code&gt; &lt;code&gt;A[2][1]&lt;/code&gt; &lt;code&gt;A[2][2]&lt;/code&gt; &lt;code&gt;A[2][3]&lt;/code&gt; |
| 3 | &lt;code&gt;A[3][0]&lt;/code&gt; &lt;code&gt;A[3][1]&lt;/code&gt; &lt;code&gt;A[3][2]&lt;/code&gt; &lt;code&gt;A[3][3]&lt;/code&gt; |&lt;/p&gt;
&lt;h3&gt;两种循环顺序&lt;/h3&gt;
&lt;p&gt;按行访问，缓存友好：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;#x3C; MAX; i++)
    for (j = 0; j &amp;#x3C; MAX; j++)
        y[i] += A[i][j] * x[j];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按列访问，缓存不友好：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (j = 0; j &amp;#x3C; MAX; j++)
    for (i = 0; i &amp;#x3C; MAX; i++)
        y[i] += A[i][j] * x[j];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种访问顺序类似：&lt;code&gt;A[0][0], A[0][1], A[0][2], A[0][3]&lt;/code&gt;，连续。&lt;/li&gt;
&lt;li&gt;第二种访问顺序类似：&lt;code&gt;A[0][0], A[1][0], A[2][0], A[3][0]&lt;/code&gt;，不连续。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存缺失分析&lt;/h3&gt;
&lt;p&gt;假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MAX = 4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个缓存行保存 4 个 &lt;code&gt;double&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;缓存只能保存两行 &lt;code&gt;A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;循环开始时 &lt;code&gt;A&lt;/code&gt; 不在缓存中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按行访问：每行第一次访问缺失，之后同一行命中；约 4 次缺失。&lt;/li&gt;
&lt;li&gt;按列访问：频繁跨行访问，缓存行容易被替换；缺失更多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结论：对 C 的二维数组，优先按行遍历，能更好利用空间局部性。&lt;/p&gt;
&lt;h3&gt;2.2.4 虚拟内存&lt;/h3&gt;
&lt;h3&gt;核心概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟内存&lt;/strong&gt;：让程序使用虚拟地址，由系统映射到实际物理地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页&lt;/strong&gt;：虚拟内存和物理内存管理的基本块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表&lt;/strong&gt;：记录虚拟页到物理页的映射关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TLB&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TLB&lt;/strong&gt;：Translation Lookaside Buffer，旁路转换缓冲区。&lt;/li&gt;
&lt;li&gt;作用：缓存少量页表项，加速虚拟地址到物理地址的转换。&lt;/li&gt;
&lt;li&gt;常见大小：几十到几百个条目。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 概念 | 含义 |
|---|---|
| TLB 命中 | 虚拟页号在 TLB 中，地址转换较快 |
| TLB 缺失 | 虚拟页号不在 TLB 中，需要访问页表 |
| 页面错误 | 要访问的页不在主存中，只在磁盘上，需要从磁盘调入 |&lt;/p&gt;
&lt;h3&gt;虚拟内存与缓存的区别&lt;/h3&gt;
&lt;p&gt;| 对比 | CPU 缓存 | 虚拟内存 |
|---|---|---|
| 主要控制者 | 硬件 | 硬件 + 操作系统 |
| 慢速后备存储 | 主存 | 磁盘 |
| 写策略 | 可写直达或写回 | 通常使用写回，避免频繁访问磁盘 |&lt;/p&gt;
&lt;h3&gt;2.2.5 指令级并行&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;指令级并行&lt;/strong&gt;（Instruction-Level Parallelism，ILP）：通过让多个处理器组件或功能单元同时执行指令来提高处理器性能。&lt;/p&gt;
&lt;p&gt;ILP 的两种主要方法：&lt;/p&gt;
&lt;p&gt;| 方法 | 核心思想 |
|---|---|
| 流水线 | 把功能单元分成多个阶段，多个指令在不同阶段重叠执行 |
| 多发射 | 复制功能单元，同时启动多条可并行执行的指令 |&lt;/p&gt;
&lt;p&gt;几乎所有现代 CPU 都会使用流水线和多发射。&lt;/p&gt;
&lt;h3&gt;流水线&lt;/h3&gt;
&lt;p&gt;流水线类似工厂装配线：不同阶段同时处理不同任务。&lt;/p&gt;
&lt;p&gt;浮点加法可拆成多个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取操作数&lt;/li&gt;
&lt;li&gt;比较指数&lt;/li&gt;
&lt;li&gt;移位一个操作数&lt;/li&gt;
&lt;li&gt;相加&lt;/li&gt;
&lt;li&gt;标准化结果&lt;/li&gt;
&lt;li&gt;四舍五入&lt;/li&gt;
&lt;li&gt;存储结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果每个阶段需要 &lt;code&gt;1 ns&lt;/code&gt;，单个浮点加法仍需 &lt;code&gt;7 ns&lt;/code&gt;。但在循环中，不同加法可以同时处于不同阶段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;#x3C; 1000; i++)
    z[i] = x[i] + y[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非流水线：1000 次加法约需 &lt;code&gt;7000 ns&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;7 阶段流水线：理想情况下约需 &lt;code&gt;1006 ns&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;原因：流水线填满后，几乎每 &lt;code&gt;1 ns&lt;/code&gt; 产生一个结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;code&gt;k&lt;/code&gt; 阶段流水线通常无法获得严格 &lt;code&gt;k&lt;/code&gt; 倍加速。&lt;/p&gt;
&lt;p&gt;限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阶段速度受最慢功能单元限制。&lt;/li&gt;
&lt;li&gt;等待操作数、分支等延迟可能导致流水线暂停。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多发射&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;多发射处理器&lt;/strong&gt;：复制功能单元，尝试同时执行程序中的多条不同指令。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;#x3C; 1000; i++)
    z[i] = x[i] + y[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有两个浮点加法器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个加法器计算 &lt;code&gt;z[0]&lt;/code&gt; 时，第二个可以计算 &lt;code&gt;z[1]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第一个计算 &lt;code&gt;z[2]&lt;/code&gt; 时，第二个可以计算 &lt;code&gt;z[3]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多发射分类：&lt;/p&gt;
&lt;p&gt;| 类型 | 调度时机 | 说明 |
|---|---|---|
| 静态多发射 | 编译时 | 编译器安排哪些指令同时执行 |
| 动态多发射 | 运行时 | 处理器运行时安排，也称超标量处理器 |&lt;/p&gt;
&lt;h3&gt;前瞻执行&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;前瞻执行&lt;/strong&gt;（speculation）：编译器或处理器预测后续执行路径，并提前执行相关指令。&lt;/p&gt;
&lt;p&gt;分支预测例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;z = x + y;
if (z &gt; 0)
    w = x;
else
    w = y;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统可能预测 &lt;code&gt;z &gt; 0&lt;/code&gt;，于是提前执行 &lt;code&gt;w = x&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;指针别名预测例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;z = x + y;
w = *a_p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统可能预测 &lt;code&gt;a_p&lt;/code&gt; 不指向 &lt;code&gt;z&lt;/code&gt;，于是尝试同时执行两个赋值。&lt;/p&gt;
&lt;p&gt;前瞻执行的关键：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果预测正确，保留结果。&lt;/li&gt;
&lt;li&gt;如果预测错误，丢弃预测结果并重新执行正确路径。&lt;/li&gt;
&lt;li&gt;硬件通常会把预测执行结果先放在缓冲区中，确认正确后再提交到寄存器或内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;指令重排&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;动态多发射系统可以无序执行指令。&lt;/li&gt;
&lt;li&gt;现代处理器通常仍按顺序加载指令，并按程序顺序提交结果。&lt;/li&gt;
&lt;li&gt;优化编译器也可能重排指令。&lt;/li&gt;
&lt;li&gt;指令重排会影响共享内存编程，后续需要注意内存可见性和同步问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2.6 硬件多线程&lt;/h3&gt;
&lt;h3&gt;ILP 的局限&lt;/h3&gt;
&lt;p&gt;ILP 很难利用长依赖链程序中的并行性。&lt;/p&gt;
&lt;p&gt;例子：直接计算斐波那契数列。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;f[0] = f[1] = 1;
for (i = 2; i &amp;#x3C;= n; i++)
    f[i] = f[i-1] + f[i-2];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一项依赖前两项，几乎没有可以同时执行的指令。&lt;/p&gt;
&lt;h3&gt;线程级并行&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线程级并行&lt;/strong&gt;（Thread-Level Parallelism，TLP）：通过同时执行不同线程来提供并行性。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比 ILP 粒度更粗。&lt;/li&gt;
&lt;li&gt;并行单位是线程，而不是单条指令。&lt;/li&gt;
&lt;li&gt;当当前线程因为等待内存等原因暂停时，可以切换去执行另一个线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;硬件多线程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;硬件多线程&lt;/strong&gt;：处理器支持线程之间的快速切换，使系统在一个线程暂停时继续执行其他线程。&lt;/p&gt;
&lt;p&gt;分类：&lt;/p&gt;
&lt;p&gt;| 类型     | 做法                   | 特点                      |
| ------ | -------------------- | ----------------------- |
| 细粒度多线程 | 每条指令后都可在线程间切换，跳过暂停线程 | 可减少暂停浪费，但单个活跃线程也可能被频繁打断 |
| 粗粒度多线程 | 只有线程因耗时操作暂停时才切换      | 切换次数较少，但遇到暂停前可能仍会浪费周期   |&lt;/p&gt;
&lt;h2&gt;2.3 并行硬件&lt;/h2&gt;
&lt;h3&gt;2.3.2 SIMD 系统&lt;/h3&gt;
&lt;h3&gt;基本特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SIMD&lt;/strong&gt;：Single Instruction Multiple Data，单指令流多数据流。&lt;/li&gt;
&lt;li&gt;一个控制单元向多个数据通路广播同一条指令。&lt;/li&gt;
&lt;li&gt;各数据通路对自己的数据执行相同指令，或者在不适用时空闲。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抽象 SIMD 系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-02.hxcU6kLC_1H6bqA.webp&quot; alt=&quot;Diagram 02&quot;&gt;&lt;/p&gt;
&lt;h3&gt;条件分支带来的问题&lt;/h3&gt;
&lt;p&gt;SIMD 要求所有数据通路执行相同指令或空闲，这可能降低整体性能。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;#x3C; n; i++)
    if (y[i] &gt; 0.0)
        x[i] += y[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个数据通路都要加载对应的 &lt;code&gt;y[i]&lt;/code&gt; 并判断是否为正。&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;y[i] &gt; 0&lt;/code&gt;，该通路执行加法。&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;y[i] &amp;#x3C;= 0&lt;/code&gt;，该通路在其他通路执行加法时只能空闲。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;同步执行约束&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;经典 SIMD 系统中，各数据通路必须&lt;strong&gt;同步运行&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每个数据通路必须等待下一条指令广播后才能继续。&lt;/li&gt;
&lt;li&gt;数据通路通常没有自己的指令存储区，不能把指令保存起来延迟执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据并行性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据并行性&lt;/strong&gt;：把数据划分给多个处理器或数据通路，并让它们对各自的数据子集执行相同或相似的指令。&lt;/p&gt;
&lt;p&gt;SIMD 系统主要利用的就是数据并行性。&lt;/p&gt;
&lt;h3&gt;GPU 不是纯 SIMD&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;图形处理 API 通常用点、线、三角形表示对象表面，再通过图形处理流水线生成屏幕像素。&lt;/li&gt;
&lt;li&gt;流水线中的可编程阶段由&lt;strong&gt;着色器函数&lt;/strong&gt;指定。&lt;/li&gt;
&lt;li&gt;着色器函数通常很短，并且可以应用到图形流中的大量元素，因此天然具有隐式并行性。&lt;/li&gt;
&lt;li&gt;GPU 每个处理核心通常包含大量数据通路，可用 SIMD 并行提高吞吐量。&lt;/li&gt;
&lt;li&gt;GPU 处理图像时数据量很大，需要很高的数据传输速率，因此严重依赖硬件多线程来隐藏内存访问暂停。&lt;/li&gt;
&lt;li&gt;GPU 在小问题上可能表现较差，因为需要大量线程才能让数据通路保持忙碌。&lt;/li&gt;
&lt;li&gt;GPU 会使用 SIMD 式的数据并行，但不是纯 SIMD 系统。&lt;/li&gt;
&lt;li&gt;给定核心上的多个数据通路可以执行 SIMD 并行。&lt;/li&gt;
&lt;li&gt;当代 GPU 的单个核心也可以运行多个指令流。&lt;/li&gt;
&lt;li&gt;典型 GPU 有几十个核心，这些核心也能执行相互独立的指令流。&lt;/li&gt;
&lt;li&gt;因此，GPU 既不是纯 SIMD，也不是纯 MIMD，而是混合了多种并行特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GPU 的内存特征&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;GPU 可以使用共享内存，也可以使用分布式内存。&lt;/li&gt;
&lt;li&gt;大型 GPU 系统可能同时包含两种特征：
&lt;ul&gt;
&lt;li&gt;一些核心访问共同的共享内存块。&lt;/li&gt;
&lt;li&gt;其他核心访问不同的共享内存块。&lt;/li&gt;
&lt;li&gt;访问不同内存块的核心可以通过网络通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;本书后续主要讨论使用共享内存的 GPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3.3 MIMD 系统&lt;/h3&gt;
&lt;h3&gt;基本特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MIMD&lt;/strong&gt;：Multiple Instruction Multiple Data，多指令流多数据流。&lt;/li&gt;
&lt;li&gt;支持多个并行指令流运行在多个数据流上。&lt;/li&gt;
&lt;li&gt;通常由多个独立处理单元或核心组成。&lt;/li&gt;
&lt;li&gt;每个处理单元通常有自己的控制单元和数据通路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抽象 MIMD 系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-03.FazA3C72_ZCsvRN.webp&quot; alt=&quot;Diagram 03&quot;&gt;&lt;/p&gt;
&lt;h3&gt;异步执行&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;MIMD 系统通常是&lt;strong&gt;异步&lt;/strong&gt;的。&lt;/li&gt;
&lt;li&gt;各处理器可以按自己的速度运行。&lt;/li&gt;
&lt;li&gt;许多 MIMD 系统没有全局时钟。&lt;/li&gt;
&lt;li&gt;即使多个处理器执行相同指令序列，在任意时刻也可能处于不同语句。&lt;/li&gt;
&lt;li&gt;如果需要让处理器在某些点对齐，程序员必须显式同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MIMD 的两类内存系统&lt;/h3&gt;
&lt;p&gt;| 类型 | 特点 | 通信方式 |
|---|---|---|
| 共享内存系统 | 多个处理器通过互连网络连接到同一内存系统，每个处理器可访问每个内存位置 | 通过共享数据结构隐式通信 |
| 分布式内存系统 | 每个处理器和自己的私有内存配对，处理器-内存对通过网络连接 | 通过消息或函数访问显式通信 |&lt;/p&gt;
&lt;p&gt;共享内存系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-04.Dwj2aynf_2uBGJa.webp&quot; alt=&quot;Diagram 04&quot;&gt;
分布式内存系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-05.DVEL7TsP_1pVvV4.webp&quot; alt=&quot;Diagram 05&quot;&gt;&lt;/p&gt;
&lt;h3&gt;共享内存系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;最常见的共享内存系统使用一个或多个多核处理器。&lt;/li&gt;
&lt;li&gt;多核处理器在单个芯片上包含多个 CPU 或核心。&lt;/li&gt;
&lt;li&gt;核心通常有私有 &lt;code&gt;L1&lt;/code&gt; 缓存，其他缓存可能共享，也可能不共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共享内存多核系统分为：&lt;/p&gt;
&lt;p&gt;| 类型 | 全称 | 特点 |
|---|---|---|
| UMA | Uniform Memory Access，统一内存访问 | 所有核心访问所有内存位置的时间相同，编程较容易 |
| NUMA | Non-Uniform Memory Access，非统一内存访问 | 访问本地内存更快，访问其他芯片连接的内存更慢 |&lt;/p&gt;
&lt;p&gt;UMA 多核系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-06.CTgjJ1s9_23ht3u.webp&quot; alt=&quot;Diagram 06&quot;&gt;
NUMA 多核系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-07.CIRa-BTP_Z1BRLpT.webp&quot; alt=&quot;Diagram 07&quot;&gt;&lt;/p&gt;
&lt;h3&gt;分布式内存系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;最常见的分布式内存系统称为&lt;strong&gt;集群&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;集群通常由一系列商用系统组成，例如通过以太网连接的 PC。&lt;/li&gt;
&lt;li&gt;集群中的&lt;strong&gt;节点&lt;/strong&gt;是通过通信网络连接在一起的计算单元。&lt;/li&gt;
&lt;li&gt;现代集群节点通常本身就是共享内存系统，并带有一个或多个多核处理器。&lt;/li&gt;
&lt;li&gt;为了区别于纯分布式内存系统，这类系统有时称为&lt;strong&gt;混合系统&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;现在通常默认集群具有共享内存节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;网格&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网格&lt;/strong&gt;提供基础设施，把地理上分布的大型计算机网络组织成统一的分布式内存系统。&lt;/li&gt;
&lt;li&gt;网格通常是&lt;strong&gt;异构&lt;/strong&gt;系统：不同节点可能由不同类型的硬件构成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3.5 缓存一致性&lt;/h3&gt;
&lt;h3&gt;缓存一致性问题&lt;/h3&gt;
&lt;p&gt;在共享内存多核系统中，多个核心的缓存可能保存同一个变量的副本。&lt;/p&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心 0 更新变量 &lt;code&gt;x&lt;/code&gt; 后，核心 1 缓存中的 &lt;code&gt;x&lt;/code&gt; 可能仍是旧值。&lt;/li&gt;
&lt;li&gt;写直达会更新主存，但不一定更新其他核心缓存中的副本。&lt;/li&gt;
&lt;li&gt;写回甚至可能暂时只更新核心 0 的缓存，主存和其他核心都看不到新值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缓存一致性问题&lt;/strong&gt;：当多个处理器缓存同一变量或同一缓存行时，需要保证一个处理器的更新能被其他处理器正确看到。&lt;/p&gt;
&lt;h3&gt;监听缓存一致性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;监听缓存一致性&lt;/strong&gt;（snooping cache coherence）：各核心监听互连网络上的更新广播。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-08.BVNPxy8u_1abVnr.webp&quot; alt=&quot;Diagram 08&quot;&gt;
要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;广播通常针对&lt;strong&gt;缓存行&lt;/strong&gt;，不是单个变量。&lt;/li&gt;
&lt;li&gt;互连不一定必须是总线，但必须支持从一个处理器向其他处理器广播。&lt;/li&gt;
&lt;li&gt;监听可用于写直达缓存，也可用于写回缓存。&lt;/li&gt;
&lt;li&gt;写回缓存需要额外通信，因为更新不会立刻写入主存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基于目录的缓存一致性&lt;/h3&gt;
&lt;p&gt;监听协议需要频繁广播，在大规模系统中广播成本高，可扩展性差。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于目录的缓存一致性&lt;/strong&gt;：使用一个称为&lt;strong&gt;目录&lt;/strong&gt;的数据结构记录每个缓存行的状态。&lt;/p&gt;
&lt;p&gt;目录通常记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某缓存行当前在哪些核心的缓存中有副本。&lt;/li&gt;
&lt;li&gt;缓存行是否被修改。&lt;/li&gt;
&lt;li&gt;缓存行的有效/无效状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;某核心读取一个缓存行时，目录记录该核心拥有该行副本。&lt;/li&gt;
&lt;li&gt;某核心更新该缓存行时，先查询目录。&lt;/li&gt;
&lt;li&gt;系统只通知那些真正保存该缓存行副本的核心，使其副本失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;优缺点：&lt;/p&gt;
&lt;p&gt;| 方案 | 优点 | 缺点 |
|---|---|---|
| 监听协议 | 简单，适合小规模共享系统 | 广播成本高，不易扩展 |
| 目录协议 | 只通知相关核心，适合较大系统 | 目录需要额外存储和维护开销 |&lt;/p&gt;
&lt;h3&gt;伪共享&lt;/h3&gt;
&lt;p&gt;CPU 缓存由硬件实现，并且按&lt;strong&gt;缓存行&lt;/strong&gt;工作，而不是按单个变量工作。这可能导致伪共享。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;伪共享&lt;/strong&gt;：多个核心访问的是不同变量，但这些变量位于同一个缓存行中，导致系统像它们共享同一变量一样不断失效缓存行。&lt;/p&gt;
&lt;p&gt;串行例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int i, j, m, n;
double y[m];

/* y 初始化为 0 */
for (i = 0; i &amp;#x3C; m; i++)
    for (j = 0; j &amp;#x3C; n; j++)
        y[i] += f(i, j);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以把外层循环的迭代划分给多个核心。例如两个核心时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* 私有变量 */
int i, j, iter_count;

/* 共享变量 */
int m, n, core_count;
double y[m];

iter_count = m / core_count;

/* 核心 0 */
for (i = 0; i &amp;#x3C; iter_count; i++)
    for (j = 0; j &amp;#x3C; n; j++)
        y[i] += f(i, j);

/* 核心 1 */
for (i = iter_count; i &amp;#x3C; 2 * iter_count; i++)
    for (j = 0; j &amp;#x3C; n; j++)
        y[i] += f(i, j);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m = 8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double&lt;/code&gt; 是 8 字节&lt;/li&gt;
&lt;li&gt;缓存行是 64 字节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y[0]&lt;/code&gt; 位于缓存行开头&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是一个缓存行正好能存下 8 个 &lt;code&gt;double&lt;/code&gt;，即 &lt;code&gt;y[0]..y[7]&lt;/code&gt; 全部位于同一个缓存行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-09.D9hDfpr0_2sM6B.webp&quot; alt=&quot;Diagram 09&quot;&gt;
发生过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心 0 更新 &lt;code&gt;y[0]..y[3]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;核心 1 更新 &lt;code&gt;y[4]..y[7]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;两个核心没有访问彼此的元素。&lt;/li&gt;
&lt;li&gt;但这些元素属于同一个缓存行。&lt;/li&gt;
&lt;li&gt;因此任一核心执行 &lt;code&gt;y[i] += f(i, j)&lt;/code&gt; 时，都可能使另一个核心缓存中的同一行失效。&lt;/li&gt;
&lt;li&gt;另一个核心下一次访问自己的 &lt;code&gt;y&lt;/code&gt; 元素时，必须重新从内存取回该缓存行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序结果仍正确，但会产生大量不必要的内存访问，严重降低性能。&lt;/li&gt;
&lt;li&gt;如果内层循环次数 &lt;code&gt;n&lt;/code&gt; 很大，这种额外内存访问会非常频繁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见缓解方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用线程或进程本地临时变量。&lt;/li&gt;
&lt;li&gt;最后再把本地结果复制或归约到共享存储。&lt;/li&gt;
&lt;li&gt;必要时通过填充、对齐或调整数据布局，避免不同线程频繁写同一缓存行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3.6 共享内存与分布式内存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;共享内存对程序员更直观：可以通过共享数据结构隐式协调处理器工作。&lt;/li&gt;
&lt;li&gt;分布式内存需要显式发送消息，编程负担通常更重。&lt;/li&gt;
&lt;li&gt;但硬件上，共享内存系统需要维护互连、缓存一致性和访问延迟，扩展到大规模系统更困难。&lt;/li&gt;
&lt;li&gt;因此，大规模系统常采用分布式内存或混合结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.4 并行软件&lt;/h2&gt;
&lt;h3&gt;2.4.1 并行化、通信和同步&lt;/h3&gt;
&lt;h3&gt;易并行示例&lt;/h3&gt;
&lt;p&gt;数组加法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;double x[n], y[n];

for (int i = 0; i &amp;#x3C; n; i++)
    x[i] += y[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并行化方法：把数组元素划分给不同进程或线程。&lt;/p&gt;
&lt;p&gt;例如有 &lt;code&gt;p&lt;/code&gt; 个进程/线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程/线程 0 负责 &lt;code&gt;0 ~ n/p - 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;进程/线程 1 负责 &lt;code&gt;n/p ~ 2n/p - 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;依此类推&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;并行化时需要做的事&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在进程/线程之间分配工作。&lt;/li&gt;
&lt;li&gt;保证每个进程/线程工作量大致相同，即&lt;strong&gt;负载平衡&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;尽量减少通信量。&lt;/li&gt;
&lt;li&gt;安排进程/线程之间的同步。&lt;/li&gt;
&lt;li&gt;安排进程/线程之间的通信。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通信与同步常常相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分布式内存程序通常通过进程间通信隐式同步。&lt;/li&gt;
&lt;li&gt;共享内存程序通常通过线程同步来完成通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4.3 共享内存&lt;/h3&gt;
&lt;h3&gt;共享变量与私有变量&lt;/h3&gt;
&lt;p&gt;在共享内存程序中：&lt;/p&gt;
&lt;p&gt;| 变量类型 | 含义 |
|---|---|
| 共享变量 | 任意线程都可以读写 |
| 私有变量 | 只能由某一个线程访问 |&lt;/p&gt;
&lt;p&gt;线程之间通常通过共享变量通信，因此通信是&lt;strong&gt;隐式&lt;/strong&gt;的。&lt;/p&gt;
&lt;h3&gt;动态线程和静态线程&lt;/h3&gt;
&lt;p&gt;| 类型 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 动态线程 | 主线程等待请求，按需派生工作线程，完成后线程终止并加入主线程 | 资源只在线程实际运行时占用 | 频繁创建和连接线程可能耗时 |
| 静态线程 | 主线程完成设置后一次性派生所有线程，所有工作结束后再统一停止 | 避免频繁创建/销毁线程，性能可能更好；更接近分布式内存编程范式 | 空闲线程资源无法释放 |&lt;/p&gt;
&lt;p&gt;本书后续经常使用静态线程范例。&lt;/p&gt;
&lt;h3&gt;非确定性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;非确定性&lt;/strong&gt;：同一个输入可能导致不同输出。&lt;/p&gt;
&lt;p&gt;在异步执行的 MIMD 系统中，多个线程独立运行，每个线程完成语句的相对速度可能每次运行都不同，因此输出顺序或结果可能变化。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;printf(&quot;Thread %d &gt; my_x = %d\n&quot;, my_rank, my_x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个线程的输出顺序可能不同：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thread 0 &gt; my_x = 7
Thread 1 &gt; my_x = 19
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可能是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thread 1 &gt; my_x = 19
Thread 0 &gt; my_x = 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是输出顺序不同，通常问题不大；但在共享内存更新中，非确定性可能导致错误结果。&lt;/p&gt;
&lt;h3&gt;竞争条件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;竞争条件&lt;/strong&gt;：多个线程或进程同时访问共享资源，并且访问顺序会影响结果。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;my_val = Compute_val(my_rank);
x += my_val;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x += my_val&lt;/code&gt; 通常不是一条不可分割的操作，而是大致包含：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 &lt;code&gt;x&lt;/code&gt; 加载到寄存器。&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;my_val&lt;/code&gt; 加载到寄存器。&lt;/li&gt;
&lt;li&gt;相加。&lt;/li&gt;
&lt;li&gt;把结果写回 &lt;code&gt;x&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果两个线程交错执行，可能都先读到旧的 &lt;code&gt;x = 0&lt;/code&gt;，最后一个写回的结果覆盖另一个线程的更新，导致结果错误。&lt;/p&gt;
&lt;h3&gt;原子性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原子操作&lt;/strong&gt;：操作执行期间不会被其他线程打断或观察到中间状态。&lt;/p&gt;
&lt;p&gt;对于共享变量更新，理想情况是每个线程的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;x += my_val;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作为一个原子操作完成。&lt;/p&gt;
&lt;h3&gt;临界区&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;临界区&lt;/strong&gt;：一次只能由一个线程执行的代码块。&lt;/p&gt;
&lt;p&gt;如果一个线程正在执行临界区，其他线程必须被排除在外。这称为&lt;strong&gt;互斥访问&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;互斥量&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;互斥量&lt;/strong&gt;（mutex，也叫互斥锁、锁）：用于保证临界区互斥访问的同步对象。&lt;/p&gt;
&lt;p&gt;使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;my_val = Compute_val(my_rank);
Lock(&amp;#x26;add_my_val_lock);
x += my_val;
Unlock(&amp;#x26;add_my_val_lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-10.BxHoqX2u_Z1Oqy.webp&quot; alt=&quot;Diagram 10&quot;&gt;
要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥量保证一次只有一个线程执行 &lt;code&gt;x += my_val&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;互斥量不规定哪个线程先执行。&lt;/li&gt;
&lt;li&gt;临界区会被串行化，因此临界区应尽可能少、尽可能短。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;忙等待&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;忙等待&lt;/strong&gt;（busy-waiting）：线程进入循环，不断测试某个条件是否成立。&lt;/p&gt;
&lt;p&gt;例子：保证线程 1 在线程 0 更新 &lt;code&gt;x&lt;/code&gt; 之后再更新。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;my_val = Compute_val(my_rank);

if (my_rank == 1)
    while (!ok_for_1);  /* 忙等待循环 */

x += my_val;           /* 临界区 */

if (my_rank == 0)
    ok_for_1 = true;   /* 允许线程 1 更新 x */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：概念简单，容易理解。&lt;/li&gt;
&lt;li&gt;缺点：等待线程会持续占用 CPU 资源，即使它暂时没有做有用工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;信号量和监视器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信号量&lt;/strong&gt;：类似互斥量，也可用于线程同步，具体行为细节不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监视器&lt;/strong&gt;：更高级的互斥机制，是一个对象，其方法一次只能由一个线程执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程安全&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线程安全&lt;/strong&gt;：函数或代码在多线程环境中被并发调用时，仍能得到正确结果，不会因为共享状态产生错误。&lt;/p&gt;
&lt;p&gt;很多串行程序中的函数可以直接用于并行程序，但需要注意例外。&lt;/p&gt;
&lt;p&gt;典型问题：函数内部使用&lt;strong&gt;静态局部变量&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通局部变量位于线程自己的调用栈上，通常是私有的。&lt;/li&gt;
&lt;li&gt;静态局部变量在函数调用结束后仍然存在。&lt;/li&gt;
&lt;li&gt;如果多个线程同时调用该函数，就可能共享并覆盖同一个静态变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;code&gt;strtok&lt;/code&gt; 一类函数不是线程安全的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次调用时传入一个字符串。&lt;/li&gt;
&lt;li&gt;后续调用依赖函数内部保存的静态状态继续分割字符串。&lt;/li&gt;
&lt;li&gt;如果两个线程同时使用，后一个线程可能覆盖前一个线程的内部状态，导致结果错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多线程程序中要确认库函数是否线程安全。&lt;/li&gt;
&lt;li&gt;避免在多个线程间隐式共享函数内部状态。&lt;/li&gt;
&lt;li&gt;必要时使用线程安全版本的函数，或把状态显式放在线程私有存储中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4.4 分布式内存&lt;/h3&gt;
&lt;h3&gt;基本特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在分布式内存程序中，每个核心或进程只能直接访问自己的私有内存。&lt;/li&gt;
&lt;li&gt;进程之间不能直接读写对方变量。&lt;/li&gt;
&lt;li&gt;通信通常通过&lt;strong&gt;消息传递&lt;/strong&gt;完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消息传递&lt;/h3&gt;
&lt;p&gt;消息传递 API 至少提供两类函数：&lt;/p&gt;
&lt;p&gt;| 函数 | 作用 |
|---|---|
| &lt;code&gt;Send&lt;/code&gt; | 向另一个进程发送消息 |
| &lt;code&gt;Receive&lt;/code&gt; | 从另一个进程接收消息 |&lt;/p&gt;
&lt;p&gt;示意：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-11.Cgt--3Qe_1uayMz.webp&quot; alt=&quot;Diagram 11&quot;&gt;&lt;/p&gt;
&lt;h3&gt;rank&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;消息传递程序通常用&lt;strong&gt;序列号&lt;/strong&gt;或 &lt;strong&gt;rank&lt;/strong&gt; 标识进程。&lt;/li&gt;
&lt;li&gt;如果共有 &lt;code&gt;p&lt;/code&gt; 个进程，rank 通常为 &lt;code&gt;0, 1, ..., p-1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;发送和接收时需要指定通信对象的 rank。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SPMD&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;SPMD&lt;/strong&gt;：Single Program Multiple Data，单程序多数据。&lt;/p&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个进程运行同一个可执行文件。&lt;/li&gt;
&lt;li&gt;每个进程根据自己的 &lt;code&gt;rank&lt;/code&gt; 通过不同分支执行不同操作。&lt;/li&gt;
&lt;li&gt;每个进程处理不同数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;my_rank = Get_rank();

if (my_rank == 1) {
    /* 进程 1 发送消息 */
    Send(message, MSG_CHAR, 100, 0);
} else if (my_rank == 0) {
    /* 进程 0 接收消息 */
    Receive(message, MSG_CHAR, 100, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;同名变量不共享&lt;/h3&gt;
&lt;p&gt;在分布式内存中，不同进程中的同名变量属于不同内存块。&lt;/p&gt;
&lt;p&gt;例如两个进程中都声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char message[100];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是同一块内存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程 0 的 &lt;code&gt;message&lt;/code&gt; 位于进程 0 的私有内存。&lt;/li&gt;
&lt;li&gt;进程 1 的 &lt;code&gt;message&lt;/code&gt; 位于进程 1 的私有内存。&lt;/li&gt;
&lt;li&gt;若要把进程 1 的 &lt;code&gt;message&lt;/code&gt; 传给进程 0，必须显式发送消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心区别：&lt;/p&gt;
&lt;p&gt;| 模型 | 通信方式 |
|---|---|
| 共享内存 | 通过共享变量隐式通信 |
| 分布式内存 | 通过 &lt;code&gt;Send&lt;/code&gt; / &lt;code&gt;Receive&lt;/code&gt; 显式通信 |&lt;/p&gt;
&lt;h2&gt;2.5 输入和输出&lt;/h2&gt;
&lt;h3&gt;2.5.1 MIMD 系统&lt;/h3&gt;
&lt;p&gt;这里的 MIMD 不是在讲“做计算的模型”本身，而是在讲：&lt;strong&gt;多个独立进程/线程同时运行时，如何处理输入和输出&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;为什么 I/O 容易出问题&lt;/h3&gt;
&lt;p&gt;MIMD 程序中，多个进程/线程可能同时访问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stdin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stdout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stderr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;同一个文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输出顺序不确定，多个进程的输出可能交错。&lt;/li&gt;
&lt;li&gt;输入分配不明确：不知道哪个进程应该读哪个输入。&lt;/li&gt;
&lt;li&gt;多个进程同时写同一个文件，可能导致内容混乱或覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;标准输入输出约定&lt;/h3&gt;
&lt;p&gt;为避免混乱，书中采用以下规则：&lt;/p&gt;
&lt;p&gt;| 场景 | 约定 |
|---|---|
| 分布式内存程序 | 只有进程 0 访问 &lt;code&gt;stdin&lt;/code&gt; |
| 共享内存程序 | 只有主线程或线程 0 访问 &lt;code&gt;stdin&lt;/code&gt; |
| &lt;code&gt;stdout&lt;/code&gt; / &lt;code&gt;stderr&lt;/code&gt; | 所有进程/线程都可以访问 |
| 调试以外的正式输出 | 通常只让一个进程/线程输出最终结果 |
| 普通文件 | 通常只让一个进程/线程访问同一个文件 |&lt;/p&gt;
&lt;h3&gt;为什么通常让 0 号进程负责 I/O&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;进程 0 或线程 0 通常被当作“主控者”。&lt;/li&gt;
&lt;li&gt;可以避免多个进程同时读输入造成不确定性。&lt;/li&gt;
&lt;li&gt;可以先由 0 号进程读取输入，再通过消息传递或共享变量分发给其他进程/线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.6 性能&lt;/h2&gt;
&lt;p&gt;并行程序的主要目标通常是提高性能。评价性能时，常用指标是&lt;strong&gt;加速比&lt;/strong&gt;和&lt;strong&gt;效率&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2.6.1 MIMD 系统中的加速比和效率&lt;/h3&gt;
&lt;h3&gt;理想情况&lt;/h3&gt;
&lt;p&gt;如果一个问题可以被平均分给 &lt;code&gt;p&lt;/code&gt; 个核心，并且没有额外开销，那么：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;并行运行时间 = 串行运行时间 / p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种理想情况称为&lt;strong&gt;线性加速&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;线性加速比&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线性加速比&lt;/strong&gt;：使用 &lt;code&gt;p&lt;/code&gt; 个核心时，加速比达到 &lt;code&gt;p&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;p&gt;$$
S = p
$$&lt;/p&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 2 个核心，理想上快 2 倍。&lt;/li&gt;
&lt;li&gt;用 4 个核心，理想上快 4 倍。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;p&lt;/code&gt; 个核心，理想上快 &lt;code&gt;p&lt;/code&gt; 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际中很难完全达到线性加速，因为存在同步、通信、临界区、线程创建等开销。&lt;/p&gt;
&lt;h3&gt;加速比&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;加速比&lt;/strong&gt;（speedup）定义为：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{T_{\text{serial}}}{T_{\text{parallel}}}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;p&gt;| 符号 | 含义 |
|---|---|
| $T_{\text{serial}}$ | 串行程序运行时间 |
| $T_{\text{parallel}}$ | 并行程序运行时间 |
| $S$ | 并行程序相对串行程序快了多少倍 |&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;串行运行 &lt;code&gt;100s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;并行运行 &lt;code&gt;25s&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;则：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{100}{25} = 4
$$&lt;/p&gt;
&lt;p&gt;表示并行程序快了 4 倍。&lt;/p&gt;
&lt;h3&gt;效率&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;效率&lt;/strong&gt;（efficiency）衡量核心是否被充分利用。&lt;/p&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{S}{p}
$$&lt;/p&gt;
&lt;p&gt;把加速比公式代入：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{T_{\text{serial}}}{p \cdot T_{\text{parallel}}}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;p&gt;| 符号 | 含义 |
|---|---|
| $p$ | 使用的核心数或进程/线程数 |
| $S$ | 加速比 |
| $E$ | 效率 |&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$E = 1$：完全理想，所有核心都被充分利用。&lt;/li&gt;
&lt;li&gt;$E &amp;#x3C; 1$：存在开销，核心没有被完全利用。&lt;/li&gt;
&lt;li&gt;$E$ 越接近 1，说明并行化效果越好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问题规模对性能的影响&lt;/h3&gt;
&lt;p&gt;一般来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增加问题规模时，加速比和效率可能增加。&lt;/li&gt;
&lt;li&gt;减少问题规模时，加速比和效率可能降低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并行程序通常有固定开销，例如线程创建、通信、同步、数据分发和结果收集。&lt;/li&gt;
&lt;li&gt;问题规模较小时，固定开销占比大，并行收益容易被抵消。&lt;/li&gt;
&lt;li&gt;问题规模较大时，计算量增加，固定开销被摊薄，核心更容易保持忙碌。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么实际加速比会低于线性加速&lt;/h3&gt;
&lt;p&gt;常见原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负载不均衡：有些核心先完成后等待。&lt;/li&gt;
&lt;li&gt;通信开销：进程/线程之间需要交换数据。&lt;/li&gt;
&lt;li&gt;同步开销：临界区、互斥锁、屏障等会让部分核心等待。&lt;/li&gt;
&lt;li&gt;串行部分：程序中有些部分无法并行。&lt;/li&gt;
&lt;li&gt;额外开销：线程创建、数据分发、结果收集等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.6.2 阿姆达尔定律&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;阿姆达尔定律&lt;/strong&gt;（Amdahl&apos;s Law）：如果程序中有一部分必须串行执行，那么无论增加多少核心，整体加速比都会受到这部分串行代码的限制。&lt;/p&gt;
&lt;h3&gt;基本公式&lt;/h3&gt;
&lt;p&gt;设：&lt;/p&gt;
&lt;p&gt;| 符号 | 含义 |
|---|---|
| $p$ | 核心数 |
| $r$ | 程序中必须串行执行的比例 |
| $1-r$ | 程序中可以并行执行的比例 |&lt;/p&gt;
&lt;p&gt;并行运行时间近似为：&lt;/p&gt;
&lt;p&gt;$$
T_{\text{parallel}} = rT_{\text{serial}} + \frac{(1-r)T_{\text{serial}}}{p}
$$&lt;/p&gt;
&lt;p&gt;加速比为：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{1}{r + \frac{1-r}{p}}
$$&lt;/p&gt;
&lt;p&gt;当核心数 $p$ 趋近无穷大时：&lt;/p&gt;
&lt;p&gt;$$
S \leq \frac{1}{r}
$$&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;串行部分越大，最大加速比越低&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;例子：90% 可并行&lt;/h3&gt;
&lt;p&gt;假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;串行运行时间 $T_{\text{serial}} = 20$&lt;/li&gt;
&lt;li&gt;90% 可并行&lt;/li&gt;
&lt;li&gt;10% 必须串行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;则：&lt;/p&gt;
&lt;p&gt;$$
T_{\text{parallel}} = 0.9 \times \frac{20}{p} + 0.1 \times 20 = \frac{18}{p} + 2
$$&lt;/p&gt;
&lt;p&gt;加速比：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{20}{\frac{18}{p} + 2}
$$&lt;/p&gt;
&lt;p&gt;当 $p$ 很大时，$\frac{18}{p}$ 接近 0，所以：&lt;/p&gt;
&lt;p&gt;$$
S \leq \frac{20}{2} = 10
$$&lt;/p&gt;
&lt;p&gt;结论：即使并行部分做得很好，甚至使用很多核心，只要 10% 不能并行，最大加速比也不会超过 10。&lt;/p&gt;
&lt;h3&gt;核心结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;阿姆达尔定律强调：优化并行程序时，不能只看可并行部分，也要关注串行瓶颈。&lt;/li&gt;
&lt;li&gt;串行比例 $r$ 决定了理论最大加速比。&lt;/li&gt;
&lt;li&gt;如果串行部分是 $1/100$，最大加速比也只能接近 100。&lt;/li&gt;
&lt;li&gt;要获得更高加速比，需要减少串行部分，或增大问题规模，让并行部分占比更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;注意&lt;/h3&gt;
&lt;p&gt;阿姆达尔定律通常假设问题规模固定。实际中，随着核心数增加，我们也可能处理更大的问题，此时加速表现可能比固定规模分析更好。&lt;/p&gt;
&lt;h3&gt;2.6.3 可扩展性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;可扩展性&lt;/strong&gt;：描述并行程序在增加处理器或核心数量时，性能能否持续提升的能力。&lt;/p&gt;
&lt;p&gt;常用符号：&lt;/p&gt;
&lt;p&gt;| 符号 | 含义 |
|---|---|
| $n$ | 问题规模，也可近似看作串行总工作量 |
| $P$ | 处理器、线程或进程数量 |
| $T_{\text{serial}}$ | 串行运行时间，近似为 $n$ |
| $T_{\text{parallel}}$ | 并行运行时间 |
| $T_{\text{overhead}}$ | 并行额外开销，如通信、同步、线程创建、数据分发等 |
| $E$ | 并行效率 |&lt;/p&gt;
&lt;p&gt;并行时间可粗略写成：&lt;/p&gt;
&lt;p&gt;$$
T_{\text{parallel}} = \frac{T_{\text{serial}}}{P} + T_{\text{overhead}}
$$&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;并行时间 = 理想分摊后的计算时间 + 并行额外开销
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{S}{P}
$$&lt;/p&gt;
&lt;p&gt;且：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{T_{\text{serial}}}{T_{\text{parallel}}}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{T_{\text{serial}}}{P \cdot T_{\text{parallel}}}
$$&lt;/p&gt;
&lt;p&gt;如果令 $T_{\text{serial}} = n$，并且：&lt;/p&gt;
&lt;p&gt;$$
T_{\text{parallel}} = \frac{n}{P} + T_{\text{overhead}}
$$&lt;/p&gt;
&lt;p&gt;则：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{n}{P(\frac{n}{P} + T_{\text{overhead}})}
= \frac{n}{n + P T_{\text{overhead}}}
$$&lt;/p&gt;
&lt;p&gt;这个公式说明：如果问题规模 $n$ 不变，只增加处理器数 $P$，那么分母中的 $P T_{\text{overhead}}$ 会变大，效率 $E$ 通常会下降。&lt;/p&gt;
&lt;p&gt;可扩展性分为两类：&lt;/p&gt;
&lt;p&gt;| 类型 | 含义 | 关注点 |
|---|---|---|
| 强扩展性 | 固定问题规模，增加核心数 $p$，效率仍能基本保持不变 | 同一个问题能否靠更多核心跑得更快 |
| 弱扩展性 | 增加核心数 $p$ 的同时，按相同规模增加问题规模，效率仍能基本保持不变 | 核心更多时，能否处理更大的问题 |&lt;/p&gt;
&lt;h3&gt;强扩展性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;问题规模固定。&lt;/li&gt;
&lt;li&gt;增加核心数 $P$。&lt;/li&gt;
&lt;li&gt;如果效率 $E$ 能保持不变或下降很少，说明程序具有强扩展性。&lt;/li&gt;
&lt;li&gt;只有通信、同步等并行开销很小时，才容易获得好的强扩展性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：同样大小的矩阵乘法，从 4 个核心增加到 8 个核心，看运行时间是否接近减半。&lt;/p&gt;
&lt;p&gt;理想情况下没有额外开销：&lt;/p&gt;
&lt;p&gt;$$
T_{\text{parallel}} = \frac{T_{\text{serial}}}{P}
$$&lt;/p&gt;
&lt;p&gt;此时效率：&lt;/p&gt;
&lt;p&gt;$$
E = 1
$$&lt;/p&gt;
&lt;p&gt;现实中通常有额外开销：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{n}{n + P T_{\text{overhead}}}
$$&lt;/p&gt;
&lt;p&gt;因此当 $P$ 增大时，强扩展效率通常会下降。&lt;/p&gt;
&lt;h3&gt;弱扩展性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;增加核心数 $P$。&lt;/li&gt;
&lt;li&gt;同时按相同比例增加问题规模。&lt;/li&gt;
&lt;li&gt;如果效率 $E$ 能保持不变或下降很少，说明程序具有弱扩展性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：4 个核心处理 4GB 数据，8 个核心处理 8GB 数据。如果运行时间基本不变，说明弱扩展性较好。&lt;/p&gt;
&lt;p&gt;如果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;P -&gt; aP
n -&gt; bn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则效率近似变为：&lt;/p&gt;
&lt;p&gt;$$
E = \frac{bn}{bn + aP T_{\text{overhead}}}
$$&lt;/p&gt;
&lt;p&gt;如果 $a = b$，即核心数和问题规模按相同比例增加，那么效率有可能保持稳定。&lt;/p&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;强扩展性问的是：&lt;strong&gt;同一个问题，用更多核心能不能更快？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;弱扩展性问的是：&lt;strong&gt;更多核心能不能处理更大的问题，同时保持效率？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;并行程序不是核心越多越好，因为 $P$ 变大时，通信、同步等额外开销也会被放大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第 3 章 使用 MPI 进行分布式内存编程&lt;/h1&gt;
&lt;h2&gt;3.1 MPI 概述&lt;/h2&gt;
&lt;h3&gt;重点&lt;/h3&gt;
&lt;p&gt;本章重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通信
&lt;ul&gt;
&lt;li&gt;点对点通信&lt;/li&gt;
&lt;li&gt;集合通信&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;发送和接收消息的语义
&lt;ul&gt;
&lt;li&gt;是否阻塞&lt;/li&gt;
&lt;li&gt;消息匹配&lt;/li&gt;
&lt;li&gt;消息顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI 是什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MPI&lt;/strong&gt;：Message Passing Interface，消息传递接口。&lt;/li&gt;
&lt;li&gt;MPI 用于&lt;strong&gt;分布式内存编程&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;MPI 程序的基本执行单位通常是&lt;strong&gt;进程&lt;/strong&gt;，不是线程。&lt;/li&gt;
&lt;li&gt;MPI 实际上是一个&lt;strong&gt;函数库&lt;/strong&gt;，不是一门新的编程语言。&lt;/li&gt;
&lt;li&gt;进程之间通过显式发送和接收消息来通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.2 编译与执行&lt;/h2&gt;
&lt;h3&gt;编译&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;mpicc&lt;/code&gt; 编译 MPI 程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mpicc -o hello hello.c
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;mpiexec&lt;/code&gt; 启动多个 MPI 进程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mpiexec -n 4 ./hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-n 4&lt;/code&gt;：启动 4 个进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./hello&lt;/code&gt;：每个进程运行同一个可执行文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这体现了 MPI 常见的 &lt;strong&gt;SPMD&lt;/strong&gt; 模型：多个进程运行同一个程序，但根据自己的 rank 执行不同逻辑。&lt;/p&gt;
&lt;h2&gt;3.3 MPI 初始化和结束&lt;/h2&gt;
&lt;h3&gt;MPI_Init&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Init&lt;/code&gt; 必须在所有其他 MPI 函数调用之前执行。&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化 MPI 运行环境。&lt;/li&gt;
&lt;li&gt;建立通信环境。&lt;/li&gt;
&lt;li&gt;通常可以理解为所有 MPI 进程进入 MPI 环境的起点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Init(int *argc, char ***argv);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MPI_Finalize&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Finalize&lt;/code&gt; 用于关闭 MPI 环境并清理资源。&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Finalize(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基本程序结构&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;mpi.h&gt;

int main(int argc, char* argv[]) {
    MPI_Init(&amp;#x26;argc, &amp;#x26;argv);

    /* MPI 程序主体 */

    MPI_Finalize();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：实际函数名是 &lt;code&gt;MPI_Init&lt;/code&gt; 和 &lt;code&gt;MPI_Finalize&lt;/code&gt;，大小写要一致。&lt;/p&gt;
&lt;h2&gt;3.4 通信子、进程数和 rank&lt;/h2&gt;
&lt;h3&gt;通信子&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;通信子&lt;/strong&gt;（communicator）：一组可以互相发送消息的进程集合。&lt;/p&gt;
&lt;p&gt;常用通信子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_COMM_WORLD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：由 &lt;code&gt;MPI_Init&lt;/code&gt; 创建，包含用户启动的所有 MPI 进程。&lt;/p&gt;
&lt;h3&gt;MPI_Comm_size&lt;/h3&gt;
&lt;p&gt;获取通信子中的进程总数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Comm_size(MPI_Comm comm, int *size);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int size;
MPI_Comm_size(MPI_COMM_WORLD, &amp;#x26;size);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MPI_Comm_rank&lt;/h3&gt;
&lt;p&gt;获取当前进程在通信子中的编号，即 rank。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Comm_rank(MPI_Comm comm, int *rank);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &amp;#x26;rank);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;rank 的作用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;rank 用于区分不同进程。&lt;/li&gt;
&lt;li&gt;rank 常用于决定每个进程执行什么任务。&lt;/li&gt;
&lt;li&gt;rank 也用于指定消息发送目标或消息来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.5 MPI_Send&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MPI_Send&lt;/code&gt; 用于点对点发送消息。&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Send(
    void *buf,
    int count,
    MPI_Datatype datatype,
    int dest,
    int tag,
    MPI_Comm comm
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;buf&lt;/code&gt; | 要发送的数据起始地址 |
| &lt;code&gt;count&lt;/code&gt; | 要发送的数据项个数，不是字节数 |
| &lt;code&gt;datatype&lt;/code&gt; | MPI 数据类型，如 &lt;code&gt;MPI_INT&lt;/code&gt;、&lt;code&gt;MPI_FLOAT&lt;/code&gt;、&lt;code&gt;MPI_CHAR&lt;/code&gt; |
| &lt;code&gt;dest&lt;/code&gt; | 目标进程的 rank |
| &lt;code&gt;tag&lt;/code&gt; | 消息标签，用于区分不同消息 |
| &lt;code&gt;comm&lt;/code&gt; | 通信子，常用 &lt;code&gt;MPI_COMM_WORLD&lt;/code&gt; |&lt;/p&gt;
&lt;h3&gt;tag 的作用&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tag&lt;/code&gt; 用于区分同一对进程之间的不同消息。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一部分浮点数用于加法，使用 &lt;code&gt;tag = 0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;一部分浮点数用于减法，使用 &lt;code&gt;tag = 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接收方可以通过 tag 判断收到的消息应该如何处理。&lt;/p&gt;
&lt;h2&gt;3.6 MPI_Recv&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MPI_Recv&lt;/code&gt; 用于点对点接收消息。&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Recv(
    void *buf,
    int count,
    MPI_Datatype datatype,
    int source,
    int tag,
    MPI_Comm comm,
    MPI_Status *status
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;buf&lt;/code&gt; | 接收缓冲区起始地址 |
| &lt;code&gt;count&lt;/code&gt; | 最多接收的数据项数量，不是字节数 |
| &lt;code&gt;datatype&lt;/code&gt; | MPI 数据类型，如 &lt;code&gt;MPI_INT&lt;/code&gt;、&lt;code&gt;MPI_FLOAT&lt;/code&gt; |
| &lt;code&gt;source&lt;/code&gt; | 消息来源进程的 rank，可用 &lt;code&gt;MPI_ANY_SOURCE&lt;/code&gt; |
| &lt;code&gt;tag&lt;/code&gt; | 消息标签，可用 &lt;code&gt;MPI_ANY_TAG&lt;/code&gt; |
| &lt;code&gt;comm&lt;/code&gt; | 通信子，常用 &lt;code&gt;MPI_COMM_WORLD&lt;/code&gt; |
| &lt;code&gt;status&lt;/code&gt; | 返回消息状态，也可用 &lt;code&gt;MPI_STATUS_IGNORE&lt;/code&gt; 忽略 |&lt;/p&gt;
&lt;h3&gt;MPI_Status&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Status&lt;/code&gt; 至少包含以下常用信息：&lt;/p&gt;
&lt;p&gt;| 字段 | 含义 |
|---|---|
| &lt;code&gt;MPI_SOURCE&lt;/code&gt; | 实际发送消息的进程 rank |
| &lt;code&gt;MPI_TAG&lt;/code&gt; | 实际接收消息的 tag |
| &lt;code&gt;MPI_ERROR&lt;/code&gt; | 错误码 |&lt;/p&gt;
&lt;p&gt;如果接收方事先不知道实际收到了多少个数据项，可以用 &lt;code&gt;MPI_Get_count&lt;/code&gt; 从 &lt;code&gt;status&lt;/code&gt; 中查询。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Get_count(
    MPI_Status *status_p,
    MPI_Datatype datatype,
    int *count_p
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status_p&lt;/code&gt;：某次 &lt;code&gt;MPI_Recv&lt;/code&gt; 返回的状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;datatype&lt;/code&gt;：接收时使用的数据类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count_p&lt;/code&gt;：输出参数，保存实际接收到的数据项个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用 MPI 数据类型&lt;/h3&gt;
&lt;p&gt;MPI 通信时必须说明数据类型。&lt;code&gt;count&lt;/code&gt; 表示数据项个数，&lt;code&gt;datatype&lt;/code&gt; 说明每个数据项是什么类型。&lt;/p&gt;
&lt;p&gt;| MPI 类型 | C 类型 |
|---|---|
| &lt;code&gt;MPI_CHAR&lt;/code&gt; | &lt;code&gt;char&lt;/code&gt; |
| &lt;code&gt;MPI_SHORT&lt;/code&gt; | &lt;code&gt;short&lt;/code&gt; |
| &lt;code&gt;MPI_INT&lt;/code&gt; | &lt;code&gt;int&lt;/code&gt; |
| &lt;code&gt;MPI_LONG&lt;/code&gt; | &lt;code&gt;long&lt;/code&gt; |
| &lt;code&gt;MPI_LONG_LONG&lt;/code&gt; | &lt;code&gt;long long&lt;/code&gt; |
| &lt;code&gt;MPI_UNSIGNED&lt;/code&gt; | &lt;code&gt;unsigned int&lt;/code&gt; |
| &lt;code&gt;MPI_FLOAT&lt;/code&gt; | &lt;code&gt;float&lt;/code&gt; |
| &lt;code&gt;MPI_DOUBLE&lt;/code&gt; | &lt;code&gt;double&lt;/code&gt; |
| &lt;code&gt;MPI_LONG_DOUBLE&lt;/code&gt; | &lt;code&gt;long double&lt;/code&gt; |
| &lt;code&gt;MPI_BYTE&lt;/code&gt; | 原始字节 |&lt;/p&gt;
&lt;h2&gt;3.7 消息匹配&lt;/h2&gt;
&lt;p&gt;一个 &lt;code&gt;MPI_Recv&lt;/code&gt; 能否匹配某个 &lt;code&gt;MPI_Send&lt;/code&gt;，主要看三项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;source == 发送方 rank
recv_tag == send_tag
recv_comm == send_comm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收类型和发送类型匹配。&lt;/li&gt;
&lt;li&gt;接收缓冲区足够大，即 &lt;code&gt;recv_buff_sz &gt;= send_buff_sz&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;通配符&lt;/h3&gt;
&lt;p&gt;MPI 提供两个接收方通配符：&lt;/p&gt;
&lt;p&gt;| 通配符 | 含义 |
|---|---|
| &lt;code&gt;MPI_ANY_SOURCE&lt;/code&gt; | 接收任意来源进程的消息 |
| &lt;code&gt;MPI_ANY_TAG&lt;/code&gt; | 接收任意 tag 的消息 |&lt;/p&gt;
&lt;p&gt;注意：通配符只能在接收方使用。&lt;/p&gt;
&lt;h2&gt;3.8 MPI_Send 和 MPI_Recv 的阻塞语义&lt;/h2&gt;
&lt;h3&gt;MPI_Recv&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Recv&lt;/code&gt; 一定是&lt;strong&gt;阻塞&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果匹配的消息还没到达，&lt;code&gt;MPI_Recv&lt;/code&gt; 会一直等待。&lt;/li&gt;
&lt;li&gt;除非出错或被取消，否则进程会挂起在接收调用上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI_Send&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Send&lt;/code&gt; 是否阻塞取决于消息大小和 MPI 实现。&lt;/p&gt;
&lt;p&gt;常见情况：&lt;/p&gt;
&lt;p&gt;| 情况 | 行为 |
|---|---|
| 消息较小 | MPI 可能复制到内部缓冲区，然后立即返回 |
| 消息较大 | 如果接收方还没准备好，发送方可能阻塞 |&lt;/p&gt;
&lt;p&gt;所以：&lt;code&gt;MPI_Send&lt;/code&gt; 可能立即返回，也可能阻塞。&lt;/p&gt;
&lt;p&gt;常见实现里有两种发送机制：&lt;/p&gt;
&lt;p&gt;| 机制 | 典型场景 | 行为 |
|---|---|---|
| eager 模式 | 消息较小 | MPI 先把消息复制到内部缓冲区，&lt;code&gt;MPI_Send&lt;/code&gt; 可能很快返回 |
| rendezvous 模式 | 消息较大 | 发送方通常要等接收方开始匹配接收，&lt;code&gt;MPI_Send&lt;/code&gt; 可能阻塞 |&lt;/p&gt;
&lt;p&gt;所以不能把普通 &lt;code&gt;MPI_Send&lt;/code&gt; 简单理解为“一定阻塞”或“一定非阻塞”。它的返回行为和消息大小、MPI 实现、接收方状态都有关。&lt;/p&gt;
&lt;h3&gt;消息不可超越&lt;/h3&gt;
&lt;p&gt;MPI 要求同一发送进程连续发送给同一接收进程的消息不能乱序。&lt;/p&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个进程连续发送的几条消息，必须按发送顺序到达。&lt;/li&gt;
&lt;li&gt;不同发送进程之间的消息没有这个顺序保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;进程 1 连续发送 A、B 给进程 0
进程 0 不会先收到 B 再收到 A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;进程 1 发送 A
进程 2 发送 B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进程 0 可能先收到 A，也可能先收到 B。&lt;/p&gt;
&lt;h3&gt;MPI_Ssend&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Ssend&lt;/code&gt; 是&lt;strong&gt;同步发送&lt;/strong&gt;（synchronous send）。&lt;/p&gt;
&lt;p&gt;额外的 &lt;code&gt;s&lt;/code&gt; 表示 synchronous。它和 &lt;code&gt;MPI_Send&lt;/code&gt; 的参数相同，但语义更严格：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MPI_Ssend&lt;/code&gt; 保证阻塞。&lt;/li&gt;
&lt;li&gt;只有当匹配的接收操作已经开始后，&lt;code&gt;MPI_Ssend&lt;/code&gt; 才能返回。&lt;/li&gt;
&lt;li&gt;它不会像普通 &lt;code&gt;MPI_Send&lt;/code&gt; 那样因为消息较小、被复制到内部缓冲区就提前返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Ssend(
    void *msg_buf_p,
    int msg_size,
    MPI_Datatype msg_type,
    int dest,
    int tag,
    MPI_Comm communicator
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 &lt;code&gt;MPI_Send&lt;/code&gt; 的关系：&lt;/p&gt;
&lt;p&gt;| 函数 | 是否一定阻塞 | 返回条件 |
|---|---|---|
| &lt;code&gt;MPI_Send&lt;/code&gt; | 不一定 | 可能复制到缓冲区后立即返回，也可能等待接收方 |
| &lt;code&gt;MPI_Ssend&lt;/code&gt; | 是 | 必须等匹配接收开始后才能返回 |&lt;/p&gt;
&lt;p&gt;用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以用来检查程序通信顺序是否安全。&lt;/li&gt;
&lt;li&gt;如果把 &lt;code&gt;MPI_Send&lt;/code&gt; 换成 &lt;code&gt;MPI_Ssend&lt;/code&gt; 后程序挂起或崩溃，说明原程序可能依赖了 MPI 的内部缓冲区。&lt;/li&gt;
&lt;li&gt;若使用 &lt;code&gt;MPI_Ssend&lt;/code&gt; 后程序仍能正常运行，通常说明原来的点对点通信顺序更安全。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MPI_Ssend&lt;/code&gt; 的参数和 &lt;code&gt;MPI_Send&lt;/code&gt; 相同，所以常见调试方式是直接把 &lt;code&gt;MPI_Send&lt;/code&gt; 临时替换成 &lt;code&gt;MPI_Ssend&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.9 集合通信&lt;/h2&gt;
&lt;p&gt;集合通信是多个进程共同参与的通信操作。和点对点通信不同，集合通信通常要求通信子中的所有相关进程都调用同一个集合通信函数。&lt;/p&gt;
&lt;h3&gt;MPI_Reduce&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Reduce&lt;/code&gt; 用于把多个进程的数据按某种操作归约，并把结果存到指定的 root 进程。&lt;/p&gt;
&lt;p&gt;常见用途：求和、求最大值、求最小值等。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Reduce(
    &amp;#x26;local_sum,     /* 每个进程提供的本地数据 */
    &amp;#x26;total_sum,     /* 归约结果，只在 root 进程有效 */
    1,              /* 数据个数，这里是 1 个 double */
    MPI_DOUBLE,     /* 数据类型 */
    MPI_SUM,        /* 归约操作：求和 */
    0,              /* root 进程编号，结果放到 rank 0 */
    MPI_COMM_WORLD  /* 通信子 */
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;sendbuf&lt;/code&gt; | 每个进程提供的本地数据地址 |
| &lt;code&gt;recvbuf&lt;/code&gt; | 归约结果存放地址，只在 root 进程有效 |
| &lt;code&gt;count&lt;/code&gt; | 数据项个数 |
| &lt;code&gt;datatype&lt;/code&gt; | 数据类型，如 &lt;code&gt;MPI_DOUBLE&lt;/code&gt; |
| &lt;code&gt;op&lt;/code&gt; | 归约操作，如 &lt;code&gt;MPI_SUM&lt;/code&gt; |
| &lt;code&gt;root&lt;/code&gt; | 接收最终结果的进程 rank |
| &lt;code&gt;comm&lt;/code&gt; | 通信子 |&lt;/p&gt;
&lt;h3&gt;MPI 预定义归约算子&lt;/h3&gt;
&lt;p&gt;| 算子 | 含义 |
|---|---|
| &lt;code&gt;MPI_MAX&lt;/code&gt; | 最大值 |
| &lt;code&gt;MPI_MIN&lt;/code&gt; | 最小值 |
| &lt;code&gt;MPI_SUM&lt;/code&gt; | 总和 |
| &lt;code&gt;MPI_PROD&lt;/code&gt; | 乘积 |
| &lt;code&gt;MPI_LAND&lt;/code&gt; | 逻辑与 |
| &lt;code&gt;MPI_BAND&lt;/code&gt; | 按位与 |
| &lt;code&gt;MPI_LOR&lt;/code&gt; | 逻辑或 |
| &lt;code&gt;MPI_BOR&lt;/code&gt; | 按位或 |
| &lt;code&gt;MPI_LXOR&lt;/code&gt; | 逻辑异或 |
| &lt;code&gt;MPI_BXOR&lt;/code&gt; | 按位异或 |
| &lt;code&gt;MPI_MAXLOC&lt;/code&gt; | 最大值以及最大值所在位置 |
| &lt;code&gt;MPI_MINLOC&lt;/code&gt; | 最小值以及最小值所在位置 |&lt;/p&gt;
&lt;p&gt;例子：4 个进程分别有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rank 0: local_sum = 1.5
rank 1: local_sum = 2.0
rank 2: local_sum = 3.5
rank 3: local_sum = 4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;MPI_Reduce&lt;/code&gt; 后，只有 &lt;code&gt;rank 0&lt;/code&gt; 上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;total_sum = 1.5 + 2.0 + 3.5 + 4.0;  /* 11.0 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他进程上的 &lt;code&gt;total_sum&lt;/code&gt; 不保存最终归约结果。&lt;/p&gt;
&lt;h3&gt;树形结构通信&lt;/h3&gt;
&lt;p&gt;集合通信指的是&lt;strong&gt;通信子中所有进程共同参与的通信函数&lt;/strong&gt;。&lt;code&gt;MPI_Send&lt;/code&gt; 和 &lt;code&gt;MPI_Recv&lt;/code&gt; 是点对点通信，不属于集合通信。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MPI_Reduce&lt;/code&gt; 内部通常不会简单地让所有进程都直接把结果发给 0 号进程，因为这样 root 进程需要接收和计算太多次。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;comm_sz = 1024&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单集中到 0 号进程：0 号进程需要接收并计算 &lt;code&gt;1023&lt;/code&gt; 次。&lt;/li&gt;
&lt;li&gt;树形结构归约：大约只需要 &lt;code&gt;log2(1024) = 10&lt;/code&gt; 轮通信和计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;树形归约示意：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-12._glVV1Ja_FRdKR.webp&quot; alt=&quot;Diagram 12&quot;&gt;
核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一轮把相邻进程或局部结果两两合并。&lt;/li&gt;
&lt;li&gt;合并后的部分和继续向上归约。&lt;/li&gt;
&lt;li&gt;最终结果到达 root 进程。&lt;/li&gt;
&lt;li&gt;这样能减少 root 进程的通信压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Reduce&lt;/code&gt; 和 &lt;code&gt;Scan&lt;/code&gt; 的数据流对比：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-collectives-reduce-scan.TAeOZolj_ZRdprY.webp&quot; alt=&quot;HPC MPI collectives reduce scan&quot;&gt;&lt;/p&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MPI_Reduce&lt;/code&gt;：把所有进程的数据归约到一个 root 进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MPI_Scan&lt;/code&gt;：做前缀归约，每个进程得到从 &lt;code&gt;rank 0&lt;/code&gt; 到自己这一段的部分归约结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集合通信与点对点通信的区别&lt;/h3&gt;
&lt;p&gt;集合通信和点对点通信的调用规则不同，不能把它们当成一类操作来匹配。&lt;/p&gt;
&lt;h4&gt;1. 所有进程必须调用同一个集合通信函数&lt;/h4&gt;
&lt;p&gt;通信子中的所有进程==必须调用相同的集合通信函数==。&lt;/p&gt;
&lt;p&gt;错误例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rank 0 调用 MPI_Reduce
rank 1 调用 MPI_Recv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种程序是错误的，可能导致挂起或崩溃。&lt;/p&gt;
&lt;h4&gt;2. 集合通信参数必须兼容&lt;/h4&gt;
&lt;p&gt;每个进程传给集合通信的关键参数必须一致或兼容。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;MPI_Reduce&lt;/code&gt; 为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有进程必须使用相同的通信子。&lt;/li&gt;
&lt;li&gt;所有进程必须使用相同的归约操作。&lt;/li&gt;
&lt;li&gt;所有进程必须指定相同的 root。&lt;/li&gt;
&lt;li&gt;数据类型和数据个数必须匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果一个进程把 root 设为 &lt;code&gt;0&lt;/code&gt;，另一个进程把 root 设为 &lt;code&gt;1&lt;/code&gt;，程序就是错误的。&lt;/p&gt;
&lt;h4&gt;3. 非 root 进程也要传入对应参数&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;MPI_Reduce&lt;/code&gt; 中，接收缓冲区 &lt;code&gt;recvbuf&lt;/code&gt; 只在 root 进程上真正使用。&lt;/p&gt;
&lt;p&gt;但所有进程仍然需要调用同一个函数，并传入对应位置的参数。非 root 进程上的 &lt;code&gt;recvbuf&lt;/code&gt; 不保存最终结果，通常可以理解为无效或被忽略。&lt;/p&gt;
&lt;h4&gt;4. 集合通信不靠 tag 匹配&lt;/h4&gt;
&lt;p&gt;点对点通信通过以下信息匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;source
tag
comm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集合通信不使用 tag。它的匹配主要由以下因素决定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通信子 &lt;code&gt;comm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用的集合函数类型&lt;/li&gt;
&lt;li&gt;调用顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，如果同一个通信子里连续调用两次 &lt;code&gt;MPI_Reduce&lt;/code&gt;，MPI 会把所有进程的第一次 &lt;code&gt;MPI_Reduce&lt;/code&gt; 匹配在一起，把第二次 &lt;code&gt;MPI_Reduce&lt;/code&gt; 匹配在一起。&lt;/p&gt;
&lt;h3&gt;多次 MPI_Reduce 调用示例&lt;/h3&gt;
&lt;p&gt;假设所有进程都执行两次 &lt;code&gt;MPI_Reduce&lt;/code&gt;，并且通信子相同：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Reduce(&amp;#x26;a, &amp;#x26;b, 1, MPI_INT, MPI_SUM, 0, comm);
MPI_Reduce(&amp;#x26;c, &amp;#x26;d, 1, MPI_INT, MPI_SUM, 0, comm);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次调用只会和其他进程的第一次调用匹配。&lt;/li&gt;
&lt;li&gt;第二次调用只会和其他进程的第二次调用匹配。&lt;/li&gt;
&lt;li&gt;即使变量名不同，MPI 也不根据变量名匹配，只根据集合通信调用顺序匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以集合通信中，&lt;strong&gt;所有进程必须以相同顺序调用相同的集合通信函数&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;不要混用输入和输出缓冲区&lt;/h3&gt;
&lt;p&gt;对于 &lt;code&gt;MPI_Reduce&lt;/code&gt;，通常不应该把发送缓冲区和接收缓冲区写成同一个地址，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Reduce(&amp;#x26;x, &amp;#x26;x, 1, MPI_DOUBLE, MPI_SUM, 0, comm);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法可能导致结果不可预测，甚至是非法的。&lt;/p&gt;
&lt;p&gt;更安全的写法是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Reduce(&amp;#x26;local_x, &amp;#x26;global_x, 1, MPI_DOUBLE, MPI_SUM, 0, comm);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;local_x&lt;/code&gt;：每个进程自己的输入数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global_x&lt;/code&gt;：root 进程保存归约结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI_Bcast&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Bcast&lt;/code&gt; 用于&lt;strong&gt;广播&lt;/strong&gt;：把 root 进程上的一份数据发送给通信子中的所有进程。&lt;/p&gt;
&lt;p&gt;它可以看作是树形归约通信方向的“反向操作”：归约是把多个进程的数据汇总到 root，广播是把 root 的数据分发给所有进程。&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Bcast(
    void *data_p,
    int count,
    MPI_Datatype datatype,
    int source_proc,
    MPI_Comm comm
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;data_p&lt;/code&gt; | 数据缓冲区；root 上是输入，其他进程上是输出 |
| &lt;code&gt;count&lt;/code&gt; | 数据项个数 |
| &lt;code&gt;datatype&lt;/code&gt; | 数据类型 |
| &lt;code&gt;source_proc&lt;/code&gt; | root 进程 rank，即数据来源 |
| &lt;code&gt;comm&lt;/code&gt; | 通信子 |&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Bcast(a_p, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
MPI_Bcast(b_p, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
MPI_Bcast(n_p, 1, MPI_INT,    0, MPI_COMM_WORLD);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rank 0 上的 &lt;code&gt;a_p&lt;/code&gt;、&lt;code&gt;b_p&lt;/code&gt;、&lt;code&gt;n_p&lt;/code&gt; 是源数据。&lt;/li&gt;
&lt;li&gt;调用后，通信子中所有进程都得到相同的 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;树形广播示意：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-bcast-tree.CBEMg4jf_1CEnSO.webp&quot; alt=&quot;HPC MPI bcast tree&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MPI_Bcast&lt;/code&gt; 是集合通信，通信子中的所有进程都必须调用。&lt;/li&gt;
&lt;li&gt;所有进程传入的 &lt;code&gt;count&lt;/code&gt;、&lt;code&gt;datatype&lt;/code&gt;、&lt;code&gt;source_proc&lt;/code&gt;、&lt;code&gt;comm&lt;/code&gt; 必须匹配。&lt;/li&gt;
&lt;li&gt;与手写多次 &lt;code&gt;MPI_Send&lt;/code&gt; 相比，&lt;code&gt;MPI_Bcast&lt;/code&gt; 通常更简洁，也可能由 MPI 库用树形结构优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI_Scatter&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Scatter&lt;/code&gt; 用于&lt;strong&gt;分散&lt;/strong&gt;：root 进程把一整块数据切成多段，分别发送给通信子中的各个进程。&lt;/p&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;root 上的大数组 -&gt; 每个进程拿到其中一小段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;典型用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root 进程读入完整向量。&lt;/li&gt;
&lt;li&gt;每个进程只处理自己负责的子向量。&lt;/li&gt;
&lt;li&gt;避免把完整向量复制到所有进程，节省内存和通信量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Scatter(
    void *send_buf_p,
    int send_count,
    MPI_Datatype send_type,
    void *recv_buf_p,
    int recv_count,
    MPI_Datatype recv_type,
    int src_proc,
    MPI_Comm comm
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点参数：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;send_buf_p&lt;/code&gt; | root 进程上的完整发送缓冲区 |
| &lt;code&gt;send_count&lt;/code&gt; | 发送给&lt;strong&gt;每个进程&lt;/strong&gt;的数据项个数 |
| &lt;code&gt;recv_buf_p&lt;/code&gt; | 每个进程接收自己那一段数据的缓冲区 |
| &lt;code&gt;recv_count&lt;/code&gt; | 每个进程实际接收的数据项个数 |
| &lt;code&gt;src_proc&lt;/code&gt; | root 进程 rank |&lt;/p&gt;
&lt;p&gt;注意：&lt;code&gt;send_count&lt;/code&gt; 不是 &lt;code&gt;send_buf_p&lt;/code&gt; 中的总数据量，而是&lt;strong&gt;每个进程收到多少个元素&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果通信子中有 &lt;code&gt;comm_sz&lt;/code&gt; 个进程，&lt;code&gt;MPI_Scatter&lt;/code&gt; 会把 &lt;code&gt;send_buf_p&lt;/code&gt; 指向的数据分成 &lt;code&gt;comm_sz&lt;/code&gt; 个片段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;第 1 个片段 -&gt; rank 0
第 2 个片段 -&gt; rank 1
第 3 个片段 -&gt; rank 2
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 root 上的发送缓冲区至少需要容纳：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;send_count * comm_sz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;个元素。&lt;/p&gt;
&lt;p&gt;例子：有 &lt;code&gt;comm_sz&lt;/code&gt; 个进程，向量总长度为 &lt;code&gt;n&lt;/code&gt;，则每个进程收到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;local_n = n / comm_sz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用时通常类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Scatter(x, local_n, MPI_DOUBLE,
            local_x, local_n, MPI_DOUBLE,
            0, MPI_COMM_WORLD);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MPI_Bcast&lt;/code&gt;、&lt;code&gt;MPI_Scatter&lt;/code&gt; 和 &lt;code&gt;MPI_Gather&lt;/code&gt; 的数据方向对比：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-collectives-bcast-scatter-gather.B75AGwoQ_1NQKna.webp&quot; alt=&quot;HPC MPI collectives bcast scatter gather&quot;&gt;&lt;/p&gt;
&lt;h3&gt;MPI_Gather&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Gather&lt;/code&gt; 用于&lt;strong&gt;收集&lt;/strong&gt;：每个进程把自己的一段数据发送给 root，root 把这些片段按 rank 顺序拼成完整数据。&lt;/p&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;每个进程的一小段 -&gt; root 上的大数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它和 &lt;code&gt;MPI_Scatter&lt;/code&gt; 方向相反：&lt;/p&gt;
&lt;p&gt;| 函数 | 数据方向 |
|---|---|
| &lt;code&gt;MPI_Scatter&lt;/code&gt; | root 分发给所有进程 |
| &lt;code&gt;MPI_Gather&lt;/code&gt; | 所有进程收集到 root |&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Gather(
    void *send_buf_p,
    int send_count,
    MPI_Datatype send_type,
    void *recv_buf_p,
    int recv_count,
    MPI_Datatype recv_type,
    int dest_proc,
    MPI_Comm comm
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点参数：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;send_buf_p&lt;/code&gt; | 每个进程要发送的本地数据 |
| &lt;code&gt;send_count&lt;/code&gt; | 每个进程发送的数据项个数 |
| &lt;code&gt;recv_buf_p&lt;/code&gt; | root 进程上的接收缓冲区，用来存放完整结果 |
| &lt;code&gt;recv_count&lt;/code&gt; | root 从&lt;strong&gt;每个进程&lt;/strong&gt;接收的数据项个数 |
| &lt;code&gt;dest_proc&lt;/code&gt; | root 进程 rank，即结果收集到哪里 |&lt;/p&gt;
&lt;p&gt;注意：&lt;code&gt;recv_count&lt;/code&gt; 是从每个进程接收多少项，不是 root 最终接收到的总项数。最终总量通常是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;recv_count * comm_sz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;root 上的 &lt;code&gt;recv_buf_p&lt;/code&gt; 会按进程 rank 顺序存放结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rank 0 的数据 -&gt; 第 1 块
rank 1 的数据 -&gt; 第 2 块
rank 2 的数据 -&gt; 第 3 块
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MPI_Gather&lt;/code&gt; 和 &lt;code&gt;MPI_Scatter&lt;/code&gt; 的重点都是 &lt;code&gt;count&lt;/code&gt; 参数的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MPI_Scatter&lt;/code&gt; 的 &lt;code&gt;send_count&lt;/code&gt;：root 发给&lt;strong&gt;每个进程&lt;/strong&gt;多少个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MPI_Gather&lt;/code&gt; 的 &lt;code&gt;recv_count&lt;/code&gt;：root 从&lt;strong&gt;每个进程&lt;/strong&gt;收多少个元素。&lt;/li&gt;
&lt;li&gt;它们都不是 root 缓冲区里的总元素数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI_Allgather&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Allgather&lt;/code&gt; 用于&lt;strong&gt;全局聚集&lt;/strong&gt;：每个进程发送一段数据，并且每个进程最终都得到所有进程的数据。&lt;/p&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MPI_Allgather = MPI_Gather + MPI_Bcast
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，它先把各进程的数据聚集成完整结果，再让所有进程都拥有这份完整结果。实际 MPI 实现不一定真的分两步执行，但语义上可以这样理解。&lt;/p&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;p&gt;| 函数 | 谁得到完整结果 |
|---|---|
| &lt;code&gt;MPI_Gather&lt;/code&gt; | 只有 root 进程 |
| &lt;code&gt;MPI_Allgather&lt;/code&gt; | 所有进程 |&lt;/p&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Allgather(
    void *send_buf_p,
    int send_count,
    MPI_Datatype send_type,
    void *recv_buf_p,
    int recv_count,
    MPI_Datatype recv_type,
    MPI_Comm comm
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 &lt;code&gt;MPI_Gather&lt;/code&gt; 相比，&lt;code&gt;MPI_Allgather&lt;/code&gt; &lt;strong&gt;没有 root 参数&lt;/strong&gt;，因为所有进程都会得到完整结果。&lt;/p&gt;
&lt;p&gt;例子：4 个进程中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sendval = rank + 1;
int recvbuf[4];

MPI_Allgather(&amp;#x26;sendval, 1, MPI_INT,
              recvbuf, 1, MPI_INT,
              MPI_COMM_WORLD);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后，每个进程的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;recvbuf = [1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有进程都要提供 &lt;code&gt;recvbuf&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recvbuf&lt;/code&gt; 大小通常至少是 &lt;code&gt;recv_count * comm_sz&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;和 &lt;code&gt;MPI_Gather&lt;/code&gt; 不同，&lt;code&gt;MPI_Allgather&lt;/code&gt; 没有 &lt;code&gt;dest_proc/root&lt;/code&gt; 参数，因为结果不是只发给某一个进程，而是发给所有进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MPI_Allgather&lt;/code&gt; 和 &lt;code&gt;MPI_Alltoall&lt;/code&gt; 的语义对比：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-collectives-allgather-alltoall.DAfr_8IU_ZNa081.webp&quot; alt=&quot;HPC MPI collectives allgather alltoall&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MPI_Allgather&lt;/code&gt; 的常见实现之一是&lt;strong&gt;环算法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-allgather-ring.BKqXJbcj_Z1BiLqp.webp&quot; alt=&quot;HPC MPI allgather ring&quot;&gt;&lt;/p&gt;
&lt;p&gt;环算法的直观过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程一开始有自己的本地数据。&lt;/li&gt;
&lt;li&gt;第 0 轮发送自己的数据。&lt;/li&gt;
&lt;li&gt;之后每一轮把上一轮收到的数据继续传给右侧进程，同时从左侧进程接收新数据。&lt;/li&gt;
&lt;li&gt;经过 &lt;code&gt;p - 1&lt;/code&gt; 轮后，每个进程都拥有所有进程的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另一类实现是递归倍增/蝶式通信：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-allgather-recursive-doubling.zDqD4Y4o_M920M.webp&quot; alt=&quot;HPC MPI allgather recursive doubling&quot;&gt;&lt;/p&gt;
&lt;p&gt;它每一轮和更远距离的伙伴进程交换已有数据，轮数约为 &lt;code&gt;log2(p)&lt;/code&gt;。实际 MPI 库会根据消息大小、进程数和网络结构选择实现方式。&lt;/p&gt;
&lt;h3&gt;MPI_Allreduce&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Allreduce&lt;/code&gt; 会把归约结果发送给通信子中的所有进程。&lt;/p&gt;
&lt;p&gt;如果希望所有进程都得到总和，应使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Allreduce(
    &amp;#x26;local_sum,
    &amp;#x26;total_sum,
    1,
    MPI_DOUBLE,
    MPI_SUM,
    MPI_COMM_WORLD
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;p&gt;| 函数 | 结果保存在哪些进程 |
|---|---|
| &lt;code&gt;MPI_Reduce&lt;/code&gt; | 只保存在 root 进程 |
| &lt;code&gt;MPI_Allreduce&lt;/code&gt; | 所有参与进程都得到结果 |&lt;/p&gt;
&lt;h3&gt;全局和的两种实现模式&lt;/h3&gt;
&lt;p&gt;全局和的目标是：每个进程一开始只有自己的局部值，最后所有进程都得到全局求和结果。&lt;/p&gt;
&lt;p&gt;示例初始值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rank 0:  5
rank 1:  2
rank 2: -1
rank 3: -3
rank 4:  6
rank 5:  5
rank 6: -7
rank 7:  2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总和为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5 + 2 - 1 - 3 + 6 + 5 - 7 + 2 = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模式一：先归约，再分发&lt;/h4&gt;
&lt;p&gt;第一阶段：树形归约，把所有值逐步加到 root 进程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-13.DxGZQP45_1cWdo4.webp&quot; alt=&quot;Diagram 13&quot;&gt;
第二阶段：root 再把结果广播给所有进程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/mpi-reduce-and-allreduce.fbW0sxe2_4IeWb.webp&quot; alt=&quot;mpi reduce and allreduce&quot;&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;思路简单：先做 &lt;code&gt;Reduce&lt;/code&gt;，再做 &lt;code&gt;Broadcast&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;root 先得到最终结果，然后再分发给其他进程。&lt;/li&gt;
&lt;li&gt;可理解为 &lt;code&gt;MPI_Allreduce = MPI_Reduce + MPI_Bcast&lt;/code&gt; 的一种实现思路。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;模式二：蝶式模式&lt;/h4&gt;
&lt;p&gt;蝶式模式中，每一轮进程和一个“伙伴进程”交换当前部分和，交换后双方都更新自己的结果。&lt;/p&gt;
&lt;p&gt;8 个进程需要 &lt;code&gt;log2(8) = 3&lt;/code&gt; 轮：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;第 1 轮：距离 1 的进程交换
(0,1), (2,3), (4,5), (6,7)

第 2 轮：距离 2 的进程交换
(0,2), (1,3), (4,6), (5,7)

第 3 轮：距离 4 的进程交换
(0,4), (1,5), (2,6), (3,7)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/mpi-broadcast-tree.pKTVz-_6_ZOYhV1.webp&quot; alt=&quot;mpi broadcast tree&quot;&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一轮所有进程都参与交换和计算。&lt;/li&gt;
&lt;li&gt;每个进程最终都直接得到全局结果。&lt;/li&gt;
&lt;li&gt;不需要先集中到 root 再广播。&lt;/li&gt;
&lt;li&gt;适合实现 &lt;code&gt;MPI_Allreduce&lt;/code&gt; 这类所有进程都需要结果的集合通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.10 MPI 程序中的 I/O 处理&lt;/h2&gt;
&lt;h3&gt;基本规则&lt;/h3&gt;
&lt;p&gt;在 MPI 程序中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：所有进程都可以访问 &lt;code&gt;stdout&lt;/code&gt;，但输出顺序不可预测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输入&lt;/strong&gt;：通常只有 0 号进程访问 &lt;code&gt;stdin&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此常见做法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;0 号进程从标准输入读取数据。&lt;/li&gt;
&lt;li&gt;0 号进程把输入数据发送给其他进程。&lt;/li&gt;
&lt;li&gt;其他进程通过 &lt;code&gt;MPI_Recv&lt;/code&gt; 接收这些数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;示例：0 号进程读入并分发参数&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (my_rank == 0) {
    printf(&quot;Enter a, b, and n\n&quot;);
    scanf(&quot;%lf %lf %d&quot;, a_p, b_p, n_p);

    for (dest = 1; dest &amp;#x3C; comm_sz; dest++) {
        MPI_Send(a_p, 1, MPI_DOUBLE, dest, 0, MPI_COMM_WORLD);
        MPI_Send(b_p, 1, MPI_DOUBLE, dest, 0, MPI_COMM_WORLD);
        MPI_Send(n_p, 1, MPI_INT,    dest, 0, MPI_COMM_WORLD);
    }
} else {
    MPI_Recv(a_p, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD,
             MPI_STATUS_IGNORE);
    MPI_Recv(b_p, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD,
             MPI_STATUS_IGNORE);
    MPI_Recv(n_p, 1, MPI_INT,    0, 0, MPI_COMM_WORLD,
             MPI_STATUS_IGNORE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码含义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my_rank == 0&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;0 号进程负责提示用户输入。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过循环把三个参数发送给 rank &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;comm_sz - 1&lt;/code&gt; 的所有进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;my_rank != 0&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;非 0 号进程不访问 &lt;code&gt;stdin&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;它们分别从 0 号进程接收 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图示&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-14.xRjdbrtV_Ztb343.webp&quot; alt=&quot;Diagram 14&quot;&gt;&lt;/p&gt;
&lt;h3&gt;注意点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt; 是数据项个数，不是字节数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a_p&lt;/code&gt; 和 &lt;code&gt;b_p&lt;/code&gt; 是 &lt;code&gt;double*&lt;/code&gt;，所以使用 &lt;code&gt;MPI_DOUBLE&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n_p&lt;/code&gt; 是 &lt;code&gt;int*&lt;/code&gt;，所以使用 &lt;code&gt;MPI_INT&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MPI_STATUS_IGNORE&lt;/code&gt; 表示忽略接收状态。&lt;/li&gt;
&lt;li&gt;这里三条消息都使用 &lt;code&gt;tag = 0&lt;/code&gt;，能够正确接收是因为同一发送方到同一接收方的消息不会乱序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;更简洁的替代方案&lt;/h3&gt;
&lt;p&gt;如果所有进程都需要同样的输入数据，更常见的写法是使用广播：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Bcast(a_p, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
MPI_Bcast(b_p, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
MPI_Bcast(n_p, 1, MPI_INT,    0, MPI_COMM_WORLD);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MPI_Bcast&lt;/code&gt; 表示：root 进程把数据广播给通信子中的所有进程。&lt;/p&gt;
&lt;h2&gt;3.11 MPI 计时与同步&lt;/h2&gt;
&lt;h3&gt;MPI_Wtime&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Wtime&lt;/code&gt; 用于获取当前进程的墙上时钟时间（wall time），单位是秒，返回值类型是 &lt;code&gt;double&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它常用于测量某段 MPI 程序的运行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;double start, end;

start = MPI_Wtime();

do_work();

end = MPI_Wtime();

printf(&quot;Elapsed time = %f seconds\n&quot;, end - start);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MPI_Wtime()&lt;/code&gt; 返回的是当前进程看到的时间。&lt;/li&gt;
&lt;li&gt;并行程序中各进程开始执行某段代码的时刻可能不同。&lt;/li&gt;
&lt;li&gt;如果要测量一段并行计算的整体耗时，通常要先同步，再开始计时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MPI_Barrier&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Barrier&lt;/code&gt; 是屏障同步函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Barrier(MPI_Comm comm);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通信子 &lt;code&gt;comm&lt;/code&gt; 中的所有进程都必须到达这个调用点。&lt;/li&gt;
&lt;li&gt;只有所有进程都到达后，大家才能继续往下执行。&lt;/li&gt;
&lt;li&gt;如果有进程还没到，其他已经到达的进程会阻塞等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型用途：同步后再测量并行区域时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Barrier(MPI_COMM_WORLD);
double start = MPI_Wtime();

do_parallel_work();

MPI_Barrier(MPI_COMM_WORLD);
double end = MPI_Wtime();

if (rank == 0) {
    printf(&quot;Parallel section took %f seconds\n&quot;, end - start);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么前后都加 &lt;code&gt;MPI_Barrier&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前面的 &lt;code&gt;MPI_Barrier&lt;/code&gt;：保证所有进程都准备好后再开始计时。&lt;/li&gt;
&lt;li&gt;后面的 &lt;code&gt;MPI_Barrier&lt;/code&gt;：保证所有进程都完成并行段后再停止计时。&lt;/li&gt;
&lt;li&gt;这样测到的时间更接近整个并行计算段的真实耗时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.12 MPI 派生数据类型&lt;/h2&gt;
&lt;p&gt;MPI 的基本数据类型只能描述连续的 &lt;code&gt;int&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt; 等数据。实际程序里经常需要发送更复杂的内存布局，例如矩阵的一列、结构体中的几个字段，或者二维数组中的一个子块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;派生数据类型&lt;/strong&gt;的作用是：告诉 MPI“我要发送的数据在内存中怎样分布”，让 MPI 直接按这个布局通信，而不需要程序员手动把数据打包到临时数组。&lt;/p&gt;
&lt;p&gt;典型使用流程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Datatype new_type;

/* 1. 创建派生类型 */
/* MPI_Type_vector / MPI_Type_indexed / MPI_Type_create_struct 等 */

/* 2. 提交类型，之后才能用于通信 */
MPI_Type_commit(&amp;#x26;new_type);

/* 3. 在 Send/Recv 或集合通信中使用 */
MPI_Send(buf, 1, new_type, dest, tag, comm);

/* 4. 不再使用后释放 */
MPI_Type_free(&amp;#x26;new_type);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;MPI_Type_contiguous&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Type_contiguous&lt;/code&gt; 用来描述一段连续的数据，相当于把多个相同类型的数据组合成一个新类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Type_contiguous(
    int count,
    MPI_Datatype old_type,
    MPI_Datatype *new_type_p
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：把 4 个连续的 &lt;code&gt;MPI_DOUBLE&lt;/code&gt; 看成一个新类型。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MPI_Type_vector&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Type_vector&lt;/code&gt; 用来描述“重复出现的等长块”，每个块之间有固定间隔。它常用于发送矩阵的一列。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Type_vector(
    int count,
    int blocklength,
    int stride,
    MPI_Datatype old_type,
    MPI_Datatype *new_type_p
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|---|---|
| &lt;code&gt;count&lt;/code&gt; | 有多少个块 |
| &lt;code&gt;blocklength&lt;/code&gt; | 每个块包含多少个旧类型元素 |
| &lt;code&gt;stride&lt;/code&gt; | 相邻块起点之间相隔多少个旧类型元素 |
| &lt;code&gt;old_type&lt;/code&gt; | 原始 MPI 数据类型 |
| &lt;code&gt;new_type_p&lt;/code&gt; | 输出的新派生类型 |&lt;/p&gt;
&lt;p&gt;示意图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-type-vector.Er1NOsWS_WQtkz.webp&quot; alt=&quot;HPC MPI type vector&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MPI_Type_indexed&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Type_indexed&lt;/code&gt; 比 &lt;code&gt;vector&lt;/code&gt; 更灵活：每个块的长度和起始位置都可以不同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Type_indexed(
    int count,
    int blocklengths[],
    int displacements[],
    MPI_Datatype old_type,
    MPI_Datatype *new_type_p
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;适合描述不规则位置的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-type-indexed.BruAeUh1_Z1O1n7f.webp&quot; alt=&quot;HPC MPI type indexed&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MPI_Type_create_subarray&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Type_create_subarray&lt;/code&gt; 用来描述多维数组中的一个子数组。它常用于矩阵块划分，例如从一个二维矩阵里发送某个矩形区域。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-type-subarray.Bw6bknhV_Z2jsKl.webp&quot; alt=&quot;HPC MPI type subarray&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MPI_Type_create_struct&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Type_create_struct&lt;/code&gt; 用来描述由不同类型字段组成的数据布局。例如一个结构体里既有 &lt;code&gt;int&lt;/code&gt;，也有 &lt;code&gt;double&lt;/code&gt;，并且只想发送其中某些字段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-type-struct-buffer.B48YSKZe_1bp6Rh.webp&quot; alt=&quot;HPC MPI type struct buffer&quot;&gt;&lt;/p&gt;
&lt;h3&gt;结构体填充和 &lt;code&gt;extent&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;C 结构体中可能存在&lt;strong&gt;内存对齐填充&lt;/strong&gt;。也就是说，字段之间或结构体末尾可能有一些程序员没有显式声明的空字节。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-struct-padding.BNoqxL0v_1pEMMj.webp&quot; alt=&quot;HPC MPI struct padding&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果直接把结构体当成连续内存发送，可能会把不需要的填充字节或相邻字段一起发送出去。派生数据类型可以只描述真正需要通信的字段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-derived-datatype-padding-send.BPgyPDCv_2cV0n.webp&quot; alt=&quot;HPC MPI derived datatype padding send&quot;&gt;&lt;/p&gt;
&lt;p&gt;还有一个容易混淆的概念是 &lt;code&gt;extent&lt;/code&gt;：它表示 MPI 在连续排列多个该类型对象时，相邻对象起点之间的跨度。结构体有填充时，&lt;code&gt;extent&lt;/code&gt; 可能大于字段本身的有效数据长度。&lt;/p&gt;
&lt;p&gt;必要时可以用 &lt;code&gt;MPI_Type_create_resized&lt;/code&gt; 调整派生类型的 &lt;code&gt;extent&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-derived-datatype-resize.BhGTfzzx_Z1Hl7XT.webp&quot; alt=&quot;HPC MPI derived datatype resize&quot;&gt;&lt;/p&gt;
&lt;p&gt;核心记忆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;派生数据类型描述的是&lt;strong&gt;内存布局&lt;/strong&gt;，不是新建一份数据。&lt;/li&gt;
&lt;li&gt;创建后必须 &lt;code&gt;MPI_Type_commit&lt;/code&gt;，不用后要 &lt;code&gt;MPI_Type_free&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;发送非连续数据时，派生类型通常比手动打包更清晰，也更不容易出错。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.13 MPI 死锁、&lt;code&gt;Sendrecv&lt;/code&gt; 和非阻塞通信&lt;/h2&gt;
&lt;h3&gt;点对点通信中的死锁&lt;/h3&gt;
&lt;p&gt;如果两个进程都先执行阻塞发送，再执行接收，就可能互相等待。&lt;/p&gt;
&lt;p&gt;典型错误模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rank 0: MPI_Send(... rank 1 ...); MPI_Recv(... rank 1 ...);
rank 1: MPI_Send(... rank 0 ...); MPI_Recv(... rank 0 ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当消息较大或使用同步发送时，两个进程都在等对方先接收，程序就可能卡住。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-deadlock-send-recv.C2w-3fju_1kWPqe.webp&quot; alt=&quot;HPC MPI deadlock send recv&quot;&gt;&lt;/p&gt;
&lt;h3&gt;解决方式一：调整发送接收顺序&lt;/h3&gt;
&lt;p&gt;让一边先发送后接收，另一边先接收后发送，就能避免两边同时卡在发送上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-deadlock-solution-reorder.BrFiUIUA_Z2m15Gg.webp&quot; alt=&quot;HPC MPI deadlock solution reorder&quot;&gt;&lt;/p&gt;
&lt;h3&gt;解决方式二：&lt;code&gt;MPI_Sendrecv&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Sendrecv&lt;/code&gt; 把一次发送和一次接收合成一个调用，适合相邻进程交换数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int MPI_Sendrecv(
    const void *sendbuf,
    int sendcount,
    MPI_Datatype sendtype,
    int dest,
    int sendtag,
    void *recvbuf,
    int recvcount,
    MPI_Datatype recvtype,
    int source,
    int recvtag,
    MPI_Comm comm,
    MPI_Status *status
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的价值是：程序员不用手动安排“谁先发、谁先收”，MPI 会处理这个交换过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-deadlock-solution-sendrecv.CxG9PpWY_14s91a.webp&quot; alt=&quot;HPC MPI deadlock solution sendrecv&quot;&gt;&lt;/p&gt;
&lt;h3&gt;缓冲发送&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MPI_Bsend&lt;/code&gt; 是缓冲发送。它依赖用户提供的缓冲区，发送数据可以先被拷贝到这个缓冲区中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-buffered-send.Bqji1vAj_Z2kFLsm.webp&quot; alt=&quot;HPC MPI buffered send&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意：缓冲发送不是首选的死锁解决方案。更常用的是重新安排通信顺序、使用 &lt;code&gt;MPI_Sendrecv&lt;/code&gt;，或者使用非阻塞通信。&lt;/p&gt;
&lt;h3&gt;非阻塞通信&lt;/h3&gt;
&lt;p&gt;非阻塞通信的核心是：函数调用可以先返回，通信在后台继续进行。&lt;/p&gt;
&lt;p&gt;常用函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Isend(..., MPI_Request *request);
MPI_Irecv(..., MPI_Request *request);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;I&lt;/code&gt; 表示 immediate。调用返回只表示“通信请求已经发起”，不表示通信已经完成。&lt;/p&gt;
&lt;p&gt;因此必须配合等待或测试函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Wait(&amp;#x26;request, &amp;#x26;status);
MPI_Test(&amp;#x26;request, &amp;#x26;flag, &amp;#x26;status);
MPI_Waitall(count, requests, statuses);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最重要的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 &lt;code&gt;MPI_Isend&lt;/code&gt;，在通信完成前，不要修改或释放发送缓冲区。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;MPI_Irecv&lt;/code&gt;，在通信完成前，不要读取接收缓冲区中的结果。&lt;/li&gt;
&lt;li&gt;是否完成要看 &lt;code&gt;MPI_Wait&lt;/code&gt; / &lt;code&gt;MPI_Test&lt;/code&gt; 等函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MPI_Request&lt;/code&gt; 可以理解成一次未完成通信的“句柄”：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-nonblocking-request-handle.Db5iHMWv_llKp0.webp&quot; alt=&quot;HPC MPI nonblocking request handle&quot;&gt;&lt;/p&gt;
&lt;p&gt;多个非阻塞通信可以一起等待：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-nonblocking-waitall.d1LdElUh_1zpV1V.webp&quot; alt=&quot;HPC MPI nonblocking waitall&quot;&gt;&lt;/p&gt;
&lt;p&gt;也可以等待其中任意一个或一部分完成：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/HPC-MPI-request-waitany-waitsome.Du_vwReE_fq5zj.webp&quot; alt=&quot;HPC MPI request waitany waitsome&quot;&gt;&lt;/p&gt;
&lt;p&gt;一句话总结：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;阻塞通信：调用返回时，相关通信条件已经满足。
非阻塞通信：调用先返回，但之后必须 wait/test，才能确认通信完成。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第 4 章 使用 Pthreads 进行共享内存编程&lt;/h1&gt;
&lt;h2&gt;4.1 Pthreads 概述&lt;/h2&gt;
&lt;h3&gt;Pthreads 是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pthreads&lt;/strong&gt; 是 POSIX 线程库，用于在共享内存系统中编写多线程程序。&lt;/p&gt;
&lt;p&gt;和 MPI 的主要区别：&lt;/p&gt;
&lt;p&gt;| 对比项 | MPI | Pthreads |
|---|---|---|
| 所在章节 | 第 3 章 | 第 4 章 |
| 编程对象 | 进程 | 线程 |
| 内存模型 | 分布式内存 | 共享内存 |
| 数据是否默认共享 | 不共享；每个进程有自己的地址空间 | 共享；同一进程内线程可以访问共享变量 |
| 通信方式 | 显式消息传递 | 通过共享变量隐式通信 |
| 典型 API | &lt;code&gt;MPI_Send&lt;/code&gt;、&lt;code&gt;MPI_Recv&lt;/code&gt;、&lt;code&gt;MPI_Reduce&lt;/code&gt;、&lt;code&gt;MPI_Bcast&lt;/code&gt; | &lt;code&gt;pthread_create&lt;/code&gt;、&lt;code&gt;pthread_join&lt;/code&gt;、&lt;code&gt;pthread_mutex_lock&lt;/code&gt;、&lt;code&gt;sem_wait&lt;/code&gt; |
| 协调重点 | 发送/接收、集合通信、消息匹配、阻塞语义 | 临界区、互斥锁、信号量、条件变量、读写锁 |
| 常见问题 | 死锁、消息匹配错误、通信开销 | 数据竞争、伪共享、线程不安全、锁竞争 |
| 适合场景 | 多机器、集群、大规模分布式计算 | 单机多核、小到中等规模共享内存并行 |&lt;/p&gt;
&lt;p&gt;核心区别：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MPI：进程之间不共享内存，所以靠消息传递。
Pthreads：线程之间共享内存，所以靠同步保护。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MPI 中，每个进程像独立的执行单元，有自己的内存。要交换数据，必须显式调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Send(...);
MPI_Recv(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pthreads 中，多个线程在同一个进程内运行，可以直接访问同一个共享变量。因此重点不是发送消息，而是防止多个线程同时乱改共享数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_lock(&amp;#x26;mutex);
/* 临界区 */
pthread_mutex_unlock(&amp;#x26;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;性能瓶颈也不同：&lt;/p&gt;
&lt;p&gt;| 模型 | 常见性能瓶颈 |
|---|---|
| MPI | 通信次数、消息大小、网络延迟、集合通信效率 |
| Pthreads | 锁竞争、临界区太长、缓存一致性开销、伪共享、线程调度 |&lt;/p&gt;
&lt;p&gt;两者都可以使用 &lt;strong&gt;SPMD&lt;/strong&gt; 思路：同一份程序由多个执行单元运行。区别是 MPI 通常用 &lt;code&gt;rank&lt;/code&gt; 区分进程，Pthreads 通常用线程编号区分线程。&lt;/p&gt;
&lt;p&gt;共享内存系统适合在同一台机器上使用多个线程，规模通常不会太大，因为多个核心访问共享内存时会受到内存带宽、总线、缓存一致性等因素限制。&lt;/p&gt;
&lt;h3&gt;Pthreads 程序的核心问题&lt;/h3&gt;
&lt;p&gt;Pthreads 的重点不是“如何传消息”，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程之间如何通信：通过访问共享变量。&lt;/li&gt;
&lt;li&gt;如何避免写写冲突：避免多个线程同时写同一个共享变量。&lt;/li&gt;
&lt;li&gt;如何构造临界区：把一次只能由一个线程执行的代码包起来。&lt;/li&gt;
&lt;li&gt;如何表达锁：使用互斥量 &lt;code&gt;pthread_mutex_t&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pthreads 程序也经常采用 &lt;strong&gt;SPMD&lt;/strong&gt; 思路：所有线程运行同一个线程函数，但根据线程编号处理不同数据。&lt;/p&gt;
&lt;h2&gt;4.2 Hello World 示例&lt;/h2&gt;
&lt;h3&gt;编译与运行&lt;/h3&gt;
&lt;p&gt;编译 Pthreads 程序时需要加 &lt;code&gt;-pthread&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -pthread hello_pthread.c -o hello_pthread
./hello_pthread
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;pthread.h&gt;

void* say_hello(void* arg) {
    int thread_id = *(int*)arg;
    printf(&quot;Hello from thread %d!\n&quot;, thread_id);
    return NULL;
}

int main() {
    const int NUM_THREADS = 4;
    pthread_t threads[NUM_THREADS];
    int ids[NUM_THREADS];

    for (int i = 0; i &amp;#x3C; NUM_THREADS; i++) {
        ids[i] = i;
        pthread_create(&amp;#x26;threads[i], NULL, say_hello, (void*) &amp;#x26;ids[i]);
    }

    for (int i = 0; i &amp;#x3C; NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf(&quot;All threads finished. Goodbye!\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果线程数运行时才确定，可以动态分配线程对象数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_t* threads = malloc(num_threads * sizeof(pthread_t));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4.3 pthread_create：启动线程&lt;/h2&gt;
&lt;p&gt;在启动线程前，需要先创建线程对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_t thread;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个线程对应一个 &lt;code&gt;pthread_t&lt;/code&gt; 对象。创建线程使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_create(
    pthread_t* thread,
    const pthread_attr_t* attr,
    void* (*start_routine)(void*),
    void* arg
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;p&gt;| 参数              | 含义                     |
| --------------- | ---------------------- |
| &lt;code&gt;thread&lt;/code&gt;        | 指向线程对象的指针，创建成功后保存线程 ID |
| &lt;code&gt;attr&lt;/code&gt;          | 线程属性，通常传 &lt;code&gt;NULL&lt;/code&gt;        |
| &lt;code&gt;start_routine&lt;/code&gt; | 线程要执行的函数               |
| &lt;code&gt;arg&lt;/code&gt;           | 传给线程函数的参数              |&lt;/p&gt;
&lt;p&gt;线程函数的固定形式通常是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void* thread_func(void* arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要传多个参数，常见做法是定义结构体，把结构体指针作为 &lt;code&gt;arg&lt;/code&gt; 传入。&lt;/p&gt;
&lt;h2&gt;4.4 pthread_join：等待线程结束&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pthread_join&lt;/code&gt; 用来等待一个线程结束：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_join(pthread_t thread, void** retval);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_join&lt;/code&gt; &lt;strong&gt;不会主动停止线程&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它会阻塞调用它的线程，直到目标线程正常返回。&lt;/li&gt;
&lt;li&gt;常用于主线程等待所有子线程执行完毕。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = 0; i &amp;#x3C; NUM_THREADS; i++) {
    pthread_join(threads[i], NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果主线程不等待子线程，主线程可能先结束，导致整个进程退出，子线程还没来得及完成。&lt;/p&gt;
&lt;h2&gt;4.5 共享变量、冲突和临界区&lt;/h2&gt;
&lt;h3&gt;线程如何通信&lt;/h3&gt;
&lt;p&gt;Pthreads 中，同一个进程内的多个线程共享地址空间，因此线程可以直接访问同一批全局变量、堆内存和静态变量。&lt;/p&gt;
&lt;p&gt;也就是说，线程之间的通信通常是隐式的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 A 写共享变量
线程 B 读共享变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这比 MPI 的显式发送和接收更方便，但也更容易出错。&lt;/p&gt;
&lt;h3&gt;写写冲突&lt;/h3&gt;
&lt;p&gt;如果多个线程同时写同一个共享变量，就可能出现写写冲突。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sum += my_val;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码看起来是一条语句，但底层通常包含：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从内存读取 &lt;code&gt;sum&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;加上 &lt;code&gt;my_val&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;把结果写回 &lt;code&gt;sum&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果两个线程同时执行，就可能都读到旧值，最后其中一个写入覆盖另一个写入，导致结果错误。&lt;/p&gt;
&lt;h3&gt;竞争条件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;竞争条件&lt;/strong&gt;：多个线程同时访问某个共享资源，并且程序结果依赖于线程执行的先后顺序。&lt;/p&gt;
&lt;p&gt;例子：共享变量 &lt;code&gt;x&lt;/code&gt; 初始值为 &lt;code&gt;0&lt;/code&gt;，两个线程几乎同时执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;y = compute();   /* Thread 0: y = 1; Thread 1: y = 2 */
x = x + y;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理想情况下，最终应该得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x = 1 + 2 = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但实际执行可能是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程 0 读到 &lt;code&gt;x = 0&lt;/code&gt;，准备加 &lt;code&gt;y = 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;线程 1 也读到 &lt;code&gt;x = 0&lt;/code&gt;，准备加 &lt;code&gt;y = 2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;线程 0 写回 &lt;code&gt;x = 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;线程 1 写回 &lt;code&gt;x = 2&lt;/code&gt;，覆盖线程 0 的结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终得到 &lt;code&gt;x = 2&lt;/code&gt;，而不是 &lt;code&gt;x = 3&lt;/code&gt;。这说明 &lt;code&gt;x = x + y&lt;/code&gt; 不是原子操作。&lt;/p&gt;
&lt;h3&gt;临界区&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;临界区&lt;/strong&gt;：一次只能允许一个线程执行的代码区域。&lt;/p&gt;
&lt;p&gt;通常，临界区里面放的是更新共享变量的操作。&lt;/p&gt;
&lt;p&gt;对于共享变量更新，常见做法是把更新语句放进临界区：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;lock();
sum += my_val;
unlock();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;临界区越大，串行化越严重；临界区越小，程序通常越容易保持并行性能。&lt;/p&gt;
&lt;p&gt;临界区要解决的问题是：让多个线程对共享变量的访问变成互斥访问，从而避免多个线程同时修改同一个共享变量。&lt;/p&gt;
&lt;h3&gt;互斥锁&lt;/h3&gt;
&lt;p&gt;忙等待会持续占用 CPU，因此更常用的方式是使用&lt;strong&gt;互斥锁&lt;/strong&gt;和信号量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;互斥锁&lt;/strong&gt;（mutex）是一种线程同步机制，用来保证同一时刻只有一个线程可以进入临界区。&lt;/p&gt;
&lt;p&gt;Pthreads 使用 &lt;code&gt;pthread_mutex_t&lt;/code&gt; 表示互斥量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_t mutex;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;p&gt;使用互斥锁前需要初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_mutex_init(
    pthread_mutex_t* mutex_p,
    const pthread_mutexattr_t* attr_p
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般情况下，属性参数传 &lt;code&gt;NULL&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_init(&amp;#x26;lock, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;上锁&lt;/h4&gt;
&lt;p&gt;进入临界区前调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_mutex_lock(pthread_mutex_t* mutex_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果锁空闲，当前线程获得锁并继续执行。&lt;/li&gt;
&lt;li&gt;如果锁已被其他线程持有，当前线程阻塞等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;解锁&lt;/h4&gt;
&lt;p&gt;离开临界区后调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_mutex_unlock(pthread_mutex_t* mutex_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：释放锁，让其他等待线程有机会进入临界区。&lt;/p&gt;
&lt;h4&gt;销毁&lt;/h4&gt;
&lt;p&gt;互斥锁不再使用时，应销毁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int pthread_mutex_destroy(pthread_mutex_t* mutex_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;典型使用方式&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_t lock;
pthread_mutex_init(&amp;#x26;lock, NULL);

pthread_mutex_lock(&amp;#x26;lock);

/* 临界区 */
sum += my_val;

pthread_mutex_unlock(&amp;#x26;lock);

pthread_mutex_destroy(&amp;#x26;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用锁的目的不是提高速度，而是保证共享变量更新的正确性。&lt;/p&gt;
&lt;h3&gt;互斥锁的局限：不能保证执行顺序&lt;/h3&gt;
&lt;p&gt;互斥锁只能保证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;同一时刻只有一个线程进入临界区
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但它不能保证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程按照指定顺序进入临界区
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，如果临界区里的操作对顺序敏感，仅使用互斥锁仍然可能得到不同结果。&lt;/p&gt;
&lt;p&gt;典型例子：多个线程各自生成一个矩阵，然后把它乘到共享结果矩阵 &lt;code&gt;product_mat&lt;/code&gt; 上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void* Thread_work(void* rank) {
    long my_rank = (long) rank;
    matrix_t my_mat = Allocate_matrix(n);
    Generate_matrix(my_mat);

    pthread_mutex_lock(&amp;#x26;mutex);
    Multiply_matrix(product_mat, my_mat);
    pthread_mutex_unlock(&amp;#x26;mutex);

    Free_matrix(&amp;#x26;my_mat);
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;互斥锁可以保证一次只有一个线程执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;Multiply_matrix(product_mat, my_mat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但问题是：不同线程进入临界区的顺序由操作系统调度决定。&lt;/p&gt;
&lt;p&gt;矩阵乘法不满足交换律：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A * B * C != C * B * A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，线程执行乘法的顺序不同，最终的 &lt;code&gt;product_mat&lt;/code&gt; 可能不同。&lt;/p&gt;
&lt;p&gt;结论：互斥锁适合保护共享变量，避免同时写；但如果操作本身对顺序敏感，还需要额外机制规定线程执行顺序。&lt;/p&gt;
&lt;h3&gt;信号量&lt;/h3&gt;
&lt;p&gt;有些同步模式不是简单的“只能一个线程进临界区”，而是“一个线程要等另一个线程完成某些操作后才能继续”。这种情况更适合使用&lt;strong&gt;信号量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;使用信号量需要引入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;semaphore.h&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;信号量和互斥锁最大的区别：&lt;/p&gt;
&lt;p&gt;| 对比项 | 互斥锁 | 信号量 |
|---|---|---|
| 核心含义 | 保护同一段临界区，避免多个线程同时进入 | 表达“某件事已经发生，另一个线程可以继续了” |
| 所属关系 | 谁 &lt;code&gt;lock&lt;/code&gt;，通常谁 &lt;code&gt;unlock&lt;/code&gt; | 没有所有权，任何线程都可以 &lt;code&gt;post&lt;/code&gt; |
| 典型用途 | 互斥访问共享变量 | 控制线程之间的先后顺序 |
| 顺序控制 | 不保证线程进入顺序 | 可用于让线程等待某个事件发生 |&lt;/p&gt;
&lt;p&gt;对于当前学习目的，最关键的区别是：&lt;strong&gt;信号量没有所有权&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一句话记忆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt;：谁 &lt;code&gt;lock&lt;/code&gt;，通常谁 &lt;code&gt;unlock&lt;/code&gt;；重点是保护同一段临界区。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;semaphore&lt;/code&gt;：任何线程都可以 &lt;code&gt;post&lt;/code&gt;；重点是通知另一个线程可以继续。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主线程可以把所有信号量初始化为 &lt;code&gt;0&lt;/code&gt;，表示这些事件一开始都尚未发生。&lt;/li&gt;
&lt;li&gt;任意线程都可以对任意信号量执行 &lt;code&gt;sem_wait&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;任意线程也可以对任意信号量执行 &lt;code&gt;sem_post&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;因此，一个线程可以通过 &lt;code&gt;sem_post&lt;/code&gt; 唤醒另一个正在等待该信号量的线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更准确地说，信号量的重点不是“加锁”，而是表达：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;某个事件已经发生，等待这个事件的线程可以继续了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这和互斥锁不同：互斥锁通常强调“==谁加锁，谁解锁==”。信号量更像一个可被不同线程共同操作的通知器或计数器。&lt;/p&gt;
&lt;h4&gt;sem_init：初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sem_init(
    sem_t* semaphore_p,
    int shared,
    unsigned initial_val
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;semaphore_p&lt;/code&gt;：信号量对象地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shared&lt;/code&gt;：是否用于进程间共享。通常写 &lt;code&gt;0&lt;/code&gt;，表示用于同一进程内的线程之间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initial_val&lt;/code&gt;：信号量初值。常见地，&lt;code&gt;0&lt;/code&gt; 表示事件尚未发生，等待线程需要阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sem_t sem;
sem_init(&amp;#x26;sem, 0, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;initial_val = 0&lt;/code&gt; 时，先执行 &lt;code&gt;sem_wait&lt;/code&gt; 的线程会阻塞，直到其他线程执行 &lt;code&gt;sem_post&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;sem_wait：等待&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sem_wait(sem_t* semaphore_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果信号量值 &lt;code&gt;&gt; 0&lt;/code&gt;，将其减 1，然后线程继续执行。&lt;/li&gt;
&lt;li&gt;如果信号量值 &lt;code&gt;= 0&lt;/code&gt;，线程阻塞，直到其他线程执行 &lt;code&gt;sem_post&lt;/code&gt; 发出通知。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;等待某个事件发生；如果还没发生，就睡眠等待
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sem_post：通知/释放&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sem_post(sem_t* semaphore_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将信号量值加 1。&lt;/li&gt;
&lt;li&gt;如果有线程正在 &lt;code&gt;sem_wait&lt;/code&gt; 中阻塞，可能唤醒其中一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;宣布某个事件已经发生；如果有人在等，就叫醒一个
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sem_destroy：销毁&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sem_destroy(sem_t* semaphore_p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;信号量不再使用时，应调用 &lt;code&gt;sem_destroy&lt;/code&gt; 销毁。&lt;/p&gt;
&lt;h4&gt;典型用法：控制线程先后顺序&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sem_t sem;
sem_init(&amp;#x26;sem, 0, 0);

/* 线程 A */
sem_wait(&amp;#x26;sem);
do_after_B();

/* 线程 B */
do_something();
sem_post(&amp;#x26;sem);

/* 不再使用时 */
sem_destroy(&amp;#x26;sem);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量的重点是表达“事件发生了，可以继续”。&lt;/li&gt;
&lt;li&gt;它比互斥锁更适合控制线程之间的先后顺序。&lt;/li&gt;
&lt;li&gt;初值为 &lt;code&gt;0&lt;/code&gt; 时，表示事件还没发生，等待线程会阻塞。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sem_post&lt;/code&gt; 可以由另一个线程调用，用来通知等待线程继续执行。&lt;/li&gt;
&lt;li&gt;初值为 &lt;code&gt;1&lt;/code&gt; 的二进制信号量可以模拟互斥锁，但这不是这里最重要的用法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;忙等待、互斥锁和信号量对比&lt;/h3&gt;
&lt;p&gt;这三者都可以用于协调线程，但解决问题的重点不同。&lt;/p&gt;
&lt;p&gt;本节最重要的主线不是“这三个工具分别叫什么”，而是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;它们能不能控制线程之间的执行顺序？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是避免多个线程同时进入临界区，用互斥锁通常就够了；如果要求线程 A 做完后线程 B 才能继续，就需要能表达先后关系的机制。&lt;/p&gt;
&lt;p&gt;| 机制 | 核心思想 | 能否控制执行顺序 | 等待时是否占用 CPU | 是否有所有权 | 适合解决的问题 | 主要风险 |
|---|---|---|---|---|---|---|
| 忙等待 | 用共享标志变量和空循环反复检查条件 | 可以，但方式低效 | 占用 CPU，一直空转 | 没有明确所有权 | 简单地强制线程按某个顺序执行 | 浪费 CPU；线程数多于核心数时可能严重变慢；还可能受编译器优化影响 |
| 互斥锁 | 把一段临界区锁起来，一次只允许一个线程进入 | 不能精确控制，只保证互斥 | 通常阻塞，不持续空转 | 有所有权，谁 &lt;code&gt;lock&lt;/code&gt; 通常谁 &lt;code&gt;unlock&lt;/code&gt; | 保护共享变量或共享数据结构，避免同时写 | 不保证进入临界区的顺序；临界区太大会导致串行化 |
| 信号量 | 用计数器表达资源数量或事件是否发生 | 可以，用 &lt;code&gt;wait/post&lt;/code&gt; 表达先后关系 | 信号量为 0 时阻塞 | 没有所有权，任何线程都可以 &lt;code&gt;post&lt;/code&gt; | 控制线程先后顺序、生产者-消费者同步，也可模拟互斥锁 | 用法更灵活，也更容易把 &lt;code&gt;wait/post&lt;/code&gt; 顺序写错 |&lt;/p&gt;
&lt;p&gt;一句话记忆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;忙等待：一直问“轮到我了吗？”
互斥锁：保护“这段代码一次只能一个线程执行”。
信号量：通知“某件事发生了，等待者可以继续”。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终记忆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;互斥锁解决“不能同时执行”的问题；
信号量更适合解决“必须按某个顺序执行”的问题；
忙等待也能做顺序控制，但代价是一直占用 CPU。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;栅栏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;栅栏&lt;/strong&gt;（barrier）是一个同步点：所有线程都必须到达这里，任何线程才允许继续往后执行。&lt;/p&gt;
&lt;p&gt;可以把栅栏理解成“阶段分界线”：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0: 做 A --- 到达栅栏 | 等待 | 做 B
线程 1: 做 A --------- 到达栅栏 | 做 B
线程 2: 做 A -- 到达栅栏  | 等待 | 做 B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有最后一个线程也到达栅栏后，所有线程才一起越过栅栏，进入下一阶段。&lt;/p&gt;
&lt;p&gt;常见用途：&lt;/p&gt;
&lt;p&gt;| 用途 | 说明 |
|---|---|
| 更准确计时 | 先让所有线程在栅栏处集合，再一起开始计时，避免有的线程先开始、有的线程晚开始。 |
| 调试并行程序 | 在某个位置放栅栏，然后让 0 号线程打印，表示所有线程都已经运行到这个点。 |
| 阶段同步 | 保证上一阶段所有线程都完成后，才能进入下一阶段。 |&lt;/p&gt;
&lt;p&gt;和互斥锁的区别：&lt;/p&gt;
&lt;p&gt;| 机制 | 重点 |
|---|---|
| &lt;code&gt;mutex&lt;/code&gt; | 防止多个线程同时进入同一段临界区。 |
| &lt;code&gt;barrier&lt;/code&gt; | 要求多个线程都到达同一个位置后再一起继续。 |&lt;/p&gt;
&lt;p&gt;重要风险：如果某个线程永远到不了栅栏，其他线程会一直等在那里，程序就会卡住。因此，栅栏适合用于“所有线程都会执行到这里”的阶段同步。&lt;/p&gt;
&lt;h3&gt;条件变量&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;条件变量&lt;/strong&gt;（condition variable）用于让线程在某个条件尚未满足时睡眠等待，等其他线程改变条件后再被唤醒。&lt;/p&gt;
&lt;p&gt;典型调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_cond_wait(&amp;#x26;cond_var, &amp;#x26;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它不是“拿着锁睡觉”。这个函数会在内部原子地完成几件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把当前线程放进 &lt;code&gt;cond_var&lt;/code&gt; 的等待队列。&lt;/li&gt;
&lt;li&gt;==释放 &lt;code&gt;mutex&lt;/code&gt;==，让其他线程有机会修改共享状态。&lt;/li&gt;
&lt;li&gt;让当前线程睡眠，不再占用 CPU 忙等。&lt;/li&gt;
&lt;li&gt;被唤醒后，返回之前会重新获得 &lt;code&gt;mutex&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为什么必须释放 &lt;code&gt;mutex&lt;/code&gt;：如果等待线程一直拿着锁睡眠，其他线程就无法进入临界区修改条件，也就无法唤醒它。&lt;/p&gt;
&lt;p&gt;通常写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_lock(&amp;#x26;mutex);
while (!condition_is_true) {
    pthread_cond_wait(&amp;#x26;cond_var, &amp;#x26;mutex);
}
/* 条件已经满足，继续处理共享数据 */
pthread_mutex_unlock(&amp;#x26;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：这里通常用 &lt;code&gt;while&lt;/code&gt;，不是 &lt;code&gt;if&lt;/code&gt;。因为线程被唤醒后，条件不一定仍然成立，需要重新检查。&lt;/p&gt;
&lt;h3&gt;读写锁&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;读写锁&lt;/strong&gt;（read-write lock）适合读多写少的共享资源。&lt;/p&gt;
&lt;p&gt;它允许多个线程同时读共享资源，但只允许一个线程写；并且写操作进行时，其他线程不能读也不能写。&lt;/p&gt;
&lt;p&gt;锁状态：&lt;/p&gt;
&lt;p&gt;| 锁状态 | 其他线程能否同时操作 |
|---|---|
| 无锁 | 所有线程都可以竞争加锁 |
| 读锁（共享） | 其他线程可以继续加读锁 |
| 写锁（独占） | 不允许其他线程加读锁或写锁 |&lt;/p&gt;
&lt;p&gt;初始化和销毁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_rwlock_t rwlock;

pthread_rwlock_init(&amp;#x26;rwlock, NULL);
pthread_rwlock_destroy(&amp;#x26;rwlock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加读锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_rwlock_rdlock(&amp;#x26;rwlock);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果当前没有写锁，就可以成功加读锁。&lt;/li&gt;
&lt;li&gt;多个线程可以同时持有读锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加写锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_rwlock_wrlock(&amp;#x26;rwlock);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;只有当前没有其他线程持有读锁或写锁时，才会成功。&lt;/li&gt;
&lt;li&gt;写锁是独占访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_rwlock_unlock(&amp;#x26;rwlock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读锁和写锁都用同一个 &lt;code&gt;pthread_rwlock_unlock&lt;/code&gt; 解锁。&lt;/p&gt;
&lt;p&gt;一句话记忆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;读读可以并发，写写不能并发，读写也不能并发。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多线程链表操作&lt;/h3&gt;
&lt;p&gt;链表的主要操作可以分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读操作：查找、遍历，不修改链表结构。&lt;/li&gt;
&lt;li&gt;写操作：插入、删除，会修改指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为不能让多个线程同时修改同一个指针，所以链表并发访问需要同步。&lt;/p&gt;
&lt;p&gt;常见加锁方式：&lt;/p&gt;
&lt;p&gt;| 方法 | 思路 | 特点 |
|---|---|---|
| 整个链表加锁 | 所有读写操作都先拿同一把锁 | 最简单，但并发度最低 |
| 每个节点加锁 | 每个节点都有自己的锁 | 并发度更高，但实现复杂、空间开销大，通常最慢 |
| 读写锁 | 读操作共享，写操作独占 | 读多写少时效果最好 |&lt;/p&gt;
&lt;p&gt;使用读写锁时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个查找线程可以同时持有读锁，一起读链表。&lt;/li&gt;
&lt;li&gt;插入或删除线程需要写锁，写锁期间不允许其他线程读或写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;效率结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当读操作很多、写操作较少时，读写锁通常比“整个链表加一把互斥锁”更高效。&lt;/li&gt;
&lt;li&gt;当写操作较多时，读写锁的优势会变小，通常只比整个链表加锁快一点。&lt;/li&gt;
&lt;li&gt;每个节点单独加锁看起来更细粒度，但锁操作太多，而且每个节点都要额外存锁，通常代价最大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程安全性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;线程安全&lt;/strong&gt;：一段代码或一个函数在多线程环境中被并发调用时，仍然能得到正确结果，不会因为共享状态被互相覆盖而出错。&lt;/p&gt;
&lt;p&gt;判断线程安全时，不能只看自己写的代码，还要看调用的库函数是否线程安全。&lt;/p&gt;
&lt;p&gt;例子：多个线程共同读取文本，每个线程读一行，再把这一行拆成单词。&lt;/p&gt;
&lt;p&gt;读输入可以用信号量保护：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sem_wait(&amp;#x26;sems[my_rank]);
fg_rv = fgets(my_line, MAX, stdin);
sem_post(&amp;#x26;sems[next]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以避免多个线程同时读 &lt;code&gt;stdin&lt;/code&gt;。但是，后面如果使用 &lt;code&gt;strtok&lt;/code&gt; 拆单词，程序仍然可能线程不安全：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;my_string = strtok(my_line, &quot; \t\n&quot;);
while (my_string != NULL) {
    my_string = strtok(NULL, &quot; \t\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是 &lt;code&gt;strtok(NULL, ...)&lt;/code&gt; 需要知道“上一次拆到哪里了”。&lt;code&gt;strtok&lt;/code&gt; 会把这个位置保存在函数内部的静态状态中，类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static char* saved_position;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单线程中这没有问题；但多线程中，线程 0 正在拆自己的字符串时，线程 1 也调用 &lt;code&gt;strtok&lt;/code&gt;，就可能覆盖这个内部位置。于是线程 0 下一次调用 &lt;code&gt;strtok(NULL, ...)&lt;/code&gt; 时，可能继续拆线程 1 的字符串。&lt;/p&gt;
&lt;p&gt;注意：问题不在 &lt;code&gt;my_line&lt;/code&gt;。&lt;code&gt;my_line&lt;/code&gt; 是线程自己的局部数组，通常位于各自栈上。真正的问题是 &lt;code&gt;strtok&lt;/code&gt; 内部隐藏了共享状态。&lt;/p&gt;
&lt;p&gt;解决办法是使用可重入版本 &lt;code&gt;strtok_r&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char* saveptr;
char* my_string;

my_string = strtok_r(my_line, &quot; \t\n&quot;, &amp;#x26;saveptr);
while (my_string != NULL) {
    printf(&quot;%s\n&quot;, my_string);
    my_string = strtok_r(NULL, &quot; \t\n&quot;, &amp;#x26;saveptr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;strtok_r&lt;/code&gt; 把“拆到哪里了”这个状态放到调用者提供的 &lt;code&gt;saveptr&lt;/code&gt; 中。如果每个线程都有自己的 &lt;code&gt;saveptr&lt;/code&gt;，线程之间就不会互相覆盖解析位置。&lt;/p&gt;
&lt;p&gt;保证线程安全的常见方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免共享可变状态，尽量使用局部变量或线程私有变量。&lt;/li&gt;
&lt;li&gt;如果必须共享状态，就用互斥锁、信号量、读写锁等同步机制保护。&lt;/li&gt;
&lt;li&gt;使用线程安全或可重入的库函数，例如用 &lt;code&gt;strtok_r&lt;/code&gt; 替代 &lt;code&gt;strtok&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检查整个代码块，包括它调用的函数；只要其中某个函数内部有未保护的共享状态，整体就可能线程不安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;忙等待&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;忙等待&lt;/strong&gt;（busy-waiting）：一个线程反复测试某个条件是否成立，在条件成立前不做其他有用工作。&lt;/p&gt;
&lt;p&gt;例子：用共享变量 &lt;code&gt;flag&lt;/code&gt; 控制线程进入临界区的顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;y = Compute(my_rank);
while (flag != my_rank);
x = x + y;
flag++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;while (flag != my_rank);&lt;/code&gt; 的循环体是空的，只是在反复检查条件。&lt;/li&gt;
&lt;li&gt;条件不满足时，线程会一直占用 CPU 等待。&lt;/li&gt;
&lt;li&gt;可以用来强制线程按某种顺序进入临界区，但效率通常不好。&lt;/li&gt;
&lt;li&gt;如果编译器优化或硬件重排改变了语句顺序，可能破坏程序正确性。&lt;/li&gt;
&lt;li&gt;它依赖共享变量可见性，实际编程中要非常谨慎。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;忙等待的关键问题：轮到的线程可能没被调度&lt;/h3&gt;
&lt;p&gt;忙等待最重要的问题是：&lt;strong&gt;如果 &lt;code&gt;flag&lt;/code&gt; 已经轮到某个线程，但操作系统恰好没有把 CPU 分配给这个线程，那么其他线程即使在运行，也只能卡在忙等待里空转。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设有 2 个核心、5 个线程，并用 &lt;code&gt;flag&lt;/code&gt; 控制线程按编号进入临界区：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (flag != my_rank);
/* 临界区 */
flag++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表 4.2：带有忙等待的可能事件序列。&lt;/p&gt;
&lt;p&gt;| 时间 | &lt;code&gt;flag&lt;/code&gt; | 线程 0 | 线程 1 | 线程 2 | 线程 3 | 线程 4 |
|---:|---:|---|---|---|---|---|
| 0 | 0 | 临界区 | 忙等待 | 暂停 | 暂停 | 暂停 |
| 1 | 1 | 终止 | 临界区 | 暂停 | 忙等待 | 暂停 |
| 2 | 2 | — | 终止 | 暂停：轮到它但没被调度 | 忙等待 | 忙等待 |
| ... | 2 | — | — | 仍未调度 | 空转 | 空转 |
| ? | 2 | — | — | 临界区 | 暂停 | 忙等待 |&lt;/p&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;flag = 2&lt;/code&gt; 时，只有线程 2 能进入临界区。&lt;/li&gt;
&lt;li&gt;如果线程 2 没有被操作系统调度，线程 3 和线程 4 即使在运行，也只能反复检查条件。&lt;/li&gt;
&lt;li&gt;线程 3、4 不能修改 &lt;code&gt;flag&lt;/code&gt;，所以程序没有任何有效进展。&lt;/li&gt;
&lt;li&gt;直到操作系统重新调度线程 2，程序才会继续推进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，当线程数多于核心数时，忙等待可能让 CPU 时间浪费在“不能推进程序的线程”上，性能会显著下降。&lt;/p&gt;
&lt;p&gt;一句话对比：&lt;/p&gt;
&lt;p&gt;| 机制 | 操作系统调度后会发生什么 | 临界区执行顺序 |
|---|---|---|
| 忙等待 | 随机调度到某个线程运行；如果没有选中 &lt;code&gt;flag&lt;/code&gt; 对应的线程，被调度的线程只能空转 | 可以通过 &lt;code&gt;flag&lt;/code&gt; 人为规定顺序 |
| 互斥量 | 等待锁的线程会被阻塞，操作系统不会让它们一直空转；能获得锁的线程运行后可以推进程序 | 不能确定顺序，由操作系统和锁竞争结果决定 |&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;忙等待虽然浪费 CPU，但可以用 &lt;code&gt;flag&lt;/code&gt; 强行规定线程进入临界区的顺序。&lt;/li&gt;
&lt;li&gt;互斥量不浪费 CPU 空转，但不保证哪个线程先进入临界区。&lt;/li&gt;
&lt;li&gt;当线程数多于核心数时，忙等待最怕“轮到的线程没有被调度”，因为其他线程运行也无法推进。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;互斥锁与忙等待的性能对比&lt;/h3&gt;
&lt;p&gt;互斥锁和忙等待都可以用来保护临界区，但行为不同。&lt;/p&gt;
&lt;p&gt;互斥锁的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个成功调用 &lt;code&gt;pthread_mutex_lock&lt;/code&gt; 的线程会先进入临界区。&lt;/li&gt;
&lt;li&gt;后续哪个线程获得锁由系统调度决定，Pthreads 不保证按调用顺序获得锁。&lt;/li&gt;
&lt;li&gt;有限数量的线程尝试获取锁时，最终都会有机会获得锁。&lt;/li&gt;
&lt;li&gt;等待锁的线程通常会被阻塞，不会像忙等待那样持续占用 CPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;忙等待的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程会反复检查条件。&lt;/li&gt;
&lt;li&gt;当线程数不超过核心数时，忙等待和互斥锁版本的运行时间可能接近。&lt;/li&gt;
&lt;li&gt;当线程数超过核心数时，忙等待线程会占用核心空转，影响真正能进入临界区的线程运行，性能会明显下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果线程数不超过核心数，理想情况下加速比可以接近线程数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;T_serial / T_parallel ≈ thread_count
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是达到近似线性加速。&lt;/p&gt;
&lt;p&gt;表 4.1：在两个四核处理器系统上，使用 &lt;code&gt;n = 10^8&lt;/code&gt; 项的 π 程序运行时间，单位为秒。&lt;/p&gt;
&lt;p&gt;| 线程数 | 忙等待版本 | 互斥锁版本 |
|---:|---:|---:|
| 1 | 2.90 | 2.90 |
| 2 | 1.45 | 1.45 |
| 4 | 0.73 | 0.73 |
| 8 | 0.38 | 0.38 |
| 16 | 0.50 | 0.38 |
| 32 | 0.80 | 0.40 |
| 64 | 3.56 | 0.38 |&lt;/p&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在线程数 &lt;code&gt;&amp;#x3C;= 8&lt;/code&gt; 时，两种方法差异不明显。&lt;/li&gt;
&lt;li&gt;线程数超过核心数后，忙等待版本变慢。&lt;/li&gt;
&lt;li&gt;互斥锁版本更稳定，因为等待线程不会一直占用 CPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.6 矩阵-向量乘法&lt;/h2&gt;
&lt;h3&gt;问题&lt;/h3&gt;
&lt;p&gt;给定矩阵 &lt;code&gt;A[m][n]&lt;/code&gt; 和向量 &lt;code&gt;x[n]&lt;/code&gt;，计算结果向量 &lt;code&gt;y[m]&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;y[i] = A[i][0] * x[0] + A[i][1] * x[1] + ... + A[i][n-1] * x[n-1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;并行思路：按行划分&lt;/h3&gt;
&lt;p&gt;矩阵-向量乘法适合按行划分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程负责若干行。&lt;/li&gt;
&lt;li&gt;线程只写自己负责的 &lt;code&gt;y[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;x&lt;/code&gt; 对所有线程共享，只读。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; 是共享数组，但不同线程写不同位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，如果划分得当，通常不需要对 &lt;code&gt;y[i]&lt;/code&gt; 加锁。&lt;/p&gt;
&lt;p&gt;示意：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0：计算 y[0 .. local_m-1]
线程 1：计算 y[local_m .. 2*local_m-1]
线程 2：计算 y[2*local_m .. 3*local_m-1]
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程函数大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void* Pth_mat_vect(void* rank) {
    long my_rank = (long) rank;
    int local_m = m / thread_count;
    int my_first_row = my_rank * local_m;
    int my_last_row = (my_rank + 1) * local_m - 1;

    for (int i = my_first_row; i &amp;#x3C;= my_last_row; i++) {
        y[i] = 0.0;
        for (int j = 0; j &amp;#x3C; n; j++) {
            y[i] += A[i][j] * x[j];
        }
    }

    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么没有数据竞争&lt;/h3&gt;
&lt;p&gt;在这种按行划分中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt;：所有线程只读。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt;：所有线程只读。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y[i]&lt;/code&gt;：每个元素只由一个线程写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以线程之间不会同时写同一个 &lt;code&gt;y[i]&lt;/code&gt;，一般不会产生写写冲突。&lt;/p&gt;
&lt;h3&gt;伪共享风险&lt;/h3&gt;
&lt;p&gt;虽然不同线程写的是不同的 &lt;code&gt;y[i]&lt;/code&gt;，但仍然可能发生&lt;strong&gt;伪共享&lt;/strong&gt;。原因是 CPU 缓存按&lt;strong&gt;缓存行&lt;/strong&gt;工作，而不是按单个变量工作。&lt;/p&gt;
&lt;p&gt;如果一个缓存行是 &lt;code&gt;64&lt;/code&gt; 字节，一个 &lt;code&gt;double&lt;/code&gt; 是 &lt;code&gt;8&lt;/code&gt; 字节，那么一个缓存行可以放：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;64 / 8 = 8 个 double
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，&lt;code&gt;y[0]&lt;/code&gt; 到 &lt;code&gt;y[7]&lt;/code&gt; 很可能在同一个 cache line 里。不同线程写不同的 &lt;code&gt;y[i]&lt;/code&gt;，逻辑上没有写同一个变量，但硬件缓存一致性协议会把整条 cache line 当成同步单位。&lt;/p&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序结果通常仍然正确。&lt;/li&gt;
&lt;li&gt;但 cache line 会在多个核心之间反复失效和重新获取，性能可能明显下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;同样运算量不代表同样快&lt;/h3&gt;
&lt;p&gt;矩阵-向量乘法的一个重要现象是：&lt;strong&gt;算术运算次数相同，不代表运行时间和并行效率相同&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;例如下面三种矩阵规模：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;8,000,000 x 8
8,000 x 8,000
8 x 8,000,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们都有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;m * n = 64,000,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以内层乘加次数一样。按纯算法复杂度看，它们应该差不多；但实际运行时间可能差很多，原因主要是缓存行为不同。&lt;/p&gt;
&lt;p&gt;这段代码的核心访问模式是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = my_first_row; i &amp;#x3C;= my_last_row; i++) {
    y[i] = 0.0;                  /* 写 y */
    for (int j = 0; j &amp;#x3C; n; j++) {
        y[i] += A[i][j] * x[j];  /* 读 A，读 x，读写 y */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个重要概念：&lt;/p&gt;
&lt;p&gt;| 概念 | 含义 |
|---|---|
| 写缺失（write miss） | 写 &lt;code&gt;y[i]&lt;/code&gt; 时，包含 &lt;code&gt;y[i]&lt;/code&gt; 的 cache line 不在当前核心缓存里 |
| 读缺失（read miss） | 读 &lt;code&gt;x[j]&lt;/code&gt; 时，包含 &lt;code&gt;x[j]&lt;/code&gt; 的 cache line 不在当前核心缓存里 |&lt;/p&gt;
&lt;p&gt;注意：CPU 不是只搬一个 &lt;code&gt;double&lt;/code&gt;，而是把包含该元素的整条 cache line 搬进缓存。&lt;/p&gt;
&lt;h4&gt;情况一：&lt;code&gt;8,000,000 x 8&lt;/code&gt;，主要问题是 &lt;code&gt;y&lt;/code&gt; 的写缺失&lt;/h4&gt;
&lt;p&gt;矩阵形状是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A: 8M x 8
x: 8 x 1
y: 8M x 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x&lt;/code&gt; 只有 8 个元素，很容易长期留在 cache 中。但 &lt;code&gt;y&lt;/code&gt; 有 800 万个元素，每算一行就要写一个新的 &lt;code&gt;y[i]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;y[i]&lt;/code&gt; 所在的 cache line 不在当前核心缓存里，CPU 要先把这一整条 cache line 读进来，再修改它。这就是写缺失，也常叫写分配缺失。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/cache-write-miss-y-8M-by-8.BSeto-l6_2kBTkt.webp&quot; alt=&quot;cache write miss y 8M by 8&quot;&gt;&lt;/p&gt;
&lt;p&gt;直观理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;y 太长，每次写到新的 y 位置，缓存里往往还没有这块数据，
于是频繁发生 y 的写缺失。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;情况二：&lt;code&gt;8 x 8,000,000&lt;/code&gt;，主要问题是 &lt;code&gt;x&lt;/code&gt; 的读缺失，同时 &lt;code&gt;y&lt;/code&gt; 可能伪共享&lt;/h4&gt;
&lt;p&gt;矩阵形状是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A: 8 x 8M
x: 8M x 1
y: 8 x 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时 &lt;code&gt;y&lt;/code&gt; 很短，但每一行都要扫完整个 &lt;code&gt;x&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int j = 0; j &amp;#x3C; n; j++) {
    y[i] += A[i][j] * x[j];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x&lt;/code&gt; 有 800 万个元素，远远大于 cache。访问 &lt;code&gt;x[j]&lt;/code&gt; 时，经常发现对应 cache line 不在缓存里，于是发生大量读缺失。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/cache-read-miss-x-8-by-8M.BK2xn3tp_1EMXaR.webp&quot; alt=&quot;cache read miss x 8 by 8M&quot;&gt;&lt;/p&gt;
&lt;p&gt;直观理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x 太长，每次读 x[j] 都可能要从内存重新搬一条 cache line，
于是频繁发生 x 的读缺失。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个形状还有一个并行性能问题：&lt;code&gt;y&lt;/code&gt; 只有 8 个元素，多个线程很容易写入同一条 cache line。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;8 x 8,000,000&lt;/code&gt;，&lt;code&gt;m = 8&lt;/code&gt;。如果用 4 个线程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0：y[0], y[1]
线程 1：y[2], y[3]
线程 2：y[4], y[5]
线程 3：y[6], y[7]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 &lt;code&gt;y[0]&lt;/code&gt; 从一个缓存行开头开始，那么一个缓存行正好容纳 8 个 &lt;code&gt;double&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;| 同一个 cache line（64B） |  |  |  |  |  |  |  |
|---|---|---|---|---|---|---|---|
| &lt;code&gt;y[0]&lt;/code&gt; | &lt;code&gt;y[1]&lt;/code&gt; | &lt;code&gt;y[2]&lt;/code&gt; | &lt;code&gt;y[3]&lt;/code&gt; | &lt;code&gt;y[4]&lt;/code&gt; | &lt;code&gt;y[5]&lt;/code&gt; | &lt;code&gt;y[6]&lt;/code&gt; | &lt;code&gt;y[7]&lt;/code&gt; |
| 线程 0 写 | 线程 0 写 | 线程 1 写 | 线程 1 写 | 线程 2 写 | 线程 2 写 | 线程 3 写 | 线程 3 写 |&lt;/p&gt;
&lt;p&gt;缓存一致性看到的是整条 cache line，而不是单个 &lt;code&gt;y[i]&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0 写 y[0] 或 y[1]  -&gt; 其他核心中这整条 cache line 失效
线程 1 写 y[2] 或 y[3]  -&gt; 线程 0 所在核心中的同一条 cache line 又失效
线程 2 / 线程 3 写入    -&gt; 继续让这条 cache line 在核心之间来回抢占
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑上每个线程写不同的 &lt;code&gt;y[i]&lt;/code&gt;，没有普通数据竞争；但这些 &lt;code&gt;y[i]&lt;/code&gt; 可能都在同一个缓存行里。一个线程写入时，会让其他核心缓存中的整行失效，这就是伪共享。&lt;/p&gt;
&lt;p&gt;更严重的是，原始写法会在内层循环中反复写 &lt;code&gt;y[i]&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;y[i] += A[i][j] * x[j];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;n = 8,000,000&lt;/code&gt;，每个 &lt;code&gt;y[i]&lt;/code&gt; 会被更新 800 万次，伪共享会被放大。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;8 x 8,000,000&lt;/code&gt; 的性能差，通常有两层原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; 很长，导致大量读缺失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; 很短，多个线程写的 &lt;code&gt;y[i]&lt;/code&gt; 可能挤在同一条 cache line 中，导致伪共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;情况三：&lt;code&gt;8,000 x 8,000&lt;/code&gt;，缓存压力更均衡&lt;/h4&gt;
&lt;p&gt;矩阵形状是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A: 8K x 8K
x: 8K x 1
y: 8K x 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的总运算量同样是 &lt;code&gt;64,000,000&lt;/code&gt;，但 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 都没有达到 800 万那么极端。&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; 没有特别长，读缺失相对少一些。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; 也没有特别长，写缺失相对少一些。&lt;/li&gt;
&lt;li&gt;不同线程写 &lt;code&gt;y&lt;/code&gt; 的区间较大，伪共享通常只可能出现在分界处附近。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以它虽然算术运算量一样，但缓存表现更平衡，实际运行时间可能更好。&lt;/p&gt;
&lt;h4&gt;改进思路&lt;/h4&gt;
&lt;p&gt;更好的写法是先用线程私有变量累加，最后只写一次共享数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;double temp = 0.0;

for (int j = 0; j &amp;#x3C; n; j++) {
    temp += A[i][j] * x[j];
}

y[i] = temp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以减少对 &lt;code&gt;y[i]&lt;/code&gt; 的反复写入，也能明显降低伪共享的影响。&lt;/p&gt;
&lt;p&gt;一句话记忆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;8M x 8：y 很长，主要是写缺失。
8 x 8M：x 很长，主要是读缺失；y 很短，还可能伪共享。
8K x 8K：x、y 都没那么极端，缓存表现更平衡。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心结论：共享内存程序中，线程即使写不同变量，也可能因为这些变量位于同一个缓存行而互相拖慢；这个问题从算法复杂度里看不出来，必须结合缓存行、内存布局和线程划分分析。&lt;/p&gt;
&lt;h1&gt;第 5 章 使用 OpenMP 进行共享内存编程&lt;/h1&gt;
&lt;h2&gt;5.1 OpenMP 概述&lt;/h2&gt;
&lt;p&gt;本章重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何创建线程。&lt;/li&gt;
&lt;li&gt;如何隐式创建线程。&lt;/li&gt;
&lt;li&gt;如何使用 &lt;code&gt;schedule&lt;/code&gt; 调度循环迭代。&lt;/li&gt;
&lt;li&gt;如何用 OpenMP 修饰原本已有的 &lt;code&gt;for&lt;/code&gt; 循环。&lt;/li&gt;
&lt;li&gt;如何通过调度实现负载均衡。&lt;/li&gt;
&lt;li&gt;OpenMP 变量的作用域规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;OpenMP 是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OpenMP&lt;/strong&gt; 是用于共享内存并行编程的 API。它和 Pthreads 一样面向共享内存系统，但编程方式更高层。&lt;/p&gt;
&lt;p&gt;核心区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pthreads&lt;/strong&gt;：程序员显式创建线程，并明确指定每个线程要执行什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenMP&lt;/strong&gt;：程序员通常只需要声明某个代码块可以并行执行；具体任务如何分配、哪个线程执行哪部分，主要交给编译器和运行时系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pthreads：手动管理线程。
OpenMP：标注哪里可以并行，剩下交给编译器和运行时。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenMP 的抽象层次更高，甚至可以看作是为了把已有串行程序逐步改造成并行程序而设计的。&lt;/p&gt;
&lt;h3&gt;OpenMP 和 Pthreads 的区别&lt;/h3&gt;
&lt;p&gt;| 对比项 | Pthreads | OpenMP |
|---|---|---|
| 编程方式 | 显式线程编程 | 通过编译指令声明并行区域 |
| 线程行为 | 程序员明确写出每个线程做什么 | 编译器和运行时决定任务如何分配 |
| 代码形式 | 调用线程库函数，如 &lt;code&gt;pthread_create&lt;/code&gt; | 使用编译指令，如 &lt;code&gt;#pragma omp parallel&lt;/code&gt; |
| 抽象层次 | 更底层，控制更细 | 更高层，代码更简洁 |
| 编译要求 | 只要能链接 Pthreads 库，普通 C 编译器一般即可 | 需要编译器支持 OpenMP |
| 适合场景 | 需要精细控制线程行为 | 循环并行、规则任务划分、快速并行化 |&lt;/p&gt;
&lt;h2&gt;5.2 第一个 OpenMP 程序&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;parallel&lt;/code&gt; 基本块&lt;/h3&gt;
&lt;p&gt;OpenMP 的编译指令总是以 &lt;code&gt;#pragma omp&lt;/code&gt; 开始。最基本的并行指令是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenMP 中最基本的并行结构是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel
{
    /* 多个线程都会执行这里的代码 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 &lt;code&gt;num_threads(thread_count)&lt;/code&gt;，就可以指定并行区域中要创建多少个线程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel num_threads(thread_count)
Hello();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的意思是：创建 &lt;code&gt;thread_count&lt;/code&gt; 个线程，并让每个线程都执行一次 &lt;code&gt;Hello()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果不写 &lt;code&gt;num_threads&lt;/code&gt;，线程数通常由 OpenMP 运行时系统决定，常见情况是按机器上的核心数来设置。&lt;/p&gt;
&lt;h3&gt;程序运行到 &lt;code&gt;parallel&lt;/code&gt; 时发生什么&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;parallel&lt;/code&gt; 指令之前，程序只有一个线程在执行，也就是程序启动时的原始线程。&lt;/p&gt;
&lt;p&gt;当程序运行到 &lt;code&gt;#pragma omp parallel&lt;/code&gt; 时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;原来的线程继续执行。&lt;/li&gt;
&lt;li&gt;OpenMP 额外启动 &lt;code&gt;thread_count - 1&lt;/code&gt; 个线程。&lt;/li&gt;
&lt;li&gt;原始线程和新启动的线程组成一个&lt;strong&gt;线程组&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;线程组中的每个线程都会执行 &lt;code&gt;parallel&lt;/code&gt; 后面的代码块。&lt;/li&gt;
&lt;li&gt;并行代码块结束处==有一个&lt;strong&gt;隐式 barrier&lt;/strong&gt;==。&lt;/li&gt;
&lt;li&gt;所有线程都完成该代码块后，新启动的线程终止，原始线程继续执行后面的串行代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OpenMP 常见线程术语：&lt;/p&gt;
&lt;p&gt;| 术语 | 含义 |
|---|---|
| 主线程 | 程序开始时的第一个执行线程，编号通常是 0 |
| 父线程 | 遇到 &lt;code&gt;parallel&lt;/code&gt; 指令并启动一组线程的线程；很多情况下就是主线程 |
| 子线程 | 由父线程启动的额外线程 |
| 线程组 | 执行同一个 &lt;code&gt;parallel&lt;/code&gt; 代码块的一组线程 |&lt;/p&gt;
&lt;p&gt;可以把 &lt;code&gt;parallel&lt;/code&gt; 理解成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;串行代码
    |
    | 遇到 #pragma omp parallel
    v
创建线程组：线程 0, 线程 1, ..., 线程 p-1
    |
所有线程执行同一个并行代码块
    |
隐式 barrier：所有线程都到达后才能继续
    |
子线程结束，主线程继续执行后面的串行代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这比 Pthreads 更高层：Pthreads 中通常需要手动保存每个线程的数据结构、用循环创建线程、再用循环等待线程结束；OpenMP 把这些线程创建和回收细节隐藏在 &lt;code&gt;parallel&lt;/code&gt; 指令背后。&lt;/p&gt;
&lt;h3&gt;Hello World 示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;
#include &amp;#x3C;omp.h&gt;

void Hello(void);

int main(int argc, char* argv[]) {
    int thread_count = strtol(argv[1], NULL, 10);

    #pragma omp parallel num_threads(thread_count)
    Hello();

    return 0;
}

void Hello(void) {
    int my_rank = omp_get_thread_num();
    int thread_count = omp_get_num_threads();

    printf(&quot;Hello from thread %d of %d\n&quot;, my_rank, thread_count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个重要函数：&lt;/p&gt;
&lt;p&gt;| 函数 | 含义 |
|---|---|
| &lt;code&gt;omp_get_thread_num()&lt;/code&gt; | 返回当前线程在线程组中的编号 |
| &lt;code&gt;omp_get_num_threads()&lt;/code&gt; | 返回当前并行区域中的线程总数 |&lt;/p&gt;
&lt;h3&gt;输出顺序不确定&lt;/h3&gt;
&lt;p&gt;编译和运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -fopenmp hello.c -o hello
./hello 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Hello from thread 0 of 4
Hello from thread 1 of 4
Hello from thread 2 of 4
Hello from thread 3 of 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可能输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Hello from thread 3 of 4
Hello from thread 1 of 4
Hello from thread 2 of 4
Hello from thread 0 of 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：多个线程并行执行，哪个线程先运行到 &lt;code&gt;printf&lt;/code&gt; 是不确定的。&lt;/p&gt;
&lt;h3&gt;编译支持&lt;/h3&gt;
&lt;p&gt;Pthreads 本质上是一个函数库，可以链接到 C 程序中。因此只要系统有 Pthreads 库，普通 C 编译器通常就可以编译 Pthreads 程序。&lt;/p&gt;
&lt;p&gt;OpenMP 不只是普通函数库调用，它依赖编译器识别和处理 &lt;code&gt;#pragma omp ...&lt;/code&gt; 这类编译指令。因此，如果 C 编译器不支持 OpenMP，就可能无法把 OpenMP 程序编译成真正的并行程序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#pragma&lt;/code&gt; 是 C/C++ 的预处理器指令，用来给编译器提供“建议”或“指示”。如果编译器完全不认识某个 &lt;code&gt;#pragma&lt;/code&gt;，通常会忽略它而不是报错；但是如果程序调用了 &lt;code&gt;omp_get_thread_num()&lt;/code&gt; 这类 OpenMP 函数，仍然需要 OpenMP 头文件和运行时库支持。&lt;/p&gt;
&lt;h2&gt;5.3 OpenMP 的变量作用域&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;#pragma omp parallel&lt;/code&gt; 中，变量作用域非常重要。&lt;/p&gt;
&lt;p&gt;基本规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在并行基本块外声明的变量，默认是&lt;strong&gt;共享变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在并行基本块内声明的变量，默认是&lt;strong&gt;私有变量&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int x = 0;  /* 默认共享 */

#pragma omp parallel
{
    int my_rank = omp_get_thread_num();  /* 每个线程各有一份 */
    x += my_rank;                        /* 多个线程共享同一个 x */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：共享变量如果被多个线程同时更新，仍然可能产生竞争条件。OpenMP 虽然比 Pthreads 写法更简单，但它仍然是共享内存并行，线程安全问题不会自动消失。&lt;/p&gt;
&lt;h2&gt;5.4 &lt;code&gt;schedule&lt;/code&gt; 子句&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;schedule&lt;/code&gt; 用来做什么&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;schedule&lt;/code&gt; 子句用于控制 &lt;code&gt;parallel for&lt;/code&gt; 中循环迭代如何分配给线程。&lt;/p&gt;
&lt;p&gt;如果不显式写 &lt;code&gt;schedule&lt;/code&gt;，OpenMP 会使用默认调度方式。想指定循环调度方式，可以在 &lt;code&gt;parallel for&lt;/code&gt; 指令中加入 &lt;code&gt;schedule&lt;/code&gt; 子句。&lt;/p&gt;
&lt;p&gt;没有显式调度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sum = 0.0;

#pragma omp parallel for num_threads(thread_count) \
    reduction(+:sum)
for (int i = 0; i &amp;#x3C;= n; i++) {
    sum += f(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入 &lt;code&gt;schedule&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;sum = 0.0;

#pragma omp parallel for num_threads(thread_count) \
    reduction(+:sum) schedule(static, 1)
for (int i = 0; i &amp;#x3C;= n; i++) {
    sum += f(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;schedule(static, 1)&lt;/code&gt; 表示：采用静态调度，并且每个迭代块的大小是 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;基本形式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;schedule(&amp;#x3C;type&gt;[, &amp;#x3C;chunksize&gt;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt; 表示调度方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chunksize&lt;/code&gt; 表示每个迭代块中包含多少次连续迭代。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 OpenMP 中，一个&lt;strong&gt;迭代块&lt;/strong&gt;（chunk）指的是从&lt;strong&gt;串行 &lt;code&gt;for&lt;/code&gt; 循环的迭代编号空间&lt;/strong&gt;里切出来的一段连续编号区间。块大小就是这个区间中包含的迭代次数。&lt;/p&gt;
&lt;h3&gt;常见调度类型&lt;/h3&gt;
&lt;p&gt;| 类型 | 分配时机 | 核心含义 | 适合场景 |
|---|---|---|---|
| &lt;code&gt;static&lt;/code&gt; | 循环开始前 | 先把迭代分好，再交给各线程 | 每次迭代工作量差不多 |
| &lt;code&gt;dynamic&lt;/code&gt; | 循环运行时 | 线程完成当前块后，再向运行时系统请求新块 | 每次迭代工作量不均匀 |
| &lt;code&gt;guided&lt;/code&gt; | 循环运行时 | 类似动态调度，但块大小通常逐渐变小 | 工作量不均匀，同时想减少调度开销 |
| &lt;code&gt;auto&lt;/code&gt; | 编译器或运行时决定 | 调度方式交给系统选择 | 不想手动指定策略 |
| &lt;code&gt;runtime&lt;/code&gt; | 运行时决定 | 根据环境变量决定调度方式 | 想运行时切换调度策略 |&lt;/p&gt;
&lt;p&gt;只有 &lt;code&gt;static&lt;/code&gt;、&lt;code&gt;dynamic&lt;/code&gt; 和 &lt;code&gt;guided&lt;/code&gt; 可以显式指定 &lt;code&gt;chunksize&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;static&lt;/code&gt; 调度类型&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 调度会在循环开始前把迭代块分配给线程。分配完成后，循环运行期间不再动态调整。&lt;/p&gt;
&lt;p&gt;假设有 12 个迭代，编号为 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;11&lt;/code&gt;，并且有 3 个线程。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;schedule(static, 1)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;schedule(static, 1)

线程 0：0, 3, 6, 9
线程 1：1, 4, 7, 10
线程 2：2, 5, 8, 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为块大小是 &lt;code&gt;1&lt;/code&gt;，所以每个 chunk 只有 1 次迭代，然后按轮流方式分给线程。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;schedule(static, 2)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0：0, 1, 6, 7
线程 1：2, 3, 8, 9
线程 2：4, 5, 10, 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;schedule(static, 4)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程 0：0, 1, 2, 3
线程 1：4, 5, 6, 7
线程 2：8, 9, 10, 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果省略 chunk size，默认调度由具体 OpenMP 实现决定；大多数情况下近似于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;schedule(static, total_iterations / thread_count)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是每个线程拿到一段较大的连续迭代范围。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 的优点是调度开销小。如果每次迭代的工作量差不多，它通常是不错的选择。它还有一个好处：后续循环如果使用相同迭代次数，线程通常会被分配到相同范围，这对缓存局部性和 NUMA 系统可能有帮助。&lt;/p&gt;
&lt;h3&gt;调度图的正确读法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/cache-misses-matvec.DTyx66JK_1HpUca.webp&quot; alt=&quot;cache misses matvec&quot;&gt;&lt;/p&gt;
&lt;p&gt;这张图主要画的是：&lt;strong&gt;哪些串行迭代编号被分配给哪个线程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图中的横向位置表示串行循环的迭代编号，例如 &lt;code&gt;0, 1, 2, ...&lt;/code&gt;，不是实际时间轴。&lt;/li&gt;
&lt;li&gt;chunk 是按“串行循环的迭代编号空间”切出来的连续区间。&lt;/li&gt;
&lt;li&gt;图没有体现真实执行时多个线程同时运行的并行性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dynamic&lt;/code&gt; 和 &lt;code&gt;guided&lt;/code&gt; 的具体分配会受运行时调度影响，所以图中只是可能的调度结果之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如 &lt;code&gt;schedule(static, 2)&lt;/code&gt; 中，一个 chunk 可以是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;迭代 0, 1
迭代 2, 3
迭代 4, 5
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些 chunk 会被分配给不同线程。图画的是“编号区间归属”，不是“哪个线程在某个时间点正在执行”。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;dynamic&lt;/code&gt; 调度类型&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dynamic&lt;/code&gt; 调度会把迭代分解成固定大小的连续 chunk，但这些 chunk 不会在循环开始前全部固定分给线程。&lt;/p&gt;
&lt;p&gt;执行过程是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;线程完成一个 chunk -&gt; 向运行时系统请求下一个 chunk -&gt; 直到所有迭代完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel for schedule(dynamic, 2)
for (int i = 0; i &amp;#x3C; n; i++) {
    work(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里每个线程每次领取 2 个连续迭代。哪个线程先完成，就先领取下一个 chunk。&lt;/p&gt;
&lt;p&gt;如果省略 chunk size：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel for schedule(dynamic)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认 chunk size 通常为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 和 &lt;code&gt;dynamic&lt;/code&gt; 的关键区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;：循环开始前就固定分配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dynamic&lt;/code&gt;：运行时先到先得地领取 chunk。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;dynamic&lt;/code&gt; 适合每次迭代耗时不一致的循环。例如后面的迭代计算量更大时，先完成的线程可以继续领取任务，负载更均衡。&lt;/p&gt;
&lt;p&gt;代价是：每次领取 chunk 都要和运行时系统交互，所以会产生调度开销。增大 chunk size 可以减少领取次数，在负载均衡和调度开销之间折中。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;guided&lt;/code&gt; 调度类型&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;guided&lt;/code&gt; 和 &lt;code&gt;dynamic&lt;/code&gt; 类似，也是线程完成一个 chunk 后，再向运行时系统请求下一个 chunk。&lt;/p&gt;
&lt;p&gt;区别在于 chunk 大小：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dynamic, 2：每次都领 2 个迭代
guided, 2：一开始领大块，后面逐渐变小，最小通常不低于 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;guided&lt;/code&gt; 的直观理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;刚开始剩余任务多 -&gt; 分配较大的 chunk，减少调度次数
后面剩余任务少 -&gt; 分配较小的 chunk，方便线程平均收尾
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp parallel for schedule(guided, 2)
for (int i = 0; i &amp;#x3C; 100; i++) {
    work(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能的分配趋势：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;前期：0-24, 25-43, 44-57, ...
中期：69-76, 77-82, 83-87, ...
后期：88-89, 90-91, 92-93, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;2&lt;/code&gt; 是最小 chunk size。后面 chunk 会越来越小，但通常不会小于 &lt;code&gt;2&lt;/code&gt;，最后剩余不足时除外。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;guided&lt;/code&gt; 适合这种情况：每个迭代耗时不完全一样，但又不想像 &lt;code&gt;dynamic, 1&lt;/code&gt; 那样产生太多调度开销。&lt;/p&gt;
&lt;h3&gt;三种调度的记忆方式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;static：提前固定分。
dynamic：运行时按固定小块抢任务。
guided：运行时抢任务，但块大小从大到小。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一句话总结：&lt;code&gt;schedule&lt;/code&gt; 控制的是“循环迭代怎么分给线程”。如果每次迭代工作量接近，&lt;code&gt;static&lt;/code&gt; 通常开销小；如果工作量不均匀，&lt;code&gt;dynamic&lt;/code&gt; 或 &lt;code&gt;guided&lt;/code&gt; 更容易负载均衡，其中 &lt;code&gt;guided&lt;/code&gt; 试图在动态负载均衡和调度开销之间折中。&lt;/p&gt;
&lt;h2&gt;5.5 OpenMP 同步：&lt;code&gt;atomic&lt;/code&gt;、&lt;code&gt;critical&lt;/code&gt; 和锁&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;atomic&lt;/code&gt; 指令&lt;/h3&gt;
&lt;p&gt;如果只是保护==一个共享变量==的&lt;strong&gt;单一更新操作&lt;/strong&gt;，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp atomic
x += 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;atomic&lt;/code&gt; 比 &lt;code&gt;critical&lt;/code&gt; 更窄，只能保护紧接着的一条特定 C 语句，通常用于对一个共享变量进行简单更新。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;code&gt;atomic&lt;/code&gt; 的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp atomic
x += 1;

#pragma omp atomic
x++;

#pragma omp atomic
x = y;      /* 原子写 */

#pragma omp atomic
x *= 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不适合使用 &lt;code&gt;atomic&lt;/code&gt; 的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp atomic
x += y++;   /* 同时修改 x 和 y，不是单一共享变量更新 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一句话记忆：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;atomic：保护一个共享变量的一次简单更新。
critical：保护一整段临界区代码。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;critical&lt;/code&gt; 临界区&lt;/h3&gt;
&lt;p&gt;如果要保护一段代码，可以使用 &lt;code&gt;critical&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp critical
{
    /* 临界区代码 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，所有没有名字的 &lt;code&gt;critical&lt;/code&gt; 区域共享同一把全局锁。也就是说，任何线程都不能同时执行任意一个未命名的 &lt;code&gt;critical&lt;/code&gt; 区域，即使这些区域保护的是完全无关的数据。&lt;/p&gt;
&lt;p&gt;如果不同临界区保护的是不同资源，可以给临界区命名：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp critical(queue0)
{
    /* 只和 queue0 这个名字对应的临界区互斥 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 OpenMP 只会对同名的 &lt;code&gt;critical(queue0)&lt;/code&gt; 加锁，不同名字的临界区可以并行执行。&lt;/p&gt;
&lt;h3&gt;OpenMP 锁函数&lt;/h3&gt;
&lt;p&gt;OpenMP 也提供显式锁函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;omp_init_lock(&amp;#x26;lock);
omp_set_lock(&amp;#x26;lock);
omp_unset_lock(&amp;#x26;lock);
omp_test_lock(&amp;#x26;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;p&gt;| 函数 | 含义 |
|---|---|
| &lt;code&gt;omp_init_lock&lt;/code&gt; | 初始化锁 |
| &lt;code&gt;omp_set_lock&lt;/code&gt; | 获取锁；如果锁不可用就等待 |
| &lt;code&gt;omp_unset_lock&lt;/code&gt; | 释放锁 |
| &lt;code&gt;omp_test_lock&lt;/code&gt; | 尝试获取锁；不一定阻塞 |&lt;/p&gt;
&lt;p&gt;显式锁更灵活，但也更容易写出死锁。简单场景下，优先考虑 &lt;code&gt;critical&lt;/code&gt; 或 &lt;code&gt;atomic&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;嵌套 &lt;code&gt;critical&lt;/code&gt; 和死锁&lt;/h3&gt;
&lt;p&gt;如果嵌套使用未命名 &lt;code&gt;critical&lt;/code&gt;，可能死锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp critical
{
    /* 临界区 A */

    #pragma omp critical
    {
        /* 临界区 B */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：外层已经拿到默认 &lt;code&gt;critical&lt;/code&gt; 的全局锁，内层又试图获取同一把锁，线程会卡在自己已经持有的锁上。&lt;/p&gt;
&lt;p&gt;给临界区命名也不能完全避免死锁。如果两个线程按相反顺序获取两把不同的锁，也可能互相等待：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp critical(one)
{
    #pragma omp critical(two)
    {
        /* 线程 u：先 one 后 two */
    }
}

#pragma omp critical(two)
{
    #pragma omp critical(one)
    {
        /* 线程 v：先 two 后 one */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能发生：&lt;/p&gt;
&lt;p&gt;| 时间 | 线程 u | 线程 v |
|---|---|---|
| 0 | 进入临界区 &lt;code&gt;one&lt;/code&gt; | 进入临界区 &lt;code&gt;two&lt;/code&gt; |
| 1 | 等待临界区 &lt;code&gt;two&lt;/code&gt; | 等待临界区 &lt;code&gt;one&lt;/code&gt; |
| 2 | 阻塞 | 阻塞 |&lt;/p&gt;
&lt;p&gt;这就是死锁。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;atomic&lt;/code&gt; 和 &lt;code&gt;critical&lt;/code&gt; 的区别&lt;/h3&gt;
&lt;p&gt;| 对比项 | &lt;code&gt;atomic&lt;/code&gt; | &lt;code&gt;critical&lt;/code&gt; |
|---|---|---|
| 保护范围 | 一条简单更新语句 | 一段代码块 |
| 适合场景 | &lt;code&gt;x++&lt;/code&gt;、&lt;code&gt;x += value&lt;/code&gt; 这类单变量更新 | 多条语句、复杂数据结构更新 |
| 开销 | 通常更小 | 通常更大 |
| 灵活性 | 较低 | 较高 |
| 是否能替代锁 | 只能替代很简单的锁场景 | 更接近互斥锁 |&lt;/p&gt;
&lt;p&gt;如果临界区只有特定的赋值或更新语句，优先考虑 &lt;code&gt;atomic&lt;/code&gt;。如果不能用 &lt;code&gt;atomic&lt;/code&gt; 表达，再使用 &lt;code&gt;critical&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;注意：&lt;code&gt;atomic&lt;/code&gt; 和 &lt;code&gt;critical&lt;/code&gt; 不是同一把锁。下面代码的输出仍然是不确定的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#pragma omp atomic
x += 1;

#pragma omp critical
x = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;code&gt;atomic&lt;/code&gt; 只保证自己的原子更新，&lt;code&gt;critical&lt;/code&gt; 只保证进入临界区的互斥；它们之间没有自动建立同一个互斥关系。&lt;/p&gt;
&lt;p&gt;一句话总结：OpenMP 的核心思想是“用编译指令标注并行结构”，比 Pthreads 更高层；但只要多个线程访问共享变量，仍然需要考虑作用域、调度和同步。简单单变量更新用 &lt;code&gt;atomic&lt;/code&gt;，复杂临界区用 &lt;code&gt;critical&lt;/code&gt; 或显式锁。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>WSL2 外设与多媒体配置笔记</title><link>https://iaohr9.github.io/blog/wsl2%E5%A4%96%E8%AE%BE%E4%B8%8E%E5%A4%9A%E5%AA%92%E4%BD%93%E9%85%8D%E7%BD%AE%E7%AC%94%E8%AE%B0</link><guid isPermaLink="true">https://iaohr9.github.io/blog/wsl2%E5%A4%96%E8%AE%BE%E4%B8%8E%E5%A4%9A%E5%AA%92%E4%BD%93%E9%85%8D%E7%BD%AE%E7%AC%94%E8%AE%B0</guid><description>WSL2 中 USB 设备转发、摄像头视频流测试与 WSLg 音频重定向的配置和原理整理。</description><pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章由 WSL2 相关笔记合并整理而来，主要覆盖 USB 设备转发、摄像头视频流测试，以及 WSLg 下的音频重定向配置。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;usb通信&lt;/h2&gt;
&lt;h3&gt;Windows 安装 usbipd&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;winget install usbipd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;连接 USB 设备&lt;/h4&gt;
&lt;p&gt;在附加 USB 设备之前，请确保 WSL 命令行处于打开状态。 这会使 WSL 2 轻型 VM 保持运行。&lt;/p&gt;
&lt;p&gt; 备注&lt;/p&gt;
&lt;p&gt;此文档假定已安装 &lt;a href=&quot;https://github.com/dorssel/usbipd-win/releases/latest&quot;&gt;&lt;code&gt;usbipd-win 4.0.0&lt;/code&gt; 或更高版本&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过以 &lt;em&gt;管理员&lt;/em&gt; 模式打开 PowerShell 并输入以下命令列出连接到 Windows 的所有 USB 设备。 列出设备后，选择并复制要附加到 WSL 的设备总线 ID。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;usbipd list
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;在附加 USB 设备之前，必须使用该命令 &lt;code&gt;usbipd bind&lt;/code&gt; 来共享设备，从而允许它附加到 WSL。 这需要管理员权限。 选择要在 WSL 中使用的设备的总线 ID，然后运行以下命令。 运行命令后，请再次使用命令 &lt;code&gt;usbipd list&lt;/code&gt; 验证设备是否共享。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;usbipd bind --busid 4-4
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;若要附加 USB 设备，请运行以下命令。 （不再需要使用提升的管理员提示。确保 WSL 命令提示符处于打开状态，以使 WSL 2 轻型 VM 保持活动状态。 请注意，只要 USB 设备连接到 WSL，Windows 将无法使用它。 一旦连接到 WSL，任何在 WSL 2 上运行的发行版都可以使用该 USB 设备。 请确认设备是否已连接 &lt;code&gt;usbipd list&lt;/code&gt;。 在 WSL 提示符下，运行 &lt;code&gt;lsusb&lt;/code&gt; 以验证 USB 设备是否已列出，并且可以使用 Linux 工具与之交互。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;usbipd attach --wsl --busid &amp;#x3C;busid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;打开 Ubuntu（或首选 WSL 命令行），并使用以下命令列出附加的 USB 设备：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;lsusb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应会看到刚刚附加的设备，并且能够使用普通 Linux 工具与之交互。 根据应用程序，可能需要配置 udev 规则，以允许非根用户访问设备。&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;在 WSL 中使用设备后，可以物理断开 USB 设备的连接，或者从 PowerShell 运行以下命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;usbipd detach --busid &amp;#x3C;busid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;h4&gt;总体架构概述&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;usbipd&lt;/code&gt;（USB/IP Device Manager）的实现基于 &lt;strong&gt;客户端-服务器架构&lt;/strong&gt; 和 &lt;strong&gt;网络化的设备共享&lt;/strong&gt; 理念。其核心原理可以概括为：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将 Windows 主机上的物理 USB 设备通过网络协议&quot;导出&quot;，然后由 WSL 2 内的 Linux 内核通过 USB/IP 客户端协议&quot;导入&quot;并识别为本地设备。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;技术原理分层解析&lt;/h4&gt;
&lt;h5&gt;1. USB/IP 协议基础&lt;/h5&gt;
&lt;p&gt;USB/IP 是一个开源项目，其核心思想是将 USB 协议封装在 TCP/IP 协议中传输：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;物理USB设备 → USB协议数据 → TCP/IP封装 → 网络传输 → TCP/IP解封装 → 虚拟USB设备
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. Windows 端（服务器端）工作原理&lt;/h5&gt;
&lt;h6&gt;a. 设备识别与绑定&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在Windows PowerShell中
usbipd list
# 输出显示所有可用USB设备及其BUSID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：&lt;code&gt;usbipd&lt;/code&gt; 通过 Windows Driver Kit (WDK) 接口枚举系统上的 USB 设备，获取每个设备的详细信息。&lt;/p&gt;
&lt;h6&gt;b. 设备共享（绑定）&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;usbipd bind --busid &amp;#x3C;BUSID&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;底层操作&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建虚拟 USB 控制器&lt;/strong&gt;：为指定设备创建虚拟的 USB 主机控制器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议封装&lt;/strong&gt;：将 USB 请求块 (URB) 序列化为 USB/IP 协议格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络服务启动&lt;/strong&gt;：在指定端口 (3240) 监听连接请求&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;c. 网络传输层&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议&lt;/strong&gt;：TCP/IP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端口&lt;/strong&gt;：默认 3240&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据格式&lt;/strong&gt;：自定义的 USB/IP 协议头 + USB 数据载荷&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3. WSL 2 端（客户端）工作原理&lt;/h5&gt;
&lt;h6&gt;a. USB/IP 客户端驱动&lt;/h6&gt;
&lt;p&gt;WSL 2 内核已经编译了 &lt;code&gt;usbip-host&lt;/code&gt; 驱动模块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在WSL2中检查驱动
lsmod | grep usbip
modprobe usbip-host    # 加载驱动
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;b. 设备附加流程&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 连接到Windows主机并附加设备
usbip attach -r &amp;#x3C;Windows-IP&gt; -b &amp;#x3C;BUSID&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;详细过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TCP 连接建立&lt;/strong&gt;：连接到 Windows 主机的 3240 端口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备枚举&lt;/strong&gt;：获取可用的共享设备列表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动绑定&lt;/strong&gt;：将远程设备绑定到本地虚拟 USB 端口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备节点创建&lt;/strong&gt;：在 &lt;code&gt;/dev/&lt;/code&gt; 下创建相应的设备文件&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;4. 数据传输流程&lt;/h5&gt;
&lt;h6&gt;请求路径（WSL 2 → Windows）&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;应用程序 → Linux USB子系统 → usbip-host驱动 → TCP socket → 网络 → 
Windows TCP栈 → usbipd服务 → 解析USB/IP协议 → 原生USB API调用 → 物理设备
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;响应路径（Windows → WSL 2）&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;物理设备响应 → 原生USB API → usbipd服务 → USB/IP协议封装 → 网络传输 → 
WSL2 TCP栈 → usbip-host驱动 → 协议解析 → Linux USB子系统 → 应用程序
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5. 内核层面的交互&lt;/h5&gt;
&lt;h6&gt;Windows 内核层面：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;usbipd. Sys&lt;/strong&gt;：内核模式驱动程序，处理实际的 USB 通信&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户模式服务&lt;/strong&gt;：管理连接和设备状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与原生 USB 栈交互&lt;/strong&gt;：通过标准的 Windows USB 驱动程序接口&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;Linux 内核层面（WSL 2）：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;usbip-core. Ko&lt;/strong&gt;：核心协议处理模块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;usbip-host. Ko&lt;/strong&gt;：客户端功能实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟 USB 主机控制器&lt;/strong&gt;：模拟本地 USB 硬件&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;6. 网络通信细节&lt;/h5&gt;
&lt;h6&gt;协议数据包结构：&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct usbip_header {
    uint32_t command;        // 命令类型
    uint32_t seqnum;         // 序列号
    uint32_t devid;          // 设备ID
    uint32_t direction;      // 数据传输方向
    uint32_t ep;             // 端点号
    // ... 其他字段
    uint32_t transfer_buffer_length; // 数据长度
    // 实际USB数据跟随在后
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;主要命令类型：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;USBIP_CMD_SUBMIT&lt;/code&gt;：提交 USB 请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USBIP_CMD_UNLINK&lt;/code&gt;：取消请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USBIP_RET_SUBMIT&lt;/code&gt;：请求完成响应&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;7. 性能考量与优化&lt;/h5&gt;
&lt;h6&gt;延迟处理：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批量传输缓冲&lt;/strong&gt;：对批量传输进行缓冲以减少小包开销&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等时传输优化&lt;/strong&gt;：对音频、视频等实时设备特殊处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零拷贝技术&lt;/strong&gt;：在可能的情况下避免数据复制&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;带宽管理：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TCP 拥塞控制&lt;/strong&gt;：依赖 TCP 的流量控制机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务质量 (QoS)&lt;/strong&gt;：优先处理实时设备数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;8. 安全性机制&lt;/h5&gt;
&lt;h6&gt;连接认证：&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 基于IP地址的访问控制
usbipd bind --busid &amp;#x3C;BUSID&gt; --allowed-principals &amp;#x3C;IP-address&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;数据传输安全：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地回环网络&lt;/strong&gt;：默认使用 127.0.0.1，不暴露到外部网络&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无加密&lt;/strong&gt;：USB/IP 协议本身不加密，但在本地环境下可接受&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;9. 设备类型支持差异&lt;/h5&gt;
&lt;h6&gt;良好支持的设备：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;USB 存储设备（U 盘、移动硬盘）&lt;/li&gt;
&lt;li&gt;USB 串口设备&lt;/li&gt;
&lt;li&gt;USB 网络适配器&lt;/li&gt;
&lt;li&gt;HID 设备（键盘、鼠标）&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;有限支持的设备：&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;等时传输设备&lt;/strong&gt;：摄像头、音频设备（有延迟问题）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高速设备&lt;/strong&gt;：USB 3.0+设备可能受网络带宽限制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特殊功能设备&lt;/strong&gt;：需要特定驱动程序的设备&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;10. 与 WSLg 的协同工作&lt;/h5&gt;
&lt;p&gt;当同时使用 USBIP 和 WSLg 时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;物理USB设备 → usbipd (Windows) → USB/IP协议 → WSL2 usbip-client → 
Linux设备节点 → 应用程序 → WSLg音频重定向 → Windows音频输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种架构允许某些设备通过 USB/IP 直接访问，而其他设备（如音频）通过 WSLg 重定向。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;视频流&lt;/h2&gt;
&lt;h3&gt;1. Share 端口&lt;/h3&gt;
&lt;p&gt;用 &lt;a href=&quot;#%E8%BF%9E%E6%8E%A5-usb-%E8%AE%BE%E5%A4%87&quot;&gt;连接 USB 设备&lt;/a&gt; 这里的方法，将摄像头先 share，再 attach 到 wsl 中&lt;/p&gt;
&lt;h3&gt;2. 设置环境变量&lt;/h3&gt;
&lt;p&gt;由于 X11 共享内存（MIT-SHM）扩展权限问题导致的。wsl和windows宿主机共享了x11内存，导致wsl无法直接使用摄像头&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export XLIB_SKIP_ARGB_VISUALS=1
export LIBGL_ALWAYS_INDIRECT=0
# guvcview # 测试摄像头的录制软件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 测试代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import cv2
import time

def display_camera_feed():
    &quot;&quot;&quot;
    优化的摄像头实时显示，解决卡顿问题
    按 &apos;q&apos; 键退出程序
    &quot;&quot;&quot;
    # 打开默认摄像头
    cap = cv2.VideoCapture(0, cv2.CAP_V4L2)
    
    # 检查摄像头是否成功打开
    if not cap.isOpened():
        print(&quot;错误：无法打开摄像头&quot;)
        return
    
    # 关键优化：设置缓冲区大小为1，避免帧积压
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    
    # 设置视频格式和参数
    cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(&apos;M&apos;, &apos;J&apos;, &apos;P&apos;, &apos;G&apos;))
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
    cap.set(cv2.CAP_PROP_FPS, 30)
    
    print(&quot;摄像头初始化中...&quot;)
    time.sleep(1)
    print(&quot;摄像头已打开，按 &apos;q&apos; 键退出&quot;)
    
    while True:
        # 读取帧
        ret, frame = cap.read()
        
        if not ret or frame is None:
            print(&quot;警告：无法读取帧，跳过此帧&quot;)
            continue
        
        # 关键优化：清空缓冲区，确保获取最新帧
        cap.grab()
        
        # 显示图像
        cv2.imshow(&apos;Camera Feed&apos;, frame)
        
        # 关键优化：使用更短的等待时间
        if cv2.waitKey(1) &amp;#x26; 0xFF  ord(&apos;q&apos;):
            break
    
    # 清理资源
    cap.release()
    cv2.destroyAllWindows()

def display_camera_feed_alternative():
    &quot;&quot;&quot;
    备选方案：使用线程读取帧，进一步优化性能
    &quot;&quot;&quot;
    import threading
    from queue import Queue
    
    # 帧队列
    frame_queue = Queue(maxsize=2)
    stop_event = threading.Event()
    
    def capture_frames():
        cap = cv2.VideoCapture(0, cv2.CAP_V4L2)
        cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
        cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(&apos;M&apos;, &apos;J&apos;, &apos;P&apos;, &apos;G&apos;))
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
        cap.set(cv2.CAP_PROP_FPS, 30)
        
        while not stop_event.is_set():
            ret, frame = cap.read()
            if ret and frame is not None:
                # 如果队列满了，丢弃旧帧
                if frame_queue.full():
                    try:
                        frame_queue.get_nowait()
                    except:
                        pass
                frame_queue.put(frame)
        
        cap.release()
    
    # 启动捕获线程
    capture_thread = threading.Thread(target=capture_frames)
    capture_thread.start()
    
    print(&quot;摄像头已启动（线程版本），按 &apos;q&apos; 键退出&quot;)
    
    try:
        while True:
            if not frame_queue.empty():
                frame = frame_queue.get()
                cv2.imshow(&apos;Camera Feed (Threaded)&apos;, frame)
            
            if cv2.waitKey(1) &amp;#x26; 0xFF  ord(&apos;q&apos;):
                break
    finally:
        stop_event.set()
        capture_thread.join()
        cv2.destroyAllWindows()

if __name__  &quot;__main__&quot;:
    display_camera_feed()
	# display_camera_feed_alternative() # 备选方案 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可看到视频画面了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;音频重定向&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;使用WSLg的音频重定向&lt;/strong&gt;&lt;br&gt;
较新版本的WSL2引入了WSLg，它提供了音频重定向功能，无需额外配置即可使用Windows的音频设备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保WSL2版本足够新：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; wsl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果版本低于1.0.0，请更新WSL2：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在Windows PowerShell中运行   
wsl --update
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;安装必要的音频工具：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get update

sudo apt-get install -y pulseaudio alsa-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;配置PulseAudio使用WSLg的音频服务：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建或编辑PulseAudio配置文件
mkdir -p ~/.config/pulse
echo &quot;default-server = unix:/mnt/wslg/PulseServer&quot; &gt; ~/.config/pulse/client.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;测试音频重定向：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 安装一个简单的音频测试工具
sudo apt-get install -y sox 
# 测试录音
rec test.wav
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后应该就可以正常的在WSL2当中使用麦克风设备了&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;然后播放&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;play test.wav
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS 2 基础与常用开发笔记</title><link>https://iaohr9.github.io/blog/ros2%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B8%B8%E7%94%A8%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0</link><guid isPermaLink="true">https://iaohr9.github.io/blog/ros2%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B8%B8%E7%94%A8%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0</guid><description>ROS 2 基础概念、命令、工作空间、节点通信、DDS、Launch、TF 坐标系与 URDF 的整理笔记。</description><pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章由 ROS 学习笔记合并整理而来，按基础概念、常用命令、工作空间、节点通信机制、DDS、Launch、坐标系管理和 URDF 的顺序组织。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 什么是ROS&lt;/h2&gt;
&lt;p&gt;机器人界的 Android&lt;/p&gt;
&lt;h3&gt;1 和 2 的区别&lt;/h3&gt;
&lt;h4&gt;架构&lt;/h4&gt;
&lt;p&gt;ROS 1 的架构下，所有节点需要使用 Master 进行管理
ROS 2 使用基于 DDS 的 Discovery 机制，和 Master 说拜拜&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-01.jpg&quot; alt=&quot;PixPin_2025-09-01_18-02-25&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-02.jpg&quot; alt=&quot;PixPin_2025-09-01_18-04-11&quot;&gt;&lt;/p&gt;
&lt;p&gt;DDS 协议：数据分发服务&lt;/p&gt;
&lt;p&gt;以数据为中心&lt;/p&gt;
&lt;h4&gt;接口&lt;/h4&gt;
&lt;p&gt;重设 api&lt;/p&gt;
&lt;h4&gt;编译器&lt;/h4&gt;
&lt;p&gt;新的编译系统 ament colcon&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. ROS CMD&lt;/h2&gt;
&lt;h4&gt;1. 核心概念与帮助&lt;/h4&gt;
&lt;p&gt;在任何命令后加上 &lt;code&gt;-h&lt;/code&gt; 或 &lt;code&gt;--help&lt;/code&gt; 可以查看详细帮助。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 -h
ros2 run -h
ros2 node list -h
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 节点 (Node) 相关命令&lt;/h4&gt;
&lt;p&gt;节点是 ros2 中可执行的进程。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 node list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;当前所有活跃节点 | &lt;code&gt;--include-hidden&lt;/code&gt; (显示隐藏节点) |
| &lt;code&gt;ros2 node info &amp;#x3C;node_name&gt;&lt;/code&gt; | 查看某个节点的&lt;strong&gt;详细信息&lt;/strong&gt;（发布/订阅的主题、服务等） | |
| &lt;code&gt;ros2 node status&lt;/code&gt; | (Galactic+) 显示节点的状态统计信息 | |&lt;/p&gt;
&lt;h4&gt;3. 话题 (Topic) 相关命令&lt;/h4&gt;
&lt;p&gt;话题是节点间发布/订阅的异步通信机制。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 topic list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;所有活跃话题 | &lt;code&gt;-t&lt;/code&gt; (显示话题类型), &lt;code&gt;--include-hidden&lt;/code&gt; |
| &lt;code&gt;ros2 topic echo &amp;#x3C;topic_name&gt;&lt;/code&gt; | &lt;strong&gt;显示&lt;/strong&gt;指定话题上发布的消息 | &lt;code&gt;-w &amp;#x3C;num&gt;&lt;/code&gt; (固定长度历史消息) |
| &lt;code&gt;ros2 topic info &amp;#x3C;topic_name&gt;&lt;/code&gt; | 查看话题的&lt;strong&gt;信息&lt;/strong&gt;（类型、发布/订阅者数量） | |
| &lt;code&gt;ros2 topic type &amp;#x3C;topic_name&gt;&lt;/code&gt; | 查看话题的&lt;strong&gt;消息类型&lt;/strong&gt; | |
| &lt;code&gt;ros2 topic pub &amp;#x3C;topic_name&gt; &amp;#x3C;msg_type&gt; &amp;#x3C;data&gt;&lt;/code&gt; | &lt;strong&gt;向&lt;/strong&gt;指定话题&lt;strong&gt;发布消息&lt;/strong&gt; | &lt;code&gt;-1&lt;/code&gt; (发布一次后退出), &lt;code&gt;-r &amp;#x3C;rate&gt;&lt;/code&gt; (以固定频率发布) |
| &lt;code&gt;ros2 topic hz &amp;#x3C;topic_name&gt;&lt;/code&gt; | &lt;strong&gt;统计&lt;/strong&gt;话题消息的&lt;strong&gt;发布频率&lt;/strong&gt; | |
| &lt;code&gt;ros2 topic bw &amp;#x3C;topic_name&gt;&lt;/code&gt; | &lt;strong&gt;统计&lt;/strong&gt;话题消息的&lt;strong&gt;带宽&lt;/strong&gt; | |
| &lt;code&gt;ros2 topic delay &amp;#x3C;topic_name&gt;&lt;/code&gt; | (Galactic+) 显示话题消息的&lt;strong&gt;延迟&lt;/strong&gt; | |&lt;/p&gt;
&lt;h4&gt;4. 服务 (Service) 相关命令&lt;/h4&gt;
&lt;p&gt;服务是节点间请求/响应的同步通信机制。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 service list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;所有活跃服务 | &lt;code&gt;-t&lt;/code&gt; (显示服务类型), &lt;code&gt;--include-hidden&lt;/code&gt; |
| &lt;code&gt;ros2 service type &amp;#x3C;service_name&gt;&lt;/code&gt; | 查看服务的&lt;strong&gt;类型&lt;/strong&gt; | |
| &lt;code&gt;ros2 service find &amp;#x3C;srv_type&gt;&lt;/code&gt; | 查找提供指定类型服务的&lt;strong&gt;所有服务名&lt;/strong&gt; | |
| &lt;code&gt;ros2 service call &amp;#x3C;service_name&gt; &amp;#x3C;srv_type&gt; &amp;#x3C;arguments&gt;&lt;/code&gt; | &lt;strong&gt;调用&lt;/strong&gt;一个服务 | &lt;code&gt;--wait&lt;/code&gt; (等待服务可用) |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：调用服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 调用 /spawn 服务，生成第二个乌龟
ros2 service call /spawn turtlesim/srv/Spawn &apos;{x: 5.5, y: 9.0, theta: 1.57, name: &quot;turtle2&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 参数 (Parameter) 相关命令&lt;/h4&gt;
&lt;p&gt;参数是节点的配置值，可以在运行时动态修改。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 param list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;某个节点的所有参数 | &lt;code&gt;--param-type&lt;/code&gt; (显示参数类型) |
| &lt;code&gt;ros2 param get &amp;#x3C;node_name&gt; &amp;#x3C;param_name&gt;&lt;/code&gt; | &lt;strong&gt;获取&lt;/strong&gt;某个节点的参数&lt;strong&gt;值&lt;/strong&gt; | |
| &lt;code&gt;ros2 param set &amp;#x3C;node_name&gt; &amp;#x3C;param_name&gt; &amp;#x3C;value&gt;&lt;/code&gt; | &lt;strong&gt;设置&lt;/strong&gt;某个节点的参数&lt;strong&gt;值&lt;/strong&gt; | |
| &lt;code&gt;ros2 param dump &amp;#x3C;node_name&gt;&lt;/code&gt; | 将节点的&lt;strong&gt;所有参数导出&lt;/strong&gt;到 YAML 文件 | &lt;code&gt;--output-dir &amp;#x3C;dir&gt;&lt;/code&gt; (指定输出目录) |
| &lt;code&gt;ros2 param load &amp;#x3C;node_name&gt; &amp;#x3C;parameter_file&gt;&lt;/code&gt; | 从 YAML 文件&lt;strong&gt;加载参数&lt;/strong&gt;到节点 | |
| &lt;code&gt;ros2 param delete &amp;#x3C;node_name&gt; &amp;#x3C;param_name&gt;&lt;/code&gt; | &lt;strong&gt;删除&lt;/strong&gt;节点的某个参数 | |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：参数操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出 /turtlesim 节点的所有参数
ros2 param list /turtlesim

# 设置背景颜色的红色通道值
ros2 param set /turtlesim background_r 150

# 将所有参数导出到当前目录的 turtlesim. Yaml 文件
ros2 param dump /turtlesim &gt; turtlesim. Yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. 动作 (Action) 相关命令&lt;/h4&gt;
&lt;p&gt;动作是长时间运行、可抢占、带有反馈的任务通信机制。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 action list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;所有活跃动作 | &lt;code&gt;-t&lt;/code&gt; (显示动作类型) |
| &lt;code&gt;ros2 action info &amp;#x3C;action_name&gt;&lt;/code&gt; | 查看动作的&lt;strong&gt;信息&lt;/strong&gt;（客户端/服务器） | |
| &lt;code&gt;ros2 action type &amp;#x3C;action_name&gt;&lt;/code&gt; | 查看动作的&lt;strong&gt;类型&lt;/strong&gt; | |
| &lt;code&gt;ros2 action send_goal &amp;#x3C;action_name&gt; &amp;#x3C;action_type&gt; &amp;#x3C;goal_value&gt;&lt;/code&gt; | &lt;strong&gt;发送&lt;/strong&gt;一个动作目标 | &lt;code&gt;--feedback&lt;/code&gt; (显示反馈), &lt;code&gt;--cancel-after &amp;#x3C;sec&gt;&lt;/code&gt; (N 秒后取消) |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：发送动作目标&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 让乌龟旋转 360 度，并显示反馈信息
ros2 action send_goal /turtle 1/rotate_absolute turtlesim/action/RotateAbsolute &quot;theta: 6.28&quot; --feedback
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. 功能包 (Package) 和构建 (Build) 相关命令&lt;/h4&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 pkg list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;所有已安装的功能包 | |
| &lt;code&gt;ros2 pkg create &amp;#x3C;pkg_name&gt;&lt;/code&gt; | &lt;strong&gt;创建一个新的&lt;/strong&gt;功能包 | &lt;code&gt;--build-type {ament_cmake, ament_python}&lt;/code&gt;, &lt;code&gt;--dependencies &amp;#x3C;deps&gt;&lt;/code&gt;, &lt;code&gt;--node-name &amp;#x3C;node_name&gt;&lt;/code&gt; |
| &lt;code&gt;ros2 pkg prefix &amp;#x3C;pkg_name&gt;&lt;/code&gt; | 查看功能包的&lt;strong&gt;安装路径&lt;/strong&gt; | |
| &lt;code&gt;ros2 pkg executables &amp;#x3C;pkg_name&gt;&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;功能包中的&lt;strong&gt;可执行文件&lt;/strong&gt; | |
| &lt;code&gt;ros2 pkg xml &amp;#x3C;pkg_name&gt;&lt;/code&gt; | 输出功能包的 &lt;strong&gt;&lt;code&gt;package. Xml&lt;/code&gt;&lt;/strong&gt; 内容 | |
| &lt;code&gt;colcon build&lt;/code&gt; | &lt;strong&gt;编译&lt;/strong&gt;工作空间中的功能包 | &lt;code&gt;--packages-select &amp;#x3C;pkg_name&gt;&lt;/code&gt; (只编译指定包), &lt;code&gt;--symlink-install&lt;/code&gt; (符号链接安装，便于 Python 开发) |
| &lt;code&gt;colcon list&lt;/code&gt; | 列出工作空间中所有功能包 | |&lt;/p&gt;
&lt;h4&gt;8. 运行 (Run) 和启动 (Launch) 相关命令&lt;/h4&gt;
&lt;p&gt;| 命令 | 描述 | 常用选项 |
| :--- | :--- | :--- |
| &lt;code&gt;ros2 run &amp;#x3C;pkg_name&gt; &amp;#x3C;executable_name&gt;&lt;/code&gt; | &lt;strong&gt;直接运行&lt;/strong&gt;功能包中的某个&lt;strong&gt;可执行节点&lt;/strong&gt; | &lt;code&gt;--ros-args&lt;/code&gt; (传递 ROS 参数), &lt;code&gt;--param &amp;#x3C;name&gt;:=&amp;#x3C;value&gt;&lt;/code&gt; |
| &lt;code&gt;ros2 launch &amp;#x3C;pkg_name&gt; &amp;#x3C;launch_file.py&gt;&lt;/code&gt; | &lt;strong&gt;通过启动文件运行&lt;/strong&gt;一组节点 | &lt;code&gt;--arguments &amp;#x3C;args&gt;&lt;/code&gt;, &lt;code&gt;--params-file &amp;#x3C;file&gt;&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：运行节点&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 运行 turtlesim 包中的 turtlesim_node 节点，并设置节点名和参数
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle --param background_r:=100
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9. 接口 (Interface) 相关命令&lt;/h4&gt;
&lt;p&gt;接口是消息 (. Msg)、服务 (. Srv) 和动作 (. Action) 的定义。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 |
| :--- | :--- |
| &lt;code&gt;ros2 interface list&lt;/code&gt; | &lt;strong&gt;列出&lt;/strong&gt;所有可用的接口 |
| &lt;code&gt;ros2 interface show &amp;#x3C;interface_name&gt;&lt;/code&gt; | &lt;strong&gt;显示&lt;/strong&gt;某个接口的&lt;strong&gt;详细定义&lt;/strong&gt;（字段和类型） |
| &lt;code&gt;ros2 interface package &amp;#x3C;pkg_name&gt;&lt;/code&gt; | 列出某个功能包定义的&lt;strong&gt;所有接口&lt;/strong&gt; |
| &lt;code&gt;ros2 interface packages&lt;/code&gt; | 列出所有&lt;strong&gt;包含接口定义&lt;/strong&gt;的功能包 |&lt;/p&gt;
&lt;h4&gt;10. 分布式通信设置&lt;/h4&gt;
&lt;p&gt;在多机系统中至关重要。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 |
| :--- | :--- |
| &lt;code&gt;export ROS_DOMAIN_ID=&amp;#x3C;ID&gt;&lt;/code&gt; | (Linux/macOS) &lt;strong&gt;设置域 ID&lt;/strong&gt;，只有相同 ID 的节点才能通信 |
| &lt;code&gt;set ROS_DOMAIN_ID=&amp;#x3C;ID&gt;&lt;/code&gt; | (Windows) 设置域 ID |
| &lt;code&gt;export ROS_MASTER_URI=http://&amp;#x3C;host&gt;:11311&lt;/code&gt; | 设置主节点 URI (ROS 1 兼容性/某些桥接场景) |&lt;/p&gt;
&lt;h4&gt;11. 数据录制与回放 (Bag)&lt;/h4&gt;
&lt;p&gt;| 命令                                      | 描述                      | 常用选项                          |
| :-------------------------------------- | :---------------------- | :---------------------------- |
| &lt;code&gt;ros2 bag record &amp;#x3C;topic1&gt; &amp;#x3C;topic2&gt; ...&lt;/code&gt; | &lt;strong&gt;录制&lt;/strong&gt;指定话题的数据           | &lt;code&gt;-o &amp;#x3C;name&gt;&lt;/code&gt; (设置输出文件名)         |
| &lt;code&gt;ros2 bag play &amp;#x3C;bag_file&gt;&lt;/code&gt;              | &lt;strong&gt;回放&lt;/strong&gt;录制的数据包            | &lt;code&gt;-r &amp;#x3C;rate&gt;&lt;/code&gt; (回放速率 multiplier) |
| &lt;code&gt;ros2 bag info &amp;#x3C;bag_file&gt;&lt;/code&gt;              | 查看数据包的&lt;strong&gt;信息&lt;/strong&gt;（包含的话题、时长等） |                               |
| &lt;code&gt;ros2 bag reindex &amp;#x3C;bag_file&gt;&lt;/code&gt;           | &lt;strong&gt;修复&lt;/strong&gt;损坏的数据包            |                               |&lt;/p&gt;
&lt;h4&gt;12. 守护进程管理&lt;/h4&gt;
&lt;p&gt;ros2 CLI 使用一个守护进程来缓存节点、话题等信息，加速查询。&lt;/p&gt;
&lt;p&gt;| 命令 | 描述 |
| :--- | :--- |
| &lt;code&gt;ros2 daemon status&lt;/code&gt; | 检查守护进程&lt;strong&gt;状态&lt;/strong&gt; |
| &lt;code&gt;ros2 daemon stop&lt;/code&gt; | &lt;strong&gt;停止&lt;/strong&gt;守护进程 |
| &lt;code&gt;ros2 daemon start&lt;/code&gt; | &lt;strong&gt;启动&lt;/strong&gt;守护进程 |
| &lt;code&gt;ros2 daemon restart&lt;/code&gt; | &lt;strong&gt;重启&lt;/strong&gt;守护进程（在发现命令响应异常时首先尝试） |&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;🚀 综合实战示例：Turtlesim&lt;/h4&gt;
&lt;p&gt;这是一个综合使用多种命令的经典示例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动模拟器：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run turtlesim turtlesim_node
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看节点和话题：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 node list        # 应看到 /turtlesim
ros2 topic list -t    # 应看到 /turtle 1/cmd_vel, /turtle 1/pose 等
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;控制乌龟运动（新开终端）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run turtlesim turtle_teleop_key
# 此时可以用键盘方向键控制乌龟移动
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看节点信息：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 node info /turtlesim # 查看 turtlesim 节点发布了/订阅了哪些话题和服务
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;录制数据（新开终端）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 bag record /turtle 1/cmd_vel /turtle 1/pose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制乌龟运动一会儿，然后 &lt;code&gt;Ctrl+C&lt;/code&gt; 停止录制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;回放数据（停止其他所有节点）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 bag play &amp;#x3C;生成的bag文件名&gt;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到乌龟按照刚才的路径自动运动，即使没有运行 &lt;code&gt;teleop&lt;/code&gt; 节点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这份超全的指南能成为你学习和开发 ros2 的得力助手！&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. workspace &amp;#x26; pkg&lt;/h2&gt;
&lt;h3&gt;ROS 2 工作空间管理&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;存放项目开发相关文件的文件夹&lt;/p&gt;
&lt;p&gt;Src: 代码空间 (Source Space)
Install: 安装空间 (Install Space)
Build：编译空间 (Build Space)
Log: 日志空间 (Log Space)&lt;/p&gt;
&lt;h4&gt;1. Rosdepc - ROS 依赖管理工具 (中国用户推荐)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rosdepc&lt;/code&gt; 是 &lt;code&gt;rosdep&lt;/code&gt; 的国内镜像版，解决了直接使用官方 &lt;code&gt;rosdep&lt;/code&gt; 更新慢、失败率高的问题。&lt;/p&gt;
&lt;h5&gt;安装 rosdepc&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 通过 pip 安装（推荐）
sudo pip install rosdepc

# 如果系统默认 python 是 python3，也可以使用 pip3
# sudo pip3 install rosdepc
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;初始化 rosdepc&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;只需在每台电脑上执行一次&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 初始化 rosdepc
sudo rosdepc init

# 更新 rosdepc 的数据库（从国内源，速度很快）
rosdepc update
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;核心使用方法&lt;/h5&gt;
&lt;p&gt;在&lt;strong&gt;工作空间的 &lt;code&gt;src&lt;/code&gt; 目录同级&lt;/strong&gt;（即 &lt;code&gt;ros2_ws&lt;/code&gt; 目录下）执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 安装工作空间中所有功能包所需的系统依赖
rosdepc install -i --from-path src --rosdistro $ROS_DISTRO -y

# 分解说明：
# -i: --ignore-packages-from-source, 忽略源中的包（通常不需要，可省略）
# --from-path src: 从指定的 src 路径查找 package.xml
# --rosdistro $ROS_DISTRO: 指定你的 ROS 发行版（如 humble, iron 等）
# -y: 对所有提示自动回答 yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实际例子&lt;/strong&gt;（假设你的 ROS 版本是 humble）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
rosdepc install --from-path src --rosdistro humble -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;检查依赖&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 检查但不安装依赖（用于查看缺少哪些依赖）
rosdepc check --from-path src --rosdistro $ROS_DISTRO
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Colcon - ROS 2 构建工具&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;colcon&lt;/code&gt; 是 ROS 2 的构建系统工具，替代了 ROS 1 中的 &lt;code&gt;catkin_make&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;安装 colcon&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ubuntu/Debian
sudo apt install python3-colcon-common-extensions

# 或者通过 pip
sudo pip install colcon-common-extensions
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;核心构建命令&lt;/h5&gt;
&lt;p&gt;在&lt;strong&gt;工作空间根目录&lt;/strong&gt;（&lt;code&gt;~/ros2_ws&lt;/code&gt;）下执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 基本构建：编译所有包
colcon build

# 编译特定包（最常用！）
colcon build --packages-select &amp;#x3C;your_package_name&gt;

# 编译时跳过某些包
colcon build --packages-skip &amp;#x3C;package_to_skip&gt;

# 并行编译以加快速度（使用所有CPU核心）
colcon build --parallel-workers $(nproc)

# 编译并创建符号链接（Python开发推荐）
colcon build --symlink-install

# 编译时跳过测试
colcon build --cmake-args -DBUILD_TESTING=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;常用组合命令&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 开发中最常用的组合：只编译一个包，使用符号链接，并行编译
colcon build --packages-select my_package --symlink-install --parallel-workers $(nproc)

# 编译并忽略依赖错误（谨慎使用）
colcon build --packages-select my_package --allow-overriding my_package
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;清理和重建&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 清理特定包的构建文件
colcon build --packages-select my_package --cmake-clean-first

# 彻底删除构建产物（需要重新编译）
rm -rf build install log

# 或者只删除特定包的构建产物
rm -rf build/my_package install/my_package
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;信息查询命令&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出工作空间中的所有包
colcon list

# 显示包的详细信息
colcon info &amp;#x3C;package_name&gt;

# 列出包中的可执行文件
colcon list --packages-select &amp;#x3C;package_name&gt; --executables
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 完整工作流程示例&lt;/h4&gt;
&lt;p&gt;假设你要创建一个名为 &lt;code&gt;my_robot&lt;/code&gt; 的新包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 创建并进入工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws

# 2. 创建Python包（假设依赖 rclpy 和 std_msgs）
ros2 pkg create my_robot --build-type ament_python --dependencies rclpy std_msgs

# 3. 安装系统依赖（关键步骤！）
rosdepc install -i --from-path src --rosdistro humble -y

# 4. 编写你的代码（编辑 src/my_robot/my_robot/my_robot_node.py 等文件）

# 5. 编译包（使用符号链接便于Python开发）
colcon build --packages-select my_robot --symlink-install

# 6. 激活工作空间
source install/setup.bash

# 7. 运行你的节点
ros2 run my_robot my_robot_node
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 故障排除技巧&lt;/h4&gt;
&lt;h5&gt;Rosdepc 常见问题&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 如果初始化失败，可以尝试删除后重新初始化
sudo rm -rf /etc/ros/rosdepc
sudo rosdepc init
rosdepc update

# 如果某个依赖安装失败，可以手动安装
# 先查看需要什么依赖
rosdepc check --from-path src --rosdistro humble
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Colcon 常见问题&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 如果编译失败，查看详细错误信息
colcon build --packages-select my_package --event-handlers console_direct+

# 如果Python包修改后没生效，检查是否使用了 --symlink-install
# 如果没有，需要重新编译或者使用符号链接重新构建
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 高级用法&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 运行测试
colcon test --packages-select my_package

# 生成编译数据库（用于IDE代码补全）
colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# 使用特定CMake配置
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release

# 编译后运行所有测试并显示结果
colcon test
colcon test-result --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记住这个开发循环：&lt;strong&gt;修改代码 → 编译 (&lt;code&gt;colcon build&lt;/code&gt;) → 运行测试&lt;/strong&gt;。使用 &lt;code&gt;--packages-select&lt;/code&gt; 可以显著提高开发效率，特别是大型项目中&lt;/p&gt;
&lt;p&gt;好的，我们来详细梳理在工作空间（Workspace）中，功能包（Package）从创建、编写、构建到测试的完整流程和命令。&lt;/p&gt;
&lt;h3&gt;ROS 2 工作空间内功能包 (Package) 完整管理指南&lt;/h3&gt;
&lt;h4&gt;1. 准备工作：创建与初始化工作空间&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 创建工作空间目录结构
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws

# 2. （可选但推荐）安装工作空间内所有包所需的系统依赖
# 注意：先确保已安装并初始化 rosdepc（如前所述）
rosdepc install -i --from-path src --rosdistro $ROS_DISTRO -y
# 示例（如果您的ROS2版本是Humble）：
# rosdepc install -i --from-path src --rosdistro humble -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 功能包创建 (Package Creation)&lt;/h4&gt;
&lt;h5&gt;创建新包的基本命令格式&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
ros2 pkg create &amp;#x3C;package_name&gt; --build-type &amp;#x3C;build_type&gt; --dependencies &amp;#x3C;dependencies&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;具体示例&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;创建 C++ 包：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 pkg create my_cpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建 Python 包：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 pkg create my_py_pkg --build-type ament_python --dependencies rclpy std_msgs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建包含特定节点的包：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建时直接指定节点名
ros2 pkg create my_robot --build-type ament_cmake --node-name my_robot_node --dependencies rclcpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建包含特定 launch 文件的包：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建时直接指定launch文件名
ros2 pkg create my_launch_pkg --build-type ament_cmake --dependencies rclcpp --launch-file my_launch.launch.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;创建命令的常用选项&lt;/h5&gt;
&lt;p&gt;| 选项 | 说明 | 示例 |
|------|------|------|
| &lt;code&gt;--build-type&lt;/code&gt; | 指定构建类型 | &lt;code&gt;ament_cmake&lt;/code&gt;, &lt;code&gt;ament_python&lt;/code&gt; |
| &lt;code&gt;--dependencies&lt;/code&gt; | 指定 ROS 2 依赖 | &lt;code&gt;rclcpp rclpy std_msgs sensor_msgs&lt;/code&gt; |
| &lt;code&gt;--node-name&lt;/code&gt; | 创建时同时生成节点模板 | &lt;code&gt;my_node&lt;/code&gt; |
| &lt;code&gt;--license&lt;/code&gt; | 指定许可证 | &lt;code&gt;Apache-2.0&lt;/code&gt;, &lt;code&gt;MIT&lt;/code&gt; |
| &lt;code&gt;--description&lt;/code&gt; | 包描述 | &lt;code&gt;&quot;My awesome ROS2 package&quot;&lt;/code&gt; |&lt;/p&gt;
&lt;h4&gt;3. 功能包构建 (Package Building)&lt;/h4&gt;
&lt;h5&gt;基本构建命令&lt;/h5&gt;
&lt;p&gt;在&lt;strong&gt;工作空间根目录&lt;/strong&gt; (&lt;code&gt;~/ros2_ws&lt;/code&gt;) 下执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 构建所有包
colcon build

# 构建特定包（最常用！）
colcon build --packages-select &amp;#x3C;package_name&gt;

# 构建时跳过某些包
colcon build --packages-skip &amp;#x3C;package_to_skip&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;高级构建选项&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 构建并使用符号链接（Python开发推荐）
colcon build --packages-select my_py_pkg --symlink-install

# 并行构建加速
colcon build --packages-select my_pkg --parallel-workers $(nproc)

# 构建并显示详细输出
colcon build --packages-select my_pkg --event-handlers console_direct+

# 构建时传递CMake参数（C++包）
colcon build --packages-select my_cpp_pkg --cmake-args -DCMAKE_BUILD_TYPE=Release

# 构建但不运行测试
colcon build --packages-select my_pkg --cmake-args -DBUILD_TESTING=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;清理和重建&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 清理特定包的构建文件
colcon build --packages-select my_pkg --cmake-clean-first

# 彻底删除构建产物
rm -rf build/ install/ log/

# 只删除特定包的产物
rm -rf build/my_pkg/ install/my_pkg/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 功能包测试 (Package Testing)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 运行所有测试
colcon test

# 运行特定包的测试
colcon test --packages-select my_pkg

# 显示测试结果
colcon test-result --all

# 显示详细的测试结果（包括失败信息）
colcon test-result --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 功能包信息查询 (Package Inspection)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出工作空间中所有包
colcon list

# 列出已安装的所有包（包括系统包）
ros2 pkg list

# 查看包的详细信息
ros2 pkg prefix my_pkg  # 查看安装路径
ros2 pkg xml my_pkg     # 查看package.xml内容

# 列出包中的可执行文件
ros2 pkg executables my_pkg

# 查看包的依赖关系
rosdep keys my_pkg      # 查看系统依赖
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. 功能包运行 (Package Execution)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 首先激活工作空间环境
source ~/ros2_ws/install/setup.bash

# 运行包中的节点
ros2 run my_pkg my_node

# 运行launch文件
ros2 launch my_pkg my_launch.launch.py

# 查看节点列表，确认节点已运行
ros2 node list
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. 完整开发工作流示例&lt;/h4&gt;
&lt;h5&gt;示例：创建并开发一个 Python 包&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 进入工作空间src目录
cd ~/ros2_ws/src

# 2. 创建Python包
ros2 pkg create my_py_package --build-type ament_python --dependencies rclpy --node-name my_py_node

# 3. 编辑节点代码
nano my_py_package/my_py_package/my_py_node.py
# （这里编写你的Python代码）

# 4. 编辑package.xml（如果需要添加更多依赖）
nano my_py_package/package.xml

# 5. 编辑setup.py（如果需要修改入口点）
nano my_py_package/setup.py

# 6. 返回工作空间根目录并安装依赖
cd ~/ros2_ws
rosdepc install -i --from-path src --rosdistro humble -y

# 7. 构建包（使用符号链接便于开发）
colcon build --packages-select my_py_package --symlink-install

# 8. 激活环境并运行
source install/setup.bash
ros2 run my_py_package my_py_node
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;示例：创建并开发一个 C++包&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 创建C++包
cd ~/ros2_ws/src
ros2 pkg create my_cpp_package --build-type ament_cmake --dependencies rclcpp --node-name my_cpp_node

# 2. 编辑CMakeLists.txt（如果需要修改编译选项）
nano my_cpp_package/CMakeLists.txt

# 3. 编辑源代码
nano my_cpp_package/src/my_cpp_node.cpp

# 4. 构建并运行
cd ~/ros2_ws
colcon build --packages-select my_cpp_package
source install/setup.bash
ros2 run my_cpp_package my_cpp_node
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8. 常用文件结构说明&lt;/h4&gt;
&lt;h5&gt;C++ 包典型结构&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;my_cpp_package/
├── CMakeLists.txt          # 构建配置
├── include/               # 头文件
│   └── my_cpp_package/
├── package.xml            # 包元数据
├── src/                   # 源代码
│   └── my_cpp_node.cpp
└── test/                  # 测试代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Python 包典型结构&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;my_py_package/
├── package.xml            # 包元数据
├── resource/             # 资源文件
├── setup.cfg             # Python安装配置
├── setup.py              # Python构建配置
├── test/                 # 测试代码
└── my_py_package/        # Python模块
    ├── __init__.py
    └── my_py_node.py     # 节点代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9. 故障排除技巧&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 如果找不到包，检查是否正确source了工作空间
echo $ROS_PACKAGE_PATH

# 如果编译失败，查看详细错误信息
colcon build --packages-select my_pkg --event-handlers console_direct+

# 如果依赖问题，检查package.xml中的依赖声明
# 并确保运行了rosdepc install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个完整的流程涵盖了从包创建到运行的所有关键步骤。记住核心循环：&lt;strong&gt;创建 → 编写 → 构建 → 测试 → 运行&lt;/strong&gt;。使用 &lt;code&gt;--packages-select&lt;/code&gt; 可以大大提高开发效率！&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Node&lt;/h2&gt;
&lt;h4&gt;1. Node 是什么？🤖&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Node = 一个做特定事的程序&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如：一个控制电机的程序、一个读取相机数据的程序、一个处理图像的算法程序&lt;/li&gt;
&lt;li&gt;每个 Node 只管好自己的事，通过 ROS 2 与其他 Node 聊天协作&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 最简单的理解方式 🎯&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 你的机器人系统里可能有这些 Node：
/camera_node     # 专门处理相机
/lidar_node      # 专门处理雷达  
/move_node       # 专门控制移动
/decision_node   # 专门做决策

# 它们这样聊天：
相机Node：&quot;我看到前面有障碍物&quot; → (通过话题) → 决策Node
决策Node：&quot;请向右转&quot; → (通过服务) → 移动Node
移动Node：&quot;正在转弯，已完成50%&quot; → (通过动作) → 决策Node
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 简单代码示例&lt;/h4&gt;
&lt;h5&gt;1. Python 节点代码示例 🐍&lt;/h5&gt;
&lt;h6&gt;简单的发布者节点 (&lt;code&gt;my_publisher.py&lt;/code&gt;)&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import time

class MyPublisher(Node):
    def __init__(self):
        super().__init__(&apos;my_publisher_node&apos;)
        self.publisher_ = self.create_publisher(String, &apos;chatter&apos;, 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.counter = 0
        self.get_logger().info(&apos;Publisher node started!&apos;)

    def timer_callback(self):
        msg = String()
        msg.data = f&apos;Hello ROS2! Count: {self.counter}&apos;
        self.publisher_.publish(msg)
        self.get_logger().info(f&apos;Publishing: &quot;{msg.data}&quot;&apos;)
        self.counter += 1

def main(args=None):
    rclpy.init(args=args)
    node = MyPublisher()
    
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;简单的订阅者节点 (&lt;code&gt;my_subscriber.py&lt;/code&gt;)&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class MySubscriber(Node):
    def __init__(self):
        super().__init__(&apos;my_subscriber_node&apos;)
        self.subscription = self.create_subscription(
            String,
            &apos;chatter&apos;,
            self.listener_callback,
            10)
        self.get_logger().info(&apos;Subscriber node started!&apos;)

    def listener_callback(self, msg):
        self.get_logger().info(f&apos;I heard: &quot;{msg.data}&quot;&apos;)

def main(args=None):
    rclpy.init(args=args)
    node = MySubscriber()
    
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. C++ 节点代码示例 ⚙️&lt;/h5&gt;
&lt;h6&gt;简单的发布者节点 (&lt;code&gt;my_publisher.cpp&lt;/code&gt;)&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;rclcpp/rclcpp.hpp&quot;
#include &quot;std_msgs/msg/string.hpp&quot;
#include &amp;#x3C;chrono&gt;

using namespace std::chrono_literals;

class MyPublisher : public rclcpp::Node {
public:
    MyPublisher() : Node(&quot;my_publisher_node&quot;), count_(0) {
        publisher_ = this-&gt;create_publisher&amp;#x3C;std_msgs::msg::String&gt;(&quot;chatter&quot;, 10);
        timer_ = this-&gt;create_wall_timer(
            1s,
            std::bind(&amp;#x26;MyPublisher::timer_callback, this));
        RCLCPP_INFO(this-&gt;get_logger(), &quot;Publisher node started!&quot;);
    }

private:
    void timer_callback() {
        auto message = std_msgs::msg::String();
        message.data = &quot;Hello ROS2! Count: &quot; + std::to_string(count_++);
        publisher_-&gt;publish(message);
        RCLCPP_INFO(this-&gt;get_logger(), &quot;Publishing: &apos;%s&apos;&quot;, message.data.c_str());
    }
    
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher&amp;#x3C;std_msgs::msg::String&gt;::SharedPtr publisher_;
    size_t count_;
};

int main(int argc, char * argv[]) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared&amp;#x3C;MyPublisher&gt;();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;简单的订阅者节点 (&lt;code&gt;my_subscriber.cpp&lt;/code&gt;)&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;rclcpp/rclcpp.hpp&quot;
#include &quot;std_msgs/msg/string.hpp&quot;

class MySubscriber : public rclcpp::Node {
public:
    MySubscriber() : Node(&quot;my_subscriber_node&quot;) {
        subscription_ = this-&gt;create_subscription&amp;#x3C;std_msgs::msg::String&gt;(
            &quot;chatter&quot;,
            10,
            std::bind(&amp;#x26;MySubscriber::listener_callback, this, std::placeholders::_1));
        RCLCPP_INFO(this-&gt;get_logger(), &quot;Subscriber node started!&quot;);
    }

private:
    void listener_callback(const std_msgs::msg::String::SharedPtr msg) const {
        RCLCPP_INFO(this-&gt;get_logger(), &quot;I heard: &apos;%s&apos;&quot;, msg-&gt;data.c_str());
    }
    
    rclcpp::Subscription&amp;#x3C;std_msgs::msg::String&gt;::SharedPtr subscription_;
};

int main(int argc, char * argv[]) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared&amp;#x3C;MySubscriber&gt;();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 入口点 (Entry Point) 配置说明 🚪&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;入口点 = 告诉系统：&quot;这个包里的这个文件是启动入口&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;对于 Python 包 (&lt;code&gt;ament_python&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;文件：&lt;code&gt;setup.py&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from setuptools import setup
import os
from glob import glob

package_name = &apos;my_py_pkg&apos;

setup(
    name=package_name,
    version=&apos;0.0.0&apos;,
    packages=[package_name],
    data_files=[
        (&apos;share/ament_index/resource_index/packages&apos;,
            [&apos;resource/&apos; + package_name]),
        (&apos;share/&apos; + package_name, [&apos;package.xml&apos;]),
    ],
    install_requires=[&apos;setuptools&apos;],
    zip_safe=True,
    maintainer=&apos;your_name&apos;,
    maintainer_email=&apos;your_email@example.com&apos;,
    description=&apos;TODO: Package description&apos;,
    license=&apos;TODO: License declaration&apos;,
    tests_require=[&apos;pytest&apos;],
    
    # 🔥 最重要的入口点配置 🔥
    entry_points={
        &apos;console_scripts&apos;: [
            # 格式：&apos;节点名 = 包名.文件名:函数名&apos;
            &apos;my_node = my_py_pkg.my_node:main&apos;,
            &apos;camera_driver = my_py_pkg.camera_node:main&apos;,
            &apos;simple_publisher = my_py_pkg.simple_pub:main&apos;,
        ],
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对应文件结构：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_py_pkg/
├── setup.py          # 在这里配置入口点
├── package.xml
└── my_py_pkg/        # Python包目录
    ├── __init__.py
    ├── my_node.py    # 包含 main() 函数
    ├── camera_node.py
    └── simple_pub.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;对于 C++ 包 (&lt;code&gt;ament_cmake&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;文件：&lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;# 添加可执行文件
add_executable(my_cpp_node src/my_cpp_node.cpp)

# 链接ROS2库
ament_target_dependencies(my_cpp_node
  &quot;rclcpp&quot;
  &quot;std_msgs&quot;
)

# 🔥 最重要的入口点配置 🔥
# 安装可执行文件到 lib/&amp;#x3C;包名&gt;
install(TARGETS
  my_cpp_node
  camera_driver
  simple_publisher
  DESTINATION lib/${PROJECT_NAME}
)

# 安装启动文件等其他资源
install(DIRECTORY
  launch
  DESTINATION share/${PROJECT_NAME}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对应文件结构：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_cpp_pkg/
├── CMakeLists.txt    # 在这里配置编译和安装
├── package.xml
├── include/
└── src/
    ├── my_cpp_node.cpp      # 主函数在这里
    ├── camera_driver.cpp
    └── simple_publisher.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 如何创建和运行 🏃&lt;/h4&gt;
&lt;h5&gt;创建包时指定入口点&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建Python包并指定节点名（自动配置入口点）
ros2 pkg create my_pkg --build-type ament_python --node-name my_node

# 创建C++包并指定节点名（自动配置入口点）  
ros2 pkg create my_pkg --build-type ament_cmake --node-name my_node
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;手动添加多个入口点&lt;/h5&gt;
&lt;p&gt;如果已经创建了包，需要添加更多节点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于 Python：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;my_py_pkg/&lt;/code&gt; 目录下创建新的 &lt;code&gt;.py&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;确保文件中有 &lt;code&gt;def main():&lt;/code&gt; 函数&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;setup.py&lt;/code&gt; 的 &lt;code&gt;entry_points&lt;/code&gt; 中添加新行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对于 C++：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;src/&lt;/code&gt; 目录下创建新的 &lt;code&gt;.cpp&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 中添加 &lt;code&gt;add_executable()&lt;/code&gt; 和 &lt;code&gt;ament_target_dependencies()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;install(TARGETS ...)&lt;/code&gt; 中添加新的可执行文件名&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6. 构建和运行 🏗️&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 构建包
colcon build --packages-select my_pkg

# 运行节点（系统会自动找到入口点）
ros2 run my_pkg my_node
ros2 run my_pkg camera_driver
ros2 run my_pkg simple_publisher
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. 重要提醒 ⚠️&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Python 文件需要可执行权限&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod +x my_py_pkg/my_node.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;每次修改 &lt;code&gt;setup.py&lt;/code&gt; 或 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 后需要重新构建&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;入口点名字可以自定义&lt;/strong&gt;，不一定要和文件名一样&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一个包可以有多个入口点&lt;/strong&gt;（多个可执行节点）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;8. 简单总结 🎯&lt;/h4&gt;
&lt;p&gt;| 项目 | Python 包 | C++ 包 |
|------|-----------|--------|
| &lt;strong&gt;配置文件&lt;/strong&gt; | &lt;code&gt;setup.py&lt;/code&gt; | &lt;code&gt;CMakeLists.txt&lt;/code&gt; |
| &lt;strong&gt;配置位置&lt;/strong&gt; | &lt;code&gt;entry_points&lt;/code&gt; | &lt;code&gt;add_executable()&lt;/code&gt; + &lt;code&gt;install()&lt;/code&gt; |
| &lt;strong&gt;文件要求&lt;/strong&gt; | 要有 &lt;code&gt;def main():&lt;/code&gt; 函数 | 要有 &lt;code&gt;int main()&lt;/code&gt; 函数 |
| &lt;strong&gt;构建后&lt;/strong&gt; | 生成在 &lt;code&gt;install/lib/python3.10/site-packages/&lt;/code&gt; | 生成在 &lt;code&gt;install/lib/包名/&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最简单的理解：入口点就是告诉 ROS 2：&quot;当我运行 &lt;code&gt;ros2 run 包名 XXX&lt;/code&gt; 时，到底应该执行哪个文件！&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Topic&lt;/h2&gt;
&lt;p&gt;好的！我们来详细解释 ROS 2 中 Node 间传输数据的重要机制：&lt;strong&gt;Topic（话题）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;ROS 2 Topic（话题）详解&lt;/h3&gt;
&lt;h4&gt;1. Topic 是什么？📡&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Topic = Node 之间的聊天频道&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就像微信群：一个人发言（发布），多个人收听（订阅）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步通信&lt;/strong&gt;：发言者不管有没有人听，都会一直说&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一对多通信&lt;/strong&gt;：一个话题可以有多个发布者和多个订阅者
&lt;img src=&quot;att/ros-03.png&quot; alt=&quot;Pasted image 20250901193057&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 直观比喻 🎯&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 现实世界的例子：
传感器Node：&quot;温度是25°C&quot; → /temperature_topic → 显示Node、控制Node、记录Node

# 就像：
新闻电台 → 广播频率 → 所有收音机听众
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. Topic 通信模型 📊&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;[发布者 Node] → (消息) → [/topic_name] → (消息) → [订阅者 Node]
      ↑                          ↑                          ↑
   Publisher                    Topic                    Subscriber
    (说话)                     (聊天频道)                   (听话)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 核心特性 ✨&lt;/h4&gt;
&lt;p&gt;| 特性       | 说明                   |
| -------- | -------------------- |
| &lt;strong&gt;异步&lt;/strong&gt;   | 发布者不需要等待订阅者响应        |
| &lt;strong&gt;解耦合&lt;/strong&gt;  | 发布者和订阅者互相不知道对方存在 |
| &lt;strong&gt;多对多&lt;/strong&gt;  | 支持多个发布者和多个订阅者        |
| &lt;strong&gt;类型安全&lt;/strong&gt; | 每个 Topic 有固定的消息类型    |
| &lt;strong&gt;实时性&lt;/strong&gt;  | 适合连续数据流传输            |&lt;/p&gt;
&lt;h4&gt;5. 常用命令 🛠️&lt;/h4&gt;
&lt;h5&gt;查看 Topic 信息&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出所有活跃的Topic
ros2 topic list

# 列出Topic并显示消息类型
ros2 topic list -t

# 查看Topic的详细信息
ros2 topic info /chatter

# 查看Topic的消息类型
ros2 topic type /chatter
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;监听和发布数据&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 监听某个Topic的消息（实时显示）
ros2 topic echo /chatter

# 手动向Topic发布消息
ros2 topic pub /chatter std_msgs/msg/String &quot;{data: &apos;Hello ROS2&apos;}&quot;

# 以固定频率发布消息
ros2 topic pub /chatter std_msgs/msg/String &quot;{data: &apos;Hello&apos;}&quot; --rate 1

# 查看Topic的消息频率
ros2 topic hz /chatter

# 查看Topic的数据带宽
ros2 topic bw /chatter
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;安装写好的功能包&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install ros-humble-usb-cam
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：安装摄像头驱动&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;相机驱动底层代码&lt;/strong&gt;：&lt;code&gt;usb_cam&lt;/code&gt; 包已经处理了USB设备的打开、数据采集、格式转换等复杂工作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;图像消息的序列化/反序列化&lt;/strong&gt;：ROS2已经提供了标准的 &lt;code&gt;sensor_msgs/Image&lt;/code&gt; 消息类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;通信底层代码&lt;/strong&gt;：Topic的发布订阅机制ROS2已经实现好了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;场景1：直接使用（最简单）&lt;/h5&gt;
&lt;p&gt;如果你只是想要显示摄像头画面，确实不用写代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ros2 run usb_cam usb_cam_node_exe
ros2 run learning_topic topic_webcam_sub
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;场景2：自定义处理（最常见）&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2

class MyImageProcessor(Node):
    def __init__(self):
        super().__init__(&apos;my_image_processor&apos;)
        # 订阅原始图像
        self.subscription = self.create_subscription(
            Image,
            &apos;/image_raw&apos;,  # 订阅usb_cam发布的话题
            self.image_callback,
            10)
        self.bridge = CvBridge()
        
    def image_callback(self, msg):
        # 将ROS图像消息转换为OpenCV格式
        cv_image = self.bridge.imgmsg_to_cv2(msg, &quot;bgr8&quot;)
        
        # 在这里添加你的处理代码
        # 例如：图像识别、边缘检测、目标跟踪等
        gray_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray_image, 100, 200)
        
        # 显示处理结果
        cv2.imshow(&apos;Processed Image&apos;, edges)
        cv2.waitKey(1)

def main(args=None):
    rclpy.init(args=args)
    node = MyImageProcessor()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;问题&lt;/h3&gt;
&lt;p&gt;异步传输数据&lt;/p&gt;
&lt;p&gt;发布者一直在对外发布信息，并不知道接收者究竟有没有接收到信息&lt;/p&gt;
&lt;p&gt;因此可以用service 来解决&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Service&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-04.jpg&quot; alt=&quot;PixPin_2025-09-01_19-55-34&quot;&gt;
&lt;img src=&quot;att/ros-05.jpg&quot; alt=&quot;PixPin_2025-09-01_19-56-13&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Action&lt;/h2&gt;
&lt;h4&gt;1. Action 是什么？🎯&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Action = 处理长时间、可取消、有进度反馈的任务的机制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像点餐后的完整流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下单（目标） → 厨师开始做菜（执行） → 不时汇报进度（反馈） → 上菜（结果）&lt;/li&gt;
&lt;li&gt;或者：中途可以取消订单（取消）
&lt;img src=&quot;att/ros-06.png&quot; alt=&quot;Pasted image 20250901210709&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实是通过 service 和 topic 来实现的
实际上就是一种应用层的通信机制&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-07.jpg&quot; alt=&quot;PixPin_2025-09-01_21-07-33&quot;&gt;&lt;/p&gt;
&lt;h4&gt;2. 直观比喻 🍳&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 客户端：&quot;请做一份披萨，30分钟内完成&quot;
# 服务端：&quot;开始做披萨...20%完成...50%完成...披萨做好了！&quot;

# 或者：
# 客户端：&quot;取消做披萨！&quot;
# 服务端：&quot;好的，已取消&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. Action 通信模型 📊&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;[客户端 Node] → (目标) → [/action_name] → (目标) → [服务端 Node]
     ↓               ↓               ↓               ↓
  Client          Action           Action         Server
 (顾客)           (订单系统)        (订单系统)       (厨师)
     ↓               ↓               ↓               ↓
[等待结果] ← (结果) ← [反馈] ← (进度) ← [执行任务]
     ↑               ↑               ↑               ↑
  取消指令 → → → → → → → → → → → → → → → 取消处理
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. Action 的核心组成 🧩&lt;/h4&gt;
&lt;p&gt;一个 Action 包含三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Goal&lt;/strong&gt;（目标）：要执行的任务（比如&quot;导航到 x, y 位置&quot;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feedback&lt;/strong&gt;（反馈）：执行过程中的进度更新（比如&quot;已完成 50%&quot;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result&lt;/strong&gt;（结果）：最终的执行结果（比如&quot;成功到达&quot;或&quot;失败原因&quot;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5. 与 Service、Topic 的对比 📋&lt;/h4&gt;
&lt;p&gt;| 方面 | Action | Service | Topic |
|------|---------|---------|-------|
| &lt;strong&gt;通信模式&lt;/strong&gt; | 异步，有反馈 | 同步，请求-响应 | 异步，发布-订阅 |
| &lt;strong&gt;持续时间&lt;/strong&gt; | 长时间运行 | 瞬时完成 | 持续数据流 |
| &lt;strong&gt;交互性&lt;/strong&gt; | 可取消，有进度反馈 | 不可取消，无反馈 | 无交互 |
| &lt;strong&gt;适用场景&lt;/strong&gt; | 导航、机械臂控制 | 计算、查询 | 传感器数据 |&lt;/p&gt;
&lt;h4&gt;6. 常用命令 🛠️&lt;/h4&gt;
&lt;h5&gt;查看 Action 信息&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出所有活跃的Action
ros2 action list

# 列出Action并显示类型
ros2 action list -t

# 查看Action信息
ros2 action info /turtle1/rotate_absolute

# 查看Action类型
ros2 action type /turtle1/rotate_absolute
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;发送 Action 目标&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 发送Action目标
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute &quot;{theta: 1.57}&quot;

# 发送目标并显示反馈
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute &quot;{theta: 3.14}&quot; --feedback

# 取消Action目标（需要先知道goal_id）
ros2 action cancel_goal &amp;#x3C;goal_id&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. Action 消息结构 📝&lt;/h4&gt;
&lt;h5&gt;Action 定义文件 (&lt;code&gt;RotateAbsolute.action&lt;/code&gt;)&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;# Goal - 目标
float32 theta
---
# Result - 结果
float32 delta
---
# Feedback - 反馈
float32 remaining
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;8. Params&lt;/h2&gt;
&lt;h5&gt;1. 核心概念：什么是参数（Params）？&lt;/h5&gt;
&lt;p&gt;在 ros2 中，&lt;strong&gt;参数是节点在运行时可配置的配置值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你可以把它们想象成一个节点的“设置”或“选项”。与通过主题（Topic）和服务（Service）进行的动态、频繁的通信不同，参数通常用于设置那些在节点启动后不经常改变，但又需要灵活配置的变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个简单的类比：&lt;/strong&gt;
将节点比作一个电视，主题和服务就像是遥控器，用于实时切换频道和调整音量。而参数则像是电视&lt;strong&gt;后背的物理旋钮或菜单设置&lt;/strong&gt;，用于配置屏幕亮度、色彩、语言等，你通常不会在观看时频繁调整它们，但在初次 setup 或偶尔优化时非常重要。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;2. 参数的核心特性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动态配置性&lt;/strong&gt;：参数可以在节点运行时（online）通过命令行、API 或图形化工具动态地读取和修改，而无需重启节点。这极大地提高了系统的灵活性和可调试性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;由节点管理&lt;/strong&gt;：每个节点都拥有自己独立的参数空间。一个节点的参数与另一个节点的参数是隔离的，即使参数名相同也不会冲突。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具有类型&lt;/strong&gt;：每个参数都有一个明确的数据类型，例如 &lt;code&gt;integer&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;byte arrays&lt;/code&gt; 以及这些类型的列表（arrays）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可被监控&lt;/strong&gt;：节点可以设置回调函数，当某个参数的值被外部修改时，该回调函数会被触发，节点可以据此执行相应的操作（例如，应用新的配置）。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h5&gt;3. 参数的使用场景&lt;/h5&gt;
&lt;p&gt;参数非常适合用于以下情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置硬件设置&lt;/strong&gt;：例如，配置相机节点的分辨率、帧率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法参数调优&lt;/strong&gt;：例如，配置机器人导航算法中的最大速度、安全距离、PID 控制器增益等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置节点行为模式&lt;/strong&gt;：例如，通过一个 &lt;code&gt;string&lt;/code&gt; 类型的参数选择不同的策略模式（如 &quot;aggressive&quot; 或 &quot;cautious&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化设置&lt;/strong&gt;：例如，设置机器人的名称、模拟模式还是真实模式等。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h5&gt;4. 参数的工作机制：服务与动作&lt;/h5&gt;
&lt;p&gt;参数系统并非基于主题（Pub/Sub），而是基于一套&lt;strong&gt;服务（Services）和动作（Actions）&lt;/strong&gt; 的架构。&lt;/p&gt;
&lt;p&gt;每个支持参数的节点内部都会自动创建以下几个服务：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;~/describe_parameters&lt;/code&gt;： 获取参数描述。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/get_parameter_types&lt;/code&gt;： 获取参数类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/get_parameters&lt;/code&gt;： &lt;strong&gt;读取&lt;/strong&gt;一个或多个参数的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/set_parameters&lt;/code&gt;： &lt;strong&gt;设置&lt;/strong&gt;一个或多个参数的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/set_parameters_atomically&lt;/code&gt;： 原子性地设置多个参数的值（要么全部成功，要么全部失败，防止出现中间状态）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/list_parameters&lt;/code&gt;： 列出节点中的所有参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外，还有一个&lt;strong&gt;动作（Action）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/parameter_events&lt;/code&gt;： 这是一个发布者，当参数发生变化时，节点会通过这个动作发布一个“参数事件”消息，通知系统其他部分参数发生了更改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你使用 &lt;code&gt;ros2 param set&lt;/code&gt; 这样的命令行工具时，该工具实际上是在幕后调用目标节点的 &lt;code&gt;~/set_parameters&lt;/code&gt; 服务。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;5. 如何使用参数（代码层面）&lt;/h5&gt;
&lt;p&gt;以下是一个在 Python 节点中声明和使用参数的简单示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter

class MyNode(Node):
    def __init__(self):
        super().__init__(&apos;my_node&apos;)
        
        # 1. 声明参数并设置默认值
        self.declare_parameter(&apos;my_string_param&apos;, &apos;default_value&apos;)
        self.declare_parameter(&apos;my_int_param&apos;, 10)
        self.declare_parameter(&apos;my_bool_param&apos;, True)
        
        # 2. 获取参数值
        my_string = self.get_parameter(&apos;my_string_param&apos;).value
        my_int = self.get_parameter(&apos;my_int_param&apos;).value
        my_bool = self.get_parameter(&apos;my_bool_param&apos;).value
        
        self.get_logger().info(f&apos;String param: {my_string}&apos;)
        self.get_logger().info(f&apos;Int param: {my_int}&apos;)
        self.get_logger().info(f&apos;Bool param: {my_bool}&apos;)
        
        # 3. （可选）设置参数变更回调函数
        # 当任何参数被外部修改时，此函数都会被调用
        self.add_on_set_parameters_callback(self.parameter_change_callback)
    
    def parameter_change_callback(self, params):
        &quot;&quot;&quot;参数变更回调函数&quot;&quot;&quot;
        for param in params:
            self.get_logger().info(f&apos;Parameter {param.name} changed to {param.value}&apos;)
            
            # 你可以在这里检查特定参数并做出反应
            if param.name  &apos;my_int_param&apos; and param.type_  Parameter.Type.INTEGER:
                self.get_logger().info(f&apos;New int value is: {param.value}&apos;)
                # 这里可以触发重新配置算法等操作
        
        # 返回一个成功的状态对象，表示参数接受更改
        # 你也可以在这里验证参数值，如果值非法，可以返回一个不成功的状态来拒绝此次更改。
        return SetParametersResult(successful=True)

def main(args=None):
    rclpy.init(args=args)
    node = MyNode()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 C++ 中也有类似的 API。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;6. 如何使用参数（命令行工具）&lt;/h5&gt;
&lt;p&gt;ros2 提供了一系列强大的命令行工具来管理参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 param list &amp;#x3C;node_name&gt;&lt;/code&gt;： 列出某个节点的所有参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 param list /my_node
# 输出： /my_node:
#   my_bool_param
#   my_int_param
#   my_string_param
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 param get &amp;#x3C;node_name&gt; &amp;#x3C;param_name&gt;&lt;/code&gt;： 获取某个参数的当前值和类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 param get /my_node my_int_param
# 输出： Integer value is: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 param set &amp;#x3C;node_name&gt; &amp;#x3C;param_name&gt; &amp;#x3C;value&gt;&lt;/code&gt;： 设置某个参数的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 param set /my_node my_int_param 42
# 输出： Set parameter successful
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 param dump &amp;#x3C;node_name&gt;&lt;/code&gt;： 将节点的所有当前参数值导出到一个 YAML 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 param dump /my_node &gt; my_node_params.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 YAML 文件内容类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;/my_node:
  ros__parameters:
    my_bool_param: true
    my_int_param: 42
    my_string_param: &quot;default_value&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 param load &amp;#x3C;node_name&gt; &amp;#x3C;parameter_file&gt;&lt;/code&gt;： 从一个 YAML 文件中加载参数值并应用到节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 param load /my_node my_node_params. Yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动时加载参数&lt;/strong&gt;： 更常见的做法是在启动节点时直接通过启动文件（Launch File）加载参数文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 在 Python launch 文件中
From launch_ros. Actions import Node

Node = Node (
    Package=&apos;my_package&apos;,
    Executable=&apos;my_node_executable&apos;,
    Name=&apos;my_node&apos;,
    Parameters=[&apos;path/to/my_node_params.Yaml&apos;] # 关键在这里
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;9. 分布式通信&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-08.jpg&quot; alt=&quot;PixPin_2025-09-02_22-47-17&quot;&gt;&lt;/p&gt;
&lt;p&gt;简单的说，如果不设置群组 id，那么所有的节点都会收到消息&lt;/p&gt;
&lt;p&gt;如果是设定了群组 id，那么只有相同群组 id 的节点才会收到消息&lt;/p&gt;
&lt;p&gt;设置方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .bashrc
export ROS_DOMAIN_ID=0 # yourid 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;10. DDS&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-09.jpg&quot; alt=&quot;PixPin_2025-09-02_23-48-00 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-10.jpg&quot; alt=&quot;PixPin_2025-09-02_23-47-19&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-11.jpg&quot; alt=&quot;PixPin_2025-09-02_23-49-31&quot;&gt;&lt;/p&gt;
&lt;p&gt;好的，我们来详细阐述 ROS 2 中的 DDS（Data Distribution Service）。这是一个核心且基础的概念，理解了 DDS，就理解了 ROS 2 通信机制的基石。&lt;/p&gt;
&lt;h5&gt;1. 核心概念：什么是 DDS？&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;DDS（数据分发服务）&lt;/strong&gt; 是一个由&lt;strong&gt;对象管理组织（OMG）&lt;/strong&gt; 发布的国际标准（v 1.4），它是一种&lt;strong&gt;以数据为中心的&lt;/strong&gt;、&lt;strong&gt;跨语言的&lt;/strong&gt;、&lt;strong&gt;跨平台的&lt;/strong&gt;通信中间件协议和 API 标准。&lt;/p&gt;
&lt;p&gt;其核心设计理念是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以数据为中心（Data-Centric）&lt;/strong&gt;：传统的通信方式（如 TCP/IP）是以“节点/设备”为中心的（我知道要跟谁说话）。而 DDS 是以“数据”为中心的。应用程序只关心它需要“发布”或“订阅”什么“主题”的数据，而不需要关心数据来自哪里或发送到哪里。DDS 中间件负责自动发现和高效路由数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布-订阅模式（Publish-Subscribe）&lt;/strong&gt;：DDS 严格遵循发布-订阅模型，这与 ROS 的 Topic 通信模型天然契合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时系统（Real-Time Systems）&lt;/strong&gt;：DDS 的设计目标之一就是满足硬实时和软实时系统的需求，提供可预测的性能和低延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠性（Reliability）&lt;/strong&gt;：DDS 提供了丰富的服务质量（QoS）策略，可以灵活配置通信的可靠性、持久性、寿命、截止时间等，以适应从关键任务控制到非关键监控的各种场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h5&gt;2. 为什么 ROS 2 要采用 DDS？&lt;/h5&gt;
&lt;p&gt;ROS 1 的通信层是自定义的，基于 TCPROS/UDPROS 协议。虽然工作良好，但存在一些固有缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;发现机制&lt;/strong&gt;：采用集中式的 Master 节点进行发现，单点故障问题严重。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台性&lt;/strong&gt;：主要支持 Linux，在其他操作系统上支持不佳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时性&lt;/strong&gt;：难以满足严格的实时控制需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：原生缺乏安全通信机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂性&lt;/strong&gt;：与外部非 ROS 系统集成相对复杂。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;DDS 完美地解决了这些问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;去中心化发现&lt;/strong&gt;：DDS 使用基于&lt;strong&gt;广播或组播&lt;/strong&gt;的分布式发现协议，无需中心节点，更加健壮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生跨平台&lt;/strong&gt;：DDS 是标准，有各种语言和操作系统的商业和开源实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强大的实时性&lt;/strong&gt;：许多 DDS 实现专为实时和嵌入式系统设计。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置安全模型&lt;/strong&gt;：DDS 标准包含一个安全规范（DDS-Security），提供认证、加密和访问控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统集成&lt;/strong&gt;：DDS 是工业界广泛使用的标准（尤其在航空、国防、医疗领域），ROS 2 节点可以直接与其它 DDS 应用程序通信，无缝集成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，ROS 2 选择&lt;strong&gt;不重新发明轮子&lt;/strong&gt;，而是将 DDS 作为其默认的&lt;strong&gt;中间件（Middleware）&lt;/strong&gt;，在其之上构建 ROS 的抽象层（如 Nodes, Topics, Services, Actions）。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;3. ROS 2 与 DDS 的架构关系：RCL 和 RMW&lt;/h5&gt;
&lt;p&gt;ROS 2 的软件栈被设计为可插入中间件的架构，其与 DDS 的关系可以通过以下分层模型来理解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---------------------+ (Your Code)
|    ROS 2 Client     |    (e.g., rclcpp, rclpy)
|     Library (RCL)   |
+---------------------+
|  ROS 2 Middleware   |  &amp;#x3C;--- 这是抽象接口层
|   Interface (RMW)   |
+---------------------+
|   DDS Implementation |  &amp;#x3C;--- 这是具体的实现层 (e.g., Cyclone DDS, Fast DDS)
+---------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ROS 客户端库（RCL - ROS Client Library）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是你编写 ROS 2 代码时直接调用的库，例如 C++ 的 &lt;code&gt;rclcpp&lt;/code&gt; 或 Python 的 &lt;code&gt;rclpy&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;它提供了熟悉的 &lt;code&gt;Node&lt;/code&gt;, &lt;code&gt;Publisher&lt;/code&gt;, &lt;code&gt;Subscriber&lt;/code&gt;, &lt;code&gt;Service&lt;/code&gt;, &lt;code&gt;Client&lt;/code&gt; 等接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ROS 中间件接口（RMW - ROS Middleware Interface）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是一个&lt;strong&gt;抽象层&lt;/strong&gt;，它定义了一套通用的 API（用 C 语言实现），用于所有底层中间件实现的通信功能（如发布消息、订阅主题、请求服务等）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RCL&lt;/code&gt; 调用 &lt;code&gt;RMW&lt;/code&gt; 接口，&lt;code&gt;RMW&lt;/code&gt; 再将调用转换给具体的 DDS 实现。这实现了&lt;strong&gt;解耦&lt;/strong&gt;：ROS 2 的上层代码不依赖于任何特定的 DDS 供应商。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DDS 实现层（DDS Implementation）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是具体的 DDS 库，它实现了 DDS 标准的所有功能。&lt;/li&gt;
&lt;li&gt;ROS 2 通过一个对应的 &lt;strong&gt;RMW 适配器包&lt;/strong&gt;（例如 &lt;code&gt;rmw_cyclonedds_cpp&lt;/code&gt;) 来使用这个库。&lt;/li&gt;
&lt;li&gt;你可以通过设置环境变量 &lt;code&gt;RMW_IMPLEMENTATION=rmw_cyclonedds_cpp&lt;/code&gt; 来轻松切换使用的 DDS 实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结一下数据流&lt;/strong&gt;：你的 C++/Python 代码 -&gt; &lt;code&gt;rclcpp&lt;/code&gt; / &lt;code&gt;rclpy&lt;/code&gt; -&gt; &lt;code&gt;rcl&lt;/code&gt; (C API) -&gt; &lt;code&gt;rmw&lt;/code&gt; (Middleware Interface) -&gt; 具体的 DDS 库（如 Cyclone DDS） -&gt; 网络传输。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;5. DDS 带给 ROS 2 的核心特性：QoS（服务质量）&lt;/h5&gt;
&lt;p&gt;这是 DDS 带给 ROS 2 最强大、最独特的特性。QoS 策略允许你精细地控制通信行为。&lt;/p&gt;
&lt;p&gt;| QoS 策略 | 功能描述 | 应用场景示例 |
| :--- | :--- | :--- |
| &lt;strong&gt;可靠性 (Reliability)&lt;/strong&gt; | &lt;code&gt;RELIABLE&lt;/code&gt;：确保所有消息都能送达（可能重传）。&lt;code&gt;BEST_EFFORT&lt;/code&gt;：尽力送达，不保证成功（可能丢包）。 | &lt;strong&gt;控制指令&lt;/strong&gt;必须使用 &lt;code&gt;RELIABLE&lt;/code&gt;。&lt;strong&gt;传感器数据流&lt;/strong&gt;（如视频）可使用 &lt;code&gt;BEST_EFFORT&lt;/code&gt; 以降低延迟。 |
| &lt;strong&gt;持久性 (Durability)&lt;/strong&gt; | &lt;code&gt;VOLATILE&lt;/code&gt;：不为新订阅者保留历史消息。&lt;code&gt;TRANSIENT_LOCAL&lt;/code&gt;：为新订阅者保留最后 N 个消息。 | &lt;strong&gt;地图数据&lt;/strong&gt;：新加入的导航节点需要获取当前地图（&lt;code&gt;TRANSIENT_LOCAL&lt;/code&gt;）。&lt;strong&gt;实时状态&lt;/strong&gt;：只需最新数据，如当前速度（&lt;code&gt;VOLATILE&lt;/code&gt;）。 |
| &lt;strong&gt;存活时间 (Liveliness)&lt;/strong&gt; | 定义发布者是否“存活”的策略。可以是自动声明，或由某个特定信号触发。 | 用于&lt;strong&gt;故障检测&lt;/strong&gt;。如果一个发布者不再“存活”，系统可以知道并触发安全措施。 |
| &lt;strong&gt;生命周期 (Lifespan)&lt;/strong&gt; | 消息的有效期。超过此时间的消息即使被收到也会被丢弃。 | 用于&lt;strong&gt;过时数据&lt;/strong&gt;，如一个很快会失效的临时目标点。 |
| &lt;strong&gt;截止时间 (Deadline)&lt;/strong&gt; | 规定消息发布的&lt;strong&gt;最大期望周期&lt;/strong&gt;。如果订阅者发现发布者超过此时间未发消息，会触发回调。 | 监控&lt;strong&gt;数据流频率&lt;/strong&gt;。如果激光雷达数据低于 10 Hz，则报警。 |
| &lt;strong&gt;传输优先级 (Transport Priority)&lt;/strong&gt; | 为消息指定网络优先级，让高优先级的消息优先被发送。 | 紧急停止信号 (&lt;code&gt;ESTOP&lt;/code&gt;) 应该比日志信息拥有更高的网络优先级。 |&lt;/p&gt;
&lt;p&gt;在 ROS 2 中，你可以在创建 &lt;code&gt;Publisher&lt;/code&gt; 和 &lt;code&gt;Subscriber&lt;/code&gt; 时指定它们的 QoS 策略。&lt;strong&gt;只有当发布者和订阅者的 QoS 配置兼容时，它们才能建立连接并进行通信&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;如何指定 QoS&lt;/h4&gt;
&lt;p&gt;发布 topic 等的时候加入命令行选项即可&lt;/p&gt;
&lt;p&gt;如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ros2 topic echo /chatter --qos-reliability best_effort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，发送者和接受者需要用相同的 qos&lt;/p&gt;
&lt;p&gt;或者在代码中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class PublisherNode(Node):
    
    def __init__(self, name):
        super().__init__(name)  # ROS2节点父类初始化
        
        qos_profile = QoSProfile(  # 创建一个QoS原则
            # reliability=QoSReliabilityPolicy.BEST_EFFORT,
            reliability=QoSReliabilityPolicy.RELIABLE,
            history=QoSHistoryPolicy.KEEP_LAST,
            depth=1
        )
        # 创建发布者对象（消息类型、话题名、QoS原则）
        self.pub = self.create_publisher(String, &quot;chatter&quot;, qos_profile)  
         
        # 创建一个定时器（单位为秒的周期，定时执行的回调函数）
        self.timer = self.create_timer(0.5, self.timer_callback)           
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;11. launch&lt;/h2&gt;
&lt;h5&gt;核心概念：什么是 Launch？&lt;/h5&gt;
&lt;p&gt;在 ROS 2 中，&lt;strong&gt;Launch&lt;/strong&gt; 系统是一个用于启动和管理多个 ROS 2 节点（Nodes）以及相关进程的框架。一个复杂的机器人系统通常由数十个甚至上百个相互协作的节点组成（例如，传感器驱动、感知、定位、规划、控制等）。手动在每个终端里用 &lt;code&gt;ros2 run&lt;/code&gt; 命令来启动每一个节点是非常繁琐且容易出错的。&lt;/p&gt;
&lt;p&gt;Launch 系统允许你通过一个** launch 文件**，来声明式地配置和启动一整套节点系统，并可以设置参数、重映射（Remapping）、包含其他 launch 文件、响应事件等。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;为什么需要 Launch 文件？&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动化复杂系统&lt;/strong&gt;：一键启动所有必要的组件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置管理&lt;/strong&gt;：集中管理节点的参数、命名空间、重映射规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可维护性和可复用性&lt;/strong&gt;：将启动逻辑写成文件，便于版本控制和在不同机器人或场景下复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健壮性&lt;/strong&gt;：可以配置规则，在某个节点失败时自动重启或其他节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h5&gt;ROS 2 Launch 的演进：Python 作为一等公民&lt;/h5&gt;
&lt;p&gt;与 ROS 1 主要使用 XML 格式的 &lt;code&gt;.launch&lt;/code&gt; 文件不同，&lt;strong&gt;ROS 2 的 Launch 系统是用 Python 编写的&lt;/strong&gt;，并且鼓励用户使用 Python 来编写 launch 文件（文件扩展名通常是 &lt;code&gt;.py&lt;/code&gt;）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强大灵活&lt;/strong&gt;：你可以利用 Python 的全部功能（条件判断、循环、函数、导入模块等）来动态生成复杂的启动逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可编程性&lt;/strong&gt;：可以轻松地编写逻辑来响应启动过程中发生的事件（例如，一个节点退出时执行某些操作）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可读性&lt;/strong&gt;：对于熟悉 Python 的开发者来说，Python 代码比 XML 更易读和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，ROS 2 也支持通过 XML 和 YAML 来编写 launch 文件，以实现与 ROS 1 的兼容性或满足简单需求，但 Python 方式是官方推荐且功能最强大的方式。&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;Launch 文件的核心组成部分&lt;/h5&gt;
&lt;p&gt;一个典型的 Python launch 文件通常包含以下元素：&lt;/p&gt;
&lt;h6&gt;1. 导入（Imports）&lt;/h6&gt;
&lt;p&gt;从 &lt;code&gt;launch&lt;/code&gt; 和 &lt;code&gt;launch_ros&lt;/code&gt; 模块中导入所需的类和方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2. 生成 Launch 描述的函数&lt;/h6&gt;
&lt;p&gt;定义一个函数（通常是 &lt;code&gt;generate_launch_description()&lt;/code&gt;）来创建并返回一个 &lt;code&gt;LaunchDescription&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def generate_launch_description():
    # ... 在这里配置启动内容 ...
    return LaunchDescription([
        # ... 将配置好的动作（Actions）放在这里 ...
    ])
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;3. 动作（Actions）&lt;/h6&gt;
&lt;p&gt;Actions 是 Launch 系统执行的具体操作，最常见的包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Node&lt;/code&gt;&lt;/strong&gt;：启动一个 ROS 2 节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;talker_node = Node(
    package=&apos;demo_nodes_cpp&apos;,        # 功能包名
    executable=&apos;talker&apos;,             # 可执行文件的名字
    name=&apos;my_talker&apos;,                # 覆盖节点名（可选）
    namespace=&apos;my_ns&apos;,               # 设置命名空间（可选）
    parameters=[{&apos;rate&apos;: 1.0}],      # 设置参数（可选）
    remappings=[(&apos;chatter&apos;, &apos;my_chatter&apos;)] # 重映射话题（可选）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DeclareLaunchArgument&lt;/code&gt;&lt;/strong&gt;：声明一个启动参数，允许用户从命令行传入值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;use_sim_time_arg = DeclareLaunchArgument(
    &apos;use_sim_time&apos;,
    default_value=&apos;false&apos;,
    description=&apos;Use simulation (Gazebo) clock if true&apos;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;LaunchConfiguration&lt;/code&gt;&lt;/strong&gt;：引用一个已声明的启动参数的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 在Node的parameters中使用启动参数
parameters=[{&apos;use_sim_time&apos;: LaunchConfiguration(&apos;use_sim_time&apos;)}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;IncludeLaunchDescription&lt;/code&gt;&lt;/strong&gt;：包含并执行另一个 launch 文件，实现模块化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;include_another_launch = IncludeLaunchDescription(
    PathJoinSubstitution([get_package_share_directory(&apos;another_pkg&apos;), &apos;launch&apos;, &apos;other.launch.py&apos;])
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;4.  Substitutions（替换）&lt;/h6&gt;
&lt;p&gt;允许动态地构建字符串，常用于构建路径或使用启动参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from launch.substitutions import PathJoinSubstitution, FindPackageShare

# 动态查找功能包路径并拼接文件路径
config_file = PathJoinSubstitution([
    FindPackageShare(&apos;my_robot_bringup&apos;),
    &apos;config&apos;,
    &apos;robot_params.yaml&apos;
])
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h5&gt;一个完整的示例&lt;/h5&gt;
&lt;p&gt;假设我们想启动一个话题发布者（&lt;code&gt;talker&lt;/code&gt;）和一个话题订阅者（&lt;code&gt;listener&lt;/code&gt;），并为 &lt;code&gt;talker&lt;/code&gt; 设置一个参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;my_launch_file.py&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

def generate_launch_description():

    # 1. 声明一个启动参数
    rate_arg = DeclareLaunchArgument(
        &apos;publish_rate&apos;,
        default_value=&apos;1.0&apos;,
        description=&apos;Publishing rate (Hz) for the talker node&apos;
    )
    
    # 当然也可以通过yaml加载
    &apos;&apos;&apos;
    config = os.path.join(              # 找到参数文件的完整路径
      get_package_share_directory(&apos;learning_launch&apos;),
      &apos;config&apos;,
      &apos;turtlesim.yaml&apos;
      )
    &apos;&apos;&apos;

    # 2. 配置要启动的节点
    talker_node = Node(
        package=&apos;demo_nodes_cpp&apos;,
        executable=&apos;talker&apos;,
        name=&apos;my_talker&apos;,
        parameters=[{
            &apos;rate&apos;: LaunchConfiguration(&apos;publish_rate&apos;) # 使用启动参数
        }],
        remappings=[(&apos;chatter&apos;, &apos;my_chatter_topic&apos;)] # 将/chatter重映射为/my_chatter_topic
    )

    listener_node = Node(
        package=&apos;demo_nodes_cpp&apos;,
        executable=&apos;listener&apos;,
        name=&apos;my_listener&apos;,
        remappings=[(&apos;chatter&apos;, &apos;my_chatter_topic&apos;)] # 也必须重映射，才能听到talker
    )

    # 3. 将所有的Actions加入到LaunchDescription中
    return LaunchDescription([
        rate_arg,
        talker_node,
        listener_node
    ])
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h5&gt;Launch 调用 launch&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def generate_launch_description():                # 自动生成launch文件的函数
   parameter_yaml = IncludeLaunchDescription(     # 包含指定路径下的另外一个launch文件
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory(&apos;learning_launch&apos;), &apos;launch&apos;),
         &apos;/parameters_nonamespace.launch.py&apos;])
      )
  
   parameter_yaml_with_namespace = GroupAction(   
   # 对指定launch文件中启动的功能加上命名空间
      actions=[
         PushRosNamespace(&apos;turtlesim2&apos;), # 防止冲突，主动加ns
         parameter_yaml]
      )

   return LaunchDescription([            # 返回launch文件的描述信息
      parameter_yaml_with_namespace
   ])

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;准备 setup&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;(os.path.join(&apos;share&apos;, package_name, &apos;launch&apos;), glob(os.path.join(&apos;launch&apos;, &apos;*.launch.py&apos;))),

(os.path.join(&apos;share&apos;, package_name, &apos;config&apos;), glob(os.path.join(&apos;config&apos;, &apos;*.*&apos;))),

(os.path.join(&apos;share&apos;, package_name, &apos;rviz&apos;), glob(os.path.join(&apos;rviz&apos;, &apos;*.*&apos;))),
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;如何运行 Launch 文件&lt;/h5&gt;
&lt;p&gt;使用 &lt;code&gt;ros2 launch&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 基本用法
ros2 launch &amp;#x3C;package_name&gt; &amp;#x3C;launch_file_name&gt;.py

# 运行上面的示例（假设文件在 my_package 功能包中）
ros2 launch my_package my_launch_file.py

# 在启动时覆盖参数的值
ros2 launch my_package my_launch_file.py publish_rate:=2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我来详细解释一下这个 &lt;code&gt;remapping&lt;/code&gt; 的作用，这是 ROS 中一个非常重要且强大的概念。&lt;/p&gt;
&lt;h4&gt;Remapping（重映射）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;重映射&lt;/strong&gt;就是&lt;strong&gt;在运行时改变节点的通信资源名称&lt;/strong&gt;（话题、服务、参数等）。简单说，就是给通信通道起个别名或者重新定向。&lt;/p&gt;
&lt;h5&gt;为什么需要重映射？&lt;/h5&gt;
&lt;p&gt;在这个具体的例子中，有三个节点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;turtlesim1/sim&lt;/code&gt; - 第一个乌龟模拟器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turtlesim2/sim&lt;/code&gt; - 第二个乌龟模拟器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mimic&lt;/code&gt; - 模仿节点&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;问题分析：&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mimic&lt;/code&gt; 节点的设计&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它默认&lt;strong&gt;订阅&lt;/strong&gt; &lt;code&gt;/input/pose&lt;/code&gt; 话题来获取姿势信息&lt;/li&gt;
&lt;li&gt;它默认&lt;strong&gt;发布&lt;/strong&gt; &lt;code&gt;/output/cmd_vel&lt;/code&gt; 话题来发送速度指令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实际需求&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们希望 &lt;code&gt;mimic&lt;/code&gt; 读取&lt;strong&gt;第一个乌龟&lt;/strong&gt;的姿势（&lt;code&gt;/turtlesim1/turtle1/pose&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;然后控制&lt;strong&gt;第二个乌龟&lt;/strong&gt;运动（向 &lt;code&gt;/turtlesim2/turtle1/cmd_vel&lt;/code&gt; 发布指令）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;名称不匹配&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mimic&lt;/code&gt; 想订阅的是 &lt;code&gt;/input/pose&lt;/code&gt;，但数据在 &lt;code&gt;/turtlesim1/turtle1/pose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mimic&lt;/code&gt; 想发布到 &lt;code&gt;/output/cmd_vel&lt;/code&gt;，但需要发布到 &lt;code&gt;/turtlesim2/turtle1/cmd_vel&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;重映射如何解决这个问题？&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;remappings=[
    (&apos;/input/pose&apos;, &apos;/turtlesim1/turtle1/pose&apos;),        # 输入重定向
    (&apos;/output/cmd_vel&apos;, &apos;/turtlesim2/turtle1/cmd_vel&apos;), # 输出重定向
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;效果相当于：&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;对于 &lt;code&gt;mimic&lt;/code&gt; 节点内部&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当它尝试订阅 &lt;code&gt;/input/pose&lt;/code&gt; 时，系统自动让它订阅 &lt;code&gt;/turtlesim1/turtle1/pose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当它尝试发布到 &lt;code&gt;/output/cmd_vel&lt;/code&gt; 时，系统自动让它发布到 &lt;code&gt;/turtlesim2/turtle1/cmd_vel&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对于其他节点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它们看到的是 &lt;code&gt;mimic&lt;/code&gt; 在正常地与 &lt;code&gt;/turtlesim1/turtle1/pose&lt;/code&gt; 和 &lt;code&gt;/turtlesim2/turtle1/cmd_vel&lt;/code&gt; 通信&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;可视化理解&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;mimic节点内部             重映射后实际连接
-------------           -------------------
订阅 /input/pose    →   订阅 /turtlesim1/turtle1/pose
发布 /output/cmd_vel →  发布 /turtlesim2/turtle1/cmd_vel
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;重映射的通用语法&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;remappings=[
    (&apos;原话题名&apos;, &apos;新话题名&apos;),      # 话题重映射
    (&apos;原服务名&apos;, &apos;新服务名&apos;),      # 服务重映射
    (&apos;__ns&apos;, &apos;新的命名空间&apos;),      # 特殊：重映射节点自身命名空间
    (&apos;__node&apos;, &apos;新的节点名&apos;),      # 特殊：重映射节点自身名称
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;其他常见使用场景&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;避免名称冲突&lt;/strong&gt;：两个相同功能的节点需要同时运行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口标准化&lt;/strong&gt;：让不同节点遵循统一的接口命名规范&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试调试&lt;/strong&gt;：在不修改代码的情况下改变通信路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统集成&lt;/strong&gt;：连接来自不同开发者的节点&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;12. 坐标系管理&lt;/h2&gt;
&lt;h3&gt;机器人中的坐标系&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;att/ros-12.jpg&quot; alt=&quot;PixPin_2025-09-03_10-41-34&quot;&gt;&lt;/p&gt;
&lt;p&gt;在机器人学中，一切都是相对于某个参考系而言的。&lt;/p&gt;
&lt;h6&gt;1. 为什么需要多个坐标系？&lt;/h6&gt;
&lt;p&gt;想象一个简单的移动机器人：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它的&lt;strong&gt;激光雷达&lt;/strong&gt;检测到正前方 1 米处有一个障碍物。&lt;/li&gt;
&lt;li&gt;这个“1 米”是相对于谁的？是相对于激光雷达自身的光学中心。&lt;/li&gt;
&lt;li&gt;如果我们要让机械臂去触碰这个障碍物，或者让底盘绕开它，&lt;strong&gt;机械臂和底盘的控制器需要知道这个障碍物相对于它们自己的位置&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;因此，我们需要知道激光雷达坐标系、底盘坐标系、机械臂坐标系之间的变换关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;2. 常见的坐标系类型&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基坐标系 (&lt;code&gt;base_link&lt;/code&gt;)&lt;/strong&gt;：通常固定在机器人的中心（例如，底盘的中心）。它是机器人本体上最核心的参考系，其他所有机器人本体上的坐标系都直接或间接地与它相连。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;里程计坐标系 (&lt;code&gt;odom&lt;/code&gt;)&lt;/strong&gt;：一个&lt;strong&gt;世界固定&lt;/strong&gt;的坐标系。它的原点通常是机器人启动时的位置。&lt;code&gt;odom&lt;/code&gt; 坐标系是通过里程计信息（如轮式编码器、视觉里程计）累积得到的，虽然短期内精确，但长期会有漂移，不会跳变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地图坐标系 (&lt;code&gt;map&lt;/code&gt;)&lt;/strong&gt;：一个&lt;strong&gt;世界固定&lt;/strong&gt;的坐标系。它通过传感器（如激光雷达、GPS）融合建图算法（如 SLAM）得到，是全局一致、无漂移的。但可能会发生跳变（例如，当算法修正位姿时）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传感器坐标系&lt;/strong&gt;：如 &lt;code&gt;laser_frame&lt;/code&gt;, &lt;code&gt;camera_depth_optical_frame&lt;/code&gt;, &lt;code&gt;imu_link&lt;/code&gt;。这些坐标系定义了传感器相对于 &lt;code&gt;base_link&lt;/code&gt; 的安装位置和方向。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具坐标系&lt;/strong&gt;：如机械臂末端的 &lt;code&gt;tool0&lt;/code&gt; 坐标系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;3. 坐标系变换 (Transform)&lt;/h6&gt;
&lt;p&gt;坐标系之间的关系由 &lt;strong&gt;变换&lt;/strong&gt; 来描述。一个变换包含两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;平移 (Translation)&lt;/strong&gt;：一个坐标系的原点在另一个坐标系中的 &lt;code&gt;(x, y, z)&lt;/code&gt; 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旋转 (Rotation)&lt;/strong&gt;：一个坐标系的姿态相对于另一个坐标系的旋转，通常用&lt;strong&gt;四元数 (Quaternion)&lt;/strong&gt; 来表示（比欧拉角更稳定，无万向锁问题）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个变换可以完美地回答：“在 &lt;code&gt;laser_frame&lt;/code&gt; 中看到的点 &lt;code&gt;(x, y, z)&lt;/code&gt;，在 &lt;code&gt;base_link&lt;/code&gt; 坐标系中是多少？”&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;TF 2 - 坐标变换管理神器&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;TF 2&lt;/strong&gt; (Transform Library version 2) 是 ROS 2 中用于跟踪多个坐标系关系的库和工具集。它是 ROS 1 TF 库的升级版，更高效、更安全。&lt;/p&gt;
&lt;h6&gt;1. TF 2 的核心功能&lt;/h6&gt;
&lt;p&gt;TF 2 维护着一个&lt;strong&gt;随时间变化的坐标系树&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;树状结构&lt;/strong&gt;：所有坐标系通过变换连接成一棵树。每个坐标系只有一个父坐标系，但可以有多个子坐标系。例如：&lt;code&gt;map&lt;/code&gt; -&gt; &lt;code&gt;odom&lt;/code&gt; -&gt; &lt;code&gt;base_link&lt;/code&gt; -&gt; &lt;code&gt;laser_frame&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：TF 2 不仅存储最新的变换，还会&lt;strong&gt;缓存一段时间的历史变换&lt;/strong&gt;。这意味着你可以查询“&lt;strong&gt;5 秒前&lt;/strong&gt;的 &lt;code&gt;laser_frame&lt;/code&gt; 相对于 &lt;code&gt;map&lt;/code&gt; 的变换是什么？”，这对于处理不同时间戳的传感器数据至关重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;2. TF 2 的工作流程：广播与监听&lt;/h6&gt;
&lt;p&gt;TF 2 采用“发布-订阅”模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;广播 (Broadcast)&lt;/strong&gt;：节点计算出一个坐标系相对于另一个坐标系的变换，然后将其&lt;strong&gt;发布&lt;/strong&gt;到 &lt;code&gt;/tf_static&lt;/code&gt;（对于静态的、不变的变换，如传感器安装位置）或 &lt;code&gt;/tf&lt;/code&gt;（对于动态的、变化的变换，如移动的底盘）话题上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监听 (Listen)&lt;/strong&gt;：需要用到坐标变换的节点（例如，需要将激光点云数据转换到 &lt;code&gt;map&lt;/code&gt; 坐标系中进行建图），会向 TF 2 库&lt;strong&gt;查询&lt;/strong&gt;特定的变换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;3. 核心工具和命令&lt;/h6&gt;
&lt;p&gt;ROS 2 提供了一系列强大的命令行工具来调试和可视化 TF：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 run tf2_ros tf2_echo &amp;#x3C;source_frame&gt; &amp;#x3C;target_frame&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：在终端中实时打印两个坐标系之间的变换（平移和旋转）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;code&gt;ros2 run tf2_ros tf2_echo map base_link&lt;/code&gt; 查看机器人当前位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 run rviz2 rviz2&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：最强大的可视化工具。添加 &lt;code&gt;TF&lt;/code&gt; 显示插件，可以看到所有坐标系的相对位置和姿态的 3 D 可视化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 run tf2_tools view_frames.py&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：生成一个当前坐标系树的 PDF 图示，清晰地显示所有坐标系之间的连接关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ros2 topic echo /tf&lt;/code&gt; 和 &lt;code&gt;ros2 topic echo /tf_static&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：直接查看发布到话题上的原始变换消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h5&gt;在代码中使用 TF 2 (Python 示例)&lt;/h5&gt;
&lt;h6&gt;1. 广播一个静态变换（例如，激光雷达坐标系）&lt;/h6&gt;
&lt;p&gt;静态变换只需要广播一次。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from tf2_ros import StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped

class StaticTFBroadcaster(Node):
    def __init__(self):
        super().__init__(&apos;static_tf_broadcaster&apos;)
        self.tf_broadcaster_ = StaticTransformBroadcaster(self)
        
        transform = TransformStamped()
        
        # 设置头信息
        transform.header.stamp = self.get_clock().now().to_msg()
        transform.header.frame_id = &apos;base_link&apos;  # 父坐标系
        transform.child_frame_id = &apos;laser_frame&apos; # 子坐标系
        
        # 设置变换：激光雷达安装在 base_link 前方 0.2m，高 0.1m 的位置
        transform.transform.translation.x = 0.2
        transform.transform.translation.y = 0.0
        transform.transform.translation.z = 0.1
        # 没有旋转，所以使用单位四元数
        transform.transform.rotation.x = 0.0
        transform.transform.rotation.y = 0.0
        transform.transform.rotation.z = 0.0
        transform.transform.rotation.w = 1.0
        
        # 发布静态变换
        self.tf_broadcaster_.sendTransform(transform)
        self.get_logger().info(&apos;Published static transform from base_link to laser_frame&apos;)

def main(args=None):
    rclpy.init(args=args)
    node = StaticTFBroadcaster()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2. 监听/查询一个变换&lt;/h6&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from tf2_ros import TransformException
from tf2_ros.buffer import Buffer
from tf2_ros.transform_listener import TransformListener

class TFListener(Node):
    def __init__(self):
        super().__init__(&apos;tf_listener&apos;)
        self.tf_buffer = Buffer()
        self.tf_listener = TransformListener(self.tf_buffer, self)
        
        # 创建一个定时器，定期查询变换
        self.timer = self.create_timer(1.0, self.on_timer)
        
    def on_timer(self):
        try:
            # 查找最新的从 laser_frame 到 base_link 的变换
            # target_frame, source_frame
            transform = self.tf_buffer.lookup_transform(
                &apos;base_link&apos;,
                &apos;laser_frame&apos;,
                rclpy.time.Time()) # 获取最新时间的变换
            self.get_logger().info(f&apos;Transform is: {transform}&apos;)
        except TransformException as ex:
            self.get_logger().error(f&apos;Could not get transform: {ex}&apos;)

def main(args=None):
    rclpy.init(args=args)
    node = TFListener()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__  &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;3. 转换一个点&lt;/h6&gt;
&lt;p&gt;这是最常见的应用：将一个点从一个坐标系转换到另一个坐标系。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from tf2_geometry_msgs import do_transform_point
from geometry_msgs.msg import PointStamped

# 假设我们有一个在 laser_frame 中的点
point_in_laser = PointStamped()
point_in_laser.header.frame_id = &apos;laser_frame&apos;
point_in_laser.point.x = 1.0
point_in_laser.point.y = 0.5
point_in_laser.point.z = 0.0

try:
    # 先查询变换
    transform = self.tf_buffer.lookup_transform(&apos;base_link&apos;, &apos;laser_frame&apos;, rclpy.time.Time())
    # 转换点
    point_in_base = do_transform_point(point_in_laser, transform)
    self.get_logger().info(f&apos;Point in base_link: {point_in_base.point}&apos;)
except TransformException as ex:
    self.get_logger().error(f&apos;Could not transform point: {ex}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;13. urdf&lt;/h2&gt;
&lt;h5&gt;1. URDF 是什么？&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;URDF&lt;/strong&gt; 的全称是 &lt;strong&gt;统一机器人描述格式&lt;/strong&gt;。它是一种基于 XML 的文件格式，用于在机器人领域中描述一个机器人的&lt;strong&gt;模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;您可以把它想象成机器人的“蓝图”或“解剖图”，它定义了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机器人的物理结构&lt;/strong&gt;：有哪些部件（连杆、关节）以及它们如何连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器人的几何属性&lt;/strong&gt;：每个部件的大小、形状（长方体、圆柱体、球体等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器人的运动学属性&lt;/strong&gt;：关节的类型（旋转、平移、固定等）以及它们如何运动（旋转轴、移动方向）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器人的动力学属性&lt;/strong&gt;：质量、惯性矩、摩擦系数等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉属性&lt;/strong&gt;：每个部件的颜色、纹理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;碰撞属性&lt;/strong&gt;：用于碰撞检测的简化几何模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 为什么需要 URDF？&lt;/h5&gt;
&lt;p&gt;URDF 文件是许多高级功能的基石：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;仿真&lt;/strong&gt;：在 Gazebo、PyBullet 等仿真环境中，需要 URDF 来生成和模拟机器人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可视化&lt;/strong&gt;：在 Rviz 等工具中，需要 URDF 来显示机器人模型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运动规划&lt;/strong&gt;：MoveIt! 等规划框架需要知道机器人的运动链结构，这来自于 URDF。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制&lt;/strong&gt;：控制器管理器需要 URDF 来理解关节之间的关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单来说，&lt;strong&gt;没有 URDF，ROS 中的机器人就像没有身体的灵魂，无法被看到、模拟或控制。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;3. 核心概念与 XML 标签&lt;/h5&gt;
&lt;p&gt;一个 URDF 文件总是以 &lt;code&gt;&amp;#x3C;robot&gt;&lt;/code&gt; 标签作为根元素，里面包含两种最核心的元素：&lt;strong&gt;&lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;code&gt;&amp;#x3C;joint&gt;&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;h6&gt;3.1 连杆：&lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt;&lt;/h6&gt;
&lt;p&gt;连杆代表机器人的一个&lt;strong&gt;刚体部分&lt;/strong&gt;，是机器人的基本构成块。例如：机器人底座、轮子、激光雷达、机械臂的每一个臂杆、摄像头顶盖等。&lt;/p&gt;
&lt;p&gt;一个典型的 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 包含以下子元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;visual&gt;&lt;/code&gt;&lt;/strong&gt;：定义连杆的&lt;strong&gt;外观&lt;/strong&gt;，用于可视化（如在 Rviz 中显示）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;geometry&gt;&lt;/code&gt;：形状。可以是 &lt;code&gt;&amp;#x3C;box&gt;&lt;/code&gt;（长方体）、&lt;code&gt;&amp;#x3C;cylinder&gt;&lt;/code&gt;（圆柱体）、&lt;code&gt;&amp;#x3C;sphere&gt;&lt;/code&gt;（球体）或 &lt;code&gt;&amp;#x3C;mesh&gt;&lt;/code&gt;（网格文件，如 .dae 或 .stl）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;origin&gt;&lt;/code&gt;：几何体的参考坐标系相对于世界坐标系的位姿（xyz 偏移和 rpy 旋转）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;material&gt;&lt;/code&gt;：材质。可以指定颜色 (&lt;code&gt;&amp;#x3C;color&gt;&lt;/code&gt;) 或纹理 (&lt;code&gt;&amp;#x3C;texture&gt;&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;collision&gt;&lt;/code&gt;&lt;/strong&gt;：定义连杆的&lt;strong&gt;碰撞属性&lt;/strong&gt;，用于物理仿真中的碰撞检测。为了计算效率，碰撞模型通常比视觉模型更简单（例如，用一个长方体近似一个复杂的机械臂连杆）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包含 &lt;code&gt;&amp;#x3C;geometry&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;origin&gt;&lt;/code&gt;，与 &lt;code&gt;&amp;#x3C;visual&gt;&lt;/code&gt; 中的类似。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;inertial&gt;&lt;/code&gt;&lt;/strong&gt;：定义连杆的&lt;strong&gt;动力学属性&lt;/strong&gt;，用于物理仿真。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;mass&gt;&lt;/code&gt;：质量（千克）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;origin&gt;&lt;/code&gt;：质心位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;inertia&gt;&lt;/code&gt;：惯性张量（一个 3 x 3 矩阵）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：定义一个名为 “base_link” 的长方体连杆&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;link name=&quot;base_link&quot;&gt;
  &amp;#x3C;visual&gt;
    &amp;#x3C;geometry&gt;
      &amp;#x3C;box size=&quot;0.2 0.1 0.05&quot;/&gt; &amp;#x3C;!-- 长宽高 --&gt;
    &amp;#x3C;/geometry&gt;
    &amp;#x3C;origin rpy=&quot;0 0 0&quot; xyz=&quot;0 0 0&quot;/&gt;
    &amp;#x3C;material name=&quot;blue&quot;&gt;
      &amp;#x3C;color rgba=&quot;0 0 0.8 1&quot;/&gt; &amp;#x3C;!-- RGBA，A为透明度 --&gt;
    &amp;#x3C;/material&gt;
  &amp;#x3C;/visual&gt;
  &amp;#x3C;collision&gt;
    &amp;#x3C;geometry&gt;
      &amp;#x3C;box size=&quot;0.2 0.1 0.05&quot;/&gt;
    &amp;#x3C;/geometry&gt;
  &amp;#x3C;/collision&gt;
  &amp;#x3C;inertial&gt;
    &amp;#x3C;mass value=&quot;0.5&quot;/&gt;
    &amp;#x3C;origin xyz=&quot;0 0 0&quot;/&gt;
    &amp;#x3C;inertia ixx=&quot;0.001&quot; ixy=&quot;0&quot; ixz=&quot;0&quot; iyy=&quot;0.001&quot; iyz=&quot;0&quot; izz=&quot;0.001&quot;/&gt;
  &amp;#x3C;/inertial&gt;
&amp;#x3C;/link&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;3.2 关节：&lt;code&gt;&amp;#x3C;joint&gt;&lt;/code&gt;&lt;/h6&gt;
&lt;p&gt;关节描述了两个连杆之间的&lt;strong&gt;连接方式&lt;/strong&gt;，即一个连杆（子连杆）如何相对于另一个连杆（父连杆）运动。&lt;/p&gt;
&lt;p&gt;一个 &lt;code&gt;&amp;#x3C;joint&gt;&lt;/code&gt; 的关键属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;: 关节的名称（如 “wheel_joint”）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt;: 关节的类型：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;revolute&lt;/code&gt;：旋转关节（绕单一轴旋转，有位置极限，如机械臂关节）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;continuous&lt;/code&gt;：连续旋转关节（绕单一轴无限旋转，如轮子）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prismatic&lt;/code&gt;：平移关节（沿单一轴移动，有位置极限，如伸缩杆）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixed&lt;/code&gt;：固定关节（不允许任何运动，子连杆牢牢固定在父连杆上，如传感器固定在底座上）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;planar&lt;/code&gt;：平面关节（在垂直于轴的平面内运动）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;floating&lt;/code&gt;：浮动关节（完全不受约束的运动，6 自由度）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个 &lt;code&gt;&amp;#x3C;joint&gt;&lt;/code&gt; 包含以下子元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;parent&gt;&lt;/code&gt;&lt;/strong&gt;：父连杆的名字 (&lt;code&gt;&amp;#x3C;link name=”...”&gt;&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;child&gt;&lt;/code&gt;&lt;/strong&gt;：子连杆的名字。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;origin&gt;&lt;/code&gt;&lt;/strong&gt;：从父连杆坐标系到子连杆坐标系的变换。这定义了子连杆在“零位”时的位置和姿态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;axis&gt;&lt;/code&gt;&lt;/strong&gt;：关节运动所围绕的轴（在子连杆坐标系中）。对于旋转关节，这是旋转轴；对于平移关节，这是移动方向。默认是 (1, 0, 0)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;limit&gt;&lt;/code&gt;&lt;/strong&gt;（仅用于 &lt;code&gt;revolute&lt;/code&gt; 和 &lt;code&gt;prismatic&lt;/code&gt; 关节）：运动极限。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lower&lt;/code&gt;：下限（弧度或米）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upper&lt;/code&gt;：上限（弧度或米）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;：最大 effort（力或扭矩）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;velocity&lt;/code&gt;：最大速度（ rad/s 或 m/s）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：定义一个连接 “base_link” 和 “wheel” 的连续旋转关节&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;joint name=&quot;left_wheel_joint&quot; type=&quot;continuous&quot;&gt;
  &amp;#x3C;parent link=&quot;base_link&quot;/&gt;
  &amp;#x3C;child link=&quot;left_wheel&quot;/&gt;
  &amp;#x3C;origin xyz=&quot;0 0.05 0&quot; rpy=&quot;0 0 0&quot;/&gt; &amp;#x3C;!-- 轮子安装在底座旁边 --&gt;
  &amp;#x3C;axis xyz=&quot;0 1 0&quot;/&gt; &amp;#x3C;!-- 绕Y轴旋转 --&gt;
&amp;#x3C;/joint&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h5&gt;4. 一个完整的简单 URDF 示例&lt;/h5&gt;
&lt;p&gt;下面是一个简单的机器人模型，它有一个底座和两个轮子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot;?&gt;
&amp;#x3C;robot name=&quot;my_first_robot&quot;&gt;

  &amp;#x3C;!-- 底座 LINK --&gt;
  &amp;#x3C;link name=&quot;base_link&quot;&gt;
    &amp;#x3C;visual&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;box size=&quot;0.2 0.1 0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
      &amp;#x3C;material name=&quot;blue&quot;&gt;
        &amp;#x3C;color rgba=&quot;0 0 0.8 1&quot;/&gt;
      &amp;#x3C;/material&gt;
    &amp;#x3C;/visual&gt;
    &amp;#x3C;collision&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;box size=&quot;0.2 0.1 0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
    &amp;#x3C;/collision&gt;
    &amp;#x3C;inertial&gt;
      &amp;#x3C;mass value=&quot;0.5&quot;/&gt;
      &amp;#x3C;inertia ixx=&quot;0.001&quot; ixy=&quot;0&quot; ixz=&quot;0&quot; iyy=&quot;0.001&quot; iyz=&quot;0&quot; izz=&quot;0.001&quot;/&gt;
    &amp;#x3C;/inertial&gt;
  &amp;#x3C;/link&gt;

  &amp;#x3C;!-- 左轮 LINK --&gt;
  &amp;#x3C;link name=&quot;left_wheel&quot;&gt;
    &amp;#x3C;visual&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;cylinder length=&quot;0.02&quot; radius=&quot;0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
      &amp;#x3C;material name=&quot;black&quot;&gt;
        &amp;#x3C;color rgba=&quot;0 0 0 1&quot;/&gt;
      &amp;#x3C;/material&gt;
    &amp;#x3C;/visual&gt;
    &amp;#x3C;collision&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;cylinder length=&quot;0.02&quot; radius=&quot;0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
    &amp;#x3C;/collision&gt;
    &amp;#x3C;inertial&gt;
      &amp;#x3C;mass value=&quot;0.1&quot;/&gt;
      &amp;#x3C;inertia ixx=&quot;0.0001&quot; ixy=&quot;0&quot; ixz=&quot;0&quot; iyy=&quot;0.0001&quot; iyz=&quot;0&quot; izz=&quot;0.0001&quot;/&gt;
    &amp;#x3C;/inertial&gt;
  &amp;#x3C;/link&gt;

  &amp;#x3C;!-- 右轮 LINK --&gt;
  &amp;#x3C;link name=&quot;right_wheel&quot;&gt;
    &amp;#x3C;visual&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;cylinder length=&quot;0.02&quot; radius=&quot;0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
      &amp;#x3C;material name=&quot;black&quot;/&gt;
      &amp;#x3C;!-- 复用名为black的material --&gt;
    &amp;#x3C;/visual&gt;
    &amp;#x3C;collision&gt;
      &amp;#x3C;geometry&gt;
        &amp;#x3C;cylinder length=&quot;0.02&quot; radius=&quot;0.05&quot;/&gt;
      &amp;#x3C;/geometry&gt;
    &amp;#x3C;/collision&gt;
    &amp;#x3C;inertial&gt;
      &amp;#x3C;mass value=&quot;0.1&quot;/&gt;
      &amp;#x3C;inertia ixx=&quot;0.0001&quot; ixy=&quot;0&quot; ixz=&quot;0&quot; iyy=&quot;0.0001&quot; iyz=&quot;0&quot; izz=&quot;0.0001&quot;/&gt;
    &amp;#x3C;/inertial&gt;
  &amp;#x3C;/link&gt;

  &amp;#x3C;!-- 连接底座和左轮的 JOINT --&gt;
  &amp;#x3C;joint name=&quot;left_wheel_joint&quot; type=&quot;continuous&quot;&gt;
    &amp;#x3C;parent link=&quot;base_link&quot;/&gt;
    &amp;#x3C;child link=&quot;left_wheel&quot;/&gt;
    &amp;#x3C;origin xyz=&quot;0 0.06 -0.03&quot; rpy=&quot;1.5707 0 0&quot;/&gt; 
    &amp;#x3C;!-- 
    xyz: 安装在底座右侧(y方向)和略下方(z方向)
    rpy: 将圆柱体绕X轴旋转90度（π/2≈1.5707），使其立起来
    --&gt;
    &amp;#x3C;axis xyz=&quot;0 1 0&quot;/&gt;
  &amp;#x3C;/joint&gt;

  &amp;#x3C;!-- 连接底座和右轮的 JOINT --&gt;
  &amp;#x3C;joint name=&quot;right_wheel_joint&quot; type=&quot;continuous&quot;&gt;
    &amp;#x3C;parent link=&quot;base_link&quot;/&gt;
    &amp;#x3C;child link=&quot;right_wheel&quot;/&gt;
    &amp;#x3C;origin xyz=&quot;0 -0.06 -0.03&quot; rpy=&quot;1.5707 0 0&quot;/&gt;
    &amp;#x3C;axis xyz=&quot;0 1 0&quot;/&gt;
  &amp;#x3C;/joint&gt;

&amp;#x3C;/robot&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h5&gt;5. 超越基础：URDF 的局限性及解决方案&lt;/h5&gt;
&lt;p&gt;URDF 非常流行，但它也有明显的局限性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;是单体的&lt;/strong&gt;：URDF 只能描述一个完整的、不可分割的机器人。你不能在 URDF 中定义两个独立的机器人并让它们互动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;是树形结构&lt;/strong&gt;：URDF 模型必须是一棵树，每个子连杆只能有一个父连杆。这意味着它不能描述&lt;strong&gt;闭环结构&lt;/strong&gt;（例如，一个四杆机构，一个连杆同时被两个关节连接）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺乏可扩展性&lt;/strong&gt;：对于复杂的机器人，一个巨大的 URDF 文件难以维护。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决这些问题，ROS 引入了其他工具和格式作为补充：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Xacro (XML Macros)&lt;/strong&gt;：&lt;strong&gt;这是最重要的实践！&lt;/strong&gt; Xacro 是一种宏语言，允许你编写更短、更可读、更模块化的 URDF 文件。它支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;属性（变量）&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;xacro:property name=”width” value=”0.2”/&gt;&lt;/code&gt;，然后用 &lt;code&gt;${width}&lt;/code&gt; 引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宏（函数）&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;xacro:macro name=”create_wheel” params=”name”&gt; ... &amp;#x3C;/xacro:macro&gt;&lt;/code&gt;，然后用 &lt;code&gt;&amp;#x3C;xacro:create_wheel name=”left”/&gt;&lt;/code&gt; 调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件包含&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;xacro:include filename=”$(find my_robot)/urdf/common.xacro”/&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件判断&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;xacro:if value=”${use_mesh}&quot;&gt; ... &amp;#x3C;/xacro:if&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最终，Xacro 文件会被预处理成一个纯粹的、扁平的 URDF 文件。&lt;strong&gt;强烈建议始终使用 Xacro 来编写机器人模型。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SDF (Simulation Description Format)&lt;/strong&gt;：被 Gazebo 原生使用的格式。它比 URDF 更强大，支持多个机器人、闭环、插件、大气效果等。当你把 URDF 放入 Gazebo 时，Gazebo 会将其转换为 SDF。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;joint&gt;&lt;/code&gt; 标签的巧用&lt;/strong&gt;：虽然 URDF 本身是树形结构，但通过巧妙定义虚拟的、零质量的连杆和固定关节，有时可以“模拟”出闭环结构，但这很繁琐。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;6. 工具与工作流程&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编写&lt;/strong&gt;：使用你喜欢的文本编辑器（如 VSCode）编写 &lt;code&gt;.urdf&lt;/code&gt; 或 &lt;code&gt;.xacro&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查语法&lt;/strong&gt;：&lt;code&gt;check_urdf my_robot.urdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可视化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;urdf_to_graphiz my_robot.urdf&lt;/code&gt;：生成一个 PDF，显示连杆和关节的树状图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rviz&lt;/strong&gt;：启动 Rviz，添加 RobotModel 显示类型，并设置 Description 参数指向你的 URDF 文件路径（例如 &lt;code&gt;robot_description&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 Gazebo 中仿真&lt;/strong&gt;：需要为你的 URDF 添加 Gazebo 特定的标签（如 &lt;code&gt;&amp;#x3C;gazebo&gt;&lt;/code&gt;）来定义仿真属性（颜色、摩擦、传感器插件等）。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CUDA 线程模型与内存优化</title><link>https://iaohr9.github.io/blog/cuda%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%B8%8E%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96</link><guid isPermaLink="true">https://iaohr9.github.io/blog/cuda%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%B8%8E%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96</guid><description>CUDA 线程模型、SM、warp、内存层次、nvcc 编译流程、事件计时与内存访问最佳实践整理。</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;CUDA 笔记整理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;按截图顺序整理。能抽象重画的图已经改为流程图、表格或公式；代码和索引规则单独整理，方便之后复习。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;01. 线程模型与物理结构&lt;/h2&gt;
&lt;p&gt;CUDA 的线程模型是软件层面的逻辑划分，常见层级为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Grid -&gt; Block -&gt; Thread
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GPU 的物理结构是硬件层面的执行资源，常见层级为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Device / GPU -&gt; SM -&gt; CUDA Core
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;左侧线程模型是在逻辑角度分析程序；右侧物理结构是在硬件角度分析执行资源。线程模型可以定义成千上万个线程，但真正同时活跃的线程数量会受到 SM 资源限制。&lt;/p&gt;
&lt;p&gt;线程块是调度到 SM 的基本单位。一个 grid 中的所有线程块都需要被分配到 SM 上执行；同一个线程块中的所有线程会被分配到同一个 SM 中执行。一个 SM 可以同时容纳多个线程块，但一个线程块一旦被分配到某个 SM，就不会再被拆分到其他 SM。&lt;/p&gt;
&lt;p&gt;线程块分配到 SM 后，会继续以 &lt;code&gt;32&lt;/code&gt; 个线程为一组划分成线程束（warp）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-01.QE1oBAMd_2my5zU.webp&quot; alt=&quot;Diagram 01&quot;&gt;&lt;/p&gt;
&lt;h2&gt;02. 一维线程模型&lt;/h2&gt;
&lt;p&gt;一维线程模型中，只使用 &lt;code&gt;x&lt;/code&gt; 维度来组织 grid 和 block。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;kernel_fun&amp;#x3C;&amp;#x3C;&amp;#x3C;2, 4&gt;&gt;&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gridDim.x = 2&lt;/code&gt;：一共有 &lt;code&gt;2&lt;/code&gt; 个线程块。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockDim.x = 4&lt;/code&gt;：每个线程块有 &lt;code&gt;4&lt;/code&gt; 个线程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockIdx.x&lt;/code&gt; 的取值范围是 &lt;code&gt;0 ~ 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx.x&lt;/code&gt; 的取值范围是 &lt;code&gt;0 ~ 3&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个线程在整个 grid 中的全局一维编号可以这样计算：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = threadIdx.x + blockIdx.x * blockDim.x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应关系如下：&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;blockIdx.x&lt;/code&gt; | &lt;code&gt;threadIdx.x&lt;/code&gt; | 全局编号 &lt;code&gt;idx&lt;/code&gt; |
|---:|---:|---:|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 0 | 2 | 2 |
| 0 | 3 | 3 |
| 1 | 0 | 4 |
| 1 | 1 | 5 |
| 1 | 2 | 6 |
| 1 | 3 | 7 |&lt;/p&gt;
&lt;h2&gt;03. 线程束&lt;/h2&gt;
&lt;p&gt;CUDA 使用 SIMT（Single Instruction Multiple Threads，单指令多线程）模型管理线程执行。线程进入 SM 后，会以 &lt;code&gt;32&lt;/code&gt; 个线程为一组组成一个线程束，即 &lt;code&gt;warp&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在同一个线程块中，相邻的 &lt;code&gt;32&lt;/code&gt; 个线程构成一个 warp。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;0 ~ 31&lt;/code&gt; 个线程属于 &lt;code&gt;warp 0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;32 ~ 63&lt;/code&gt; 个线程属于 &lt;code&gt;warp 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;依此类推。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程束不会跨线程块组成。也就是说，每个 warp 中只包含同一个 block 中的线程。&lt;/p&gt;
&lt;p&gt;常用的线程束内编号计算方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int thread_id_in_block = threadIdx.x;
int warp_id = thread_id_in_block / 32;
int lane_id = thread_id_in_block % 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-02.Cv2nooaN_2nJeaJ.webp&quot; alt=&quot;Diagram 02&quot;&gt;&lt;/p&gt;
&lt;h2&gt;04. 流多处理器 SM&lt;/h2&gt;
&lt;p&gt;GPU 的并行性主要依赖 SM（Streaming Multiprocessor，流多处理器）完成。一个 GPU 通常由多个 SM 构成。&lt;/p&gt;
&lt;p&gt;以 Fermi 架构为例，SM 中的关键资源包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CUDA 核心（CUDA Core）&lt;/li&gt;
&lt;li&gt;共享内存 / L1 缓存（Shared Memory / L1 Cache）&lt;/li&gt;
&lt;li&gt;寄存器文件（Register File）&lt;/li&gt;
&lt;li&gt;加载和存储单元（Load / Store Units）&lt;/li&gt;
&lt;li&gt;特殊函数单元（Special Function Unit，SFU）&lt;/li&gt;
&lt;li&gt;Warp 调度器（Warp Scheduler）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-03.BRgy89eP_1eQHGY.webp&quot; alt=&quot;Diagram 03&quot;&gt;
SM 内部资源有限，所以同一时刻能驻留的线程块、线程束和线程数量会受到寄存器、共享内存、最大线程数等资源限制。&lt;/p&gt;
&lt;h2&gt;05. SM 上的并发与并行&lt;/h2&gt;
&lt;p&gt;GPU 中每个 SM 都可以支持数百个线程并发执行。调度时以线程块 &lt;code&gt;block&lt;/code&gt; 为单位向 SM 分配任务，多个线程块可以同时分配到一个可用的 SM 上。&lt;/p&gt;
&lt;p&gt;需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程块是分配到 SM 的基本单位。&lt;/li&gt;
&lt;li&gt;多个线程块可以驻留在同一个 SM。&lt;/li&gt;
&lt;li&gt;一个线程块一旦分配到某个 SM，就不会再分配到其他 SM。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并发与并行的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发：多个任务在时间上交错推进，不一定同时执行。&lt;/li&gt;
&lt;li&gt;并行：多个任务在硬件上真正同时执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-04.CdSiA4cD_1Ogkb0.webp&quot; alt=&quot;Diagram 04&quot;&gt;&lt;/p&gt;
&lt;h2&gt;06. 线程束与线程块大小&lt;/h2&gt;
&lt;p&gt;线程束是 GPU 硬件上非常重要的执行单位：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程束只包含同一个线程块中的线程。&lt;/li&gt;
&lt;li&gt;每个线程束包含 &lt;code&gt;32&lt;/code&gt; 个线程。&lt;/li&gt;
&lt;li&gt;SM 通常以 warp 为单位调度和发射指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果一个线程块的线程数不是 &lt;code&gt;32&lt;/code&gt; 的整数倍，最后一个 warp 会存在不满 &lt;code&gt;32&lt;/code&gt; 个有效线程的情况，剩余 lane 会处于非活跃状态。&lt;/p&gt;
&lt;p&gt;线程块中 warp 数量的计算方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int warp_num = (threads_per_block + 31) / 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;p&gt;| 线程块大小 | warp 划分 | 说明 |
|---:|---:|---|
| &lt;code&gt;(128, 1, 1)&lt;/code&gt; | &lt;code&gt;4&lt;/code&gt; 个 warp | &lt;code&gt;128 / 32 = 4&lt;/code&gt;，全部为满 warp |
| &lt;code&gt;(112, 1, 1)&lt;/code&gt; | &lt;code&gt;4&lt;/code&gt; 个 warp | 前 &lt;code&gt;3&lt;/code&gt; 个满 warp，最后一个 warp 只有 &lt;code&gt;16&lt;/code&gt; 个有效线程 |&lt;/p&gt;
&lt;h2&gt;07. 内存分配&lt;/h2&gt;
&lt;p&gt;主机端内存使用 C / C++ 的内存分配方式，例如 &lt;code&gt;malloc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *fpHost_A;
fpHost_A = (float *)malloc(nBytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设备端内存使用 CUDA Runtime API 中的 &lt;code&gt;cudaMalloc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *fpDevice_A;
cudaMalloc((void **)&amp;#x26;fpDevice_A, nBytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cudaMalloc&lt;/code&gt; 的函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaError_t cudaMalloc(void **devPtr, size_t size);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;devPtr&lt;/code&gt;：指向设备端已分配内存地址的指针，因此需要传入二级指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;：申请的字节数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回值类型为 &lt;code&gt;cudaError_t&lt;/code&gt;。常见返回结果包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cudaSuccess&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cudaErrorInvalidValue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cudaErrorMemoryAllocation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;08. 内存管理&lt;/h2&gt;
&lt;p&gt;CUDA 的内存管理主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内存分配&lt;/li&gt;
&lt;li&gt;数据传递&lt;/li&gt;
&lt;li&gt;内存初始化&lt;/li&gt;
&lt;li&gt;内存释放&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;标准 C 语言内存管理函数与 CUDA 内存管理函数的对应关系如下：&lt;/p&gt;
&lt;p&gt;| 标准 C 函数 | CUDA C 函数 | 作用 |
|---|---|---|
| &lt;code&gt;malloc&lt;/code&gt; | &lt;code&gt;cudaMalloc&lt;/code&gt; | 分配内存 |
| &lt;code&gt;memcpy&lt;/code&gt; | &lt;code&gt;cudaMemcpy&lt;/code&gt; | 拷贝数据 |
| &lt;code&gt;memset&lt;/code&gt; | &lt;code&gt;cudaMemset&lt;/code&gt; | 初始化内存 |
| &lt;code&gt;free&lt;/code&gt; | &lt;code&gt;cudaFree&lt;/code&gt; | 释放内存 |&lt;/p&gt;
&lt;p&gt;CPU 和 GPU 通常有不同的内存空间。主机端数组不能直接当作设备端数组使用，通常需要用 &lt;code&gt;cudaMemcpy&lt;/code&gt; 在 Host 和 Device 之间传递数据。&lt;/p&gt;
&lt;h2&gt;09. 指定真实架构计算能力&lt;/h2&gt;
&lt;p&gt;PTX 指令最终会被转换为二进制 &lt;code&gt;cubin&lt;/code&gt; 代码，而 &lt;code&gt;cubin&lt;/code&gt; 与具体 GPU 架构相关。&lt;/p&gt;
&lt;p&gt;指定真实架构计算能力的编译选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-code=sm_XY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X&lt;/code&gt;：计算能力主版本号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Y&lt;/code&gt;：计算能力次版本号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sm_XY&lt;/code&gt;：真实 GPU 架构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;二进制 &lt;code&gt;cubin&lt;/code&gt; 代码与具体架构相关，不同大版本架构之间通常不能直接兼容。&lt;/li&gt;
&lt;li&gt;如果同时指定虚拟架构和真实架构，真实架构计算能力必须大于或等于虚拟架构计算能力。&lt;/li&gt;
&lt;li&gt;例如 &lt;code&gt;-arch=compute_61 -code=sm_60&lt;/code&gt; 不合理，因为真实架构 &lt;code&gt;sm_60&lt;/code&gt; 小于虚拟架构 &lt;code&gt;compute_61&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更合理的写法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nvcc helloworld.cu -o helloworld -arch=compute_61 -code=sm_61
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者降低虚拟架构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nvcc helloworld.cu -o helloworld -arch=compute_60 -code=sm_60
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. nvcc 编译流程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;nvcc&lt;/code&gt; 会把 &lt;code&gt;.cu&lt;/code&gt; 源代码分离成两类代码：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主机代码（Host code）&lt;/li&gt;
&lt;li&gt;设备代码（Device code）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;主机代码使用普通 C / C++ 语法编写，由主机编译器处理。设备代码使用 CUDA C / C++ 扩展语法编写，由 &lt;code&gt;nvcc&lt;/code&gt; 处理。&lt;/p&gt;
&lt;p&gt;设备代码的编译大致分为两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先将设备代码编译为 PTX（Parallel Thread Execution）伪汇编代码。&lt;/li&gt;
&lt;li&gt;再将 PTX 编译为二进制 &lt;code&gt;cubin&lt;/code&gt; 目标代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-05.DTaVvDzU_Z2kdN0y.webp&quot; alt=&quot;Diagram 05&quot;&gt;
相关选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-arch=compute_XY
-code=sm_ZW
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-arch=compute_XY&lt;/code&gt;：指定虚拟架构计算能力，用于决定 PTX 中可以使用的 CUDA 功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-code=sm_ZW&lt;/code&gt;：指定真实架构计算能力，用于决定最终可执行代码能在哪类 GPU 上运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. 三维网格与三维线程块&lt;/h2&gt;
&lt;p&gt;CUDA 可以定义三维 grid 和三维 block：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;dim3 grid_size(2, 2, 2);
dim3 block_size(4, 4, 2);

kernel_fun&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blockIdx.x&lt;/code&gt;、&lt;code&gt;blockIdx.y&lt;/code&gt;、&lt;code&gt;blockIdx.z&lt;/code&gt; 的取值范围都是 &lt;code&gt;0 ~ 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx.x&lt;/code&gt;、&lt;code&gt;threadIdx.y&lt;/code&gt; 的取值范围都是 &lt;code&gt;0 ~ 3&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx.z&lt;/code&gt; 的取值范围是 &lt;code&gt;0 ~ 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三维 block 编号可以线性化为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int blockId = blockIdx.x
            + blockIdx.y * gridDim.x
            + gridDim.x * gridDim.y * blockIdx.z;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三维 block 内部的线程编号可以线性化为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int threadId = threadIdx.z * (blockDim.x * blockDim.y)
             + threadIdx.y * blockDim.x
             + threadIdx.x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个 grid 中的全局线性编号为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int id = blockId * (blockDim.x * blockDim.y * blockDim.z) + threadId;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12. 推广到多维线程&lt;/h2&gt;
&lt;p&gt;多维网格和多维线程块本质上仍然是逻辑组织方式。GPU 物理执行时并不会真的把线程按二维或三维空间摆放，而是通过索引计算把多维结构映射成线性编号。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;dim3 grid_size(3, 2, 1);
dim3 block_size(5, 3, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个配置表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;grid 有 &lt;code&gt;3 * 2 * 1 = 6&lt;/code&gt; 个线程块。&lt;/li&gt;
&lt;li&gt;每个 block 有 &lt;code&gt;5 * 3 * 1 = 15&lt;/code&gt; 个线程。&lt;/li&gt;
&lt;li&gt;整个 kernel 启动了 &lt;code&gt;6 * 15 = 90&lt;/code&gt; 个线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;二维情况下常见的线性化方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int tid = threadIdx.y * blockDim.x + threadIdx.x;
int bid = blockIdx.y * gridDim.x + blockIdx.x;
int id = bid * (blockDim.x * blockDim.y) + tid;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;13. 内建索引变量 &lt;code&gt;blockIdx&lt;/code&gt; 和 &lt;code&gt;threadIdx&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;CUDA 可以组织三维网格和三维线程块。对应地，&lt;code&gt;blockIdx&lt;/code&gt; 和 &lt;code&gt;threadIdx&lt;/code&gt; 都是三维结构体类型，具有 &lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt;、&lt;code&gt;z&lt;/code&gt; 三个成员。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blockIdx&lt;/code&gt; 和 &lt;code&gt;threadIdx&lt;/code&gt; 的类型是 &lt;code&gt;uint3&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;blockIdx.x;
blockIdx.y;
blockIdx.z;

threadIdx.x;
threadIdx.y;
threadIdx.z;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个成员都是无符号整数类型。&lt;/p&gt;
&lt;h2&gt;14. &lt;code&gt;dim3&lt;/code&gt; 定义多维网格和线程块&lt;/h2&gt;
&lt;p&gt;多维 grid 和 block 可以用 &lt;code&gt;dim3&lt;/code&gt; 定义。&lt;code&gt;dim3&lt;/code&gt; 是 CUDA 中常用的三维尺寸类型，支持 C++ 构造函数语法。&lt;/p&gt;
&lt;p&gt;通用写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;dim3 grid_size(Gx, Gy, Gz);
dim3 block_size(Bx, By, Bz);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：定义一个 &lt;code&gt;2 x 2 x 1&lt;/code&gt; 的 grid 和一个 &lt;code&gt;5 x 3 x 1&lt;/code&gt; 的 block：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;dim3 grid_size(2, 2);   // 等价于 dim3 grid_size(2, 2, 1);
dim3 block_size(5, 3);  // 等价于 dim3 block_size(5, 3, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果省略第三个维度，默认值为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;15. 内建尺寸变量 &lt;code&gt;gridDim&lt;/code&gt; 和 &lt;code&gt;blockDim&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gridDim&lt;/code&gt; 和 &lt;code&gt;blockDim&lt;/code&gt; 是 &lt;code&gt;dim3&lt;/code&gt; 类型的内建变量，也具有 &lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt;、&lt;code&gt;z&lt;/code&gt; 三个成员：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;gridDim.x;
gridDim.y;
gridDim.z;

blockDim.x;
blockDim.y;
blockDim.z;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们对应 kernel 启动配置中的 grid 尺寸和 block 尺寸。&lt;/p&gt;
&lt;p&gt;索引变量的取值范围：&lt;/p&gt;
&lt;p&gt;| 变量 | 取值范围 |
|---|---|
| &lt;code&gt;blockIdx.x&lt;/code&gt; | &lt;code&gt;[0, gridDim.x - 1]&lt;/code&gt; |
| &lt;code&gt;blockIdx.y&lt;/code&gt; | &lt;code&gt;[0, gridDim.y - 1]&lt;/code&gt; |
| &lt;code&gt;blockIdx.z&lt;/code&gt; | &lt;code&gt;[0, gridDim.z - 1]&lt;/code&gt; |
| &lt;code&gt;threadIdx.x&lt;/code&gt; | &lt;code&gt;[0, blockDim.x - 1]&lt;/code&gt; |
| &lt;code&gt;threadIdx.y&lt;/code&gt; | &lt;code&gt;[0, blockDim.y - 1]&lt;/code&gt; |
| &lt;code&gt;threadIdx.z&lt;/code&gt; | &lt;code&gt;[0, blockDim.z - 1]&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;注意：这些内建变量用于设备端执行上下文，主机端代码不能直接访问它们。&lt;/p&gt;
&lt;h2&gt;16. 一维线程模型中的内建变量&lt;/h2&gt;
&lt;p&gt;一维情况下，每个线程在 kernel 中都有唯一身份标识。这个唯一标识由 kernel 启动配置决定：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在设备端，配置值会保存在内建变量中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gridDim.x&lt;/code&gt;：等于启动配置中的 &lt;code&gt;grid_size&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockDim.x&lt;/code&gt;：等于启动配置中的 &lt;code&gt;block_size&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程索引也保存在内建变量中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blockIdx.x&lt;/code&gt;：当前线程所在 block 在 grid 中的索引，范围为 &lt;code&gt;0 ~ gridDim.x - 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx.x&lt;/code&gt;：当前线程在线程块中的索引，范围为 &lt;code&gt;0 ~ blockDim.x - 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，一维全局线程编号为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = blockIdx.x * blockDim.x + threadIdx.x;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;17. 自定义设备函数&lt;/h2&gt;
&lt;p&gt;CUDA 中常见函数修饰符包括 &lt;code&gt;__device__&lt;/code&gt;、&lt;code&gt;__global__&lt;/code&gt; 和 &lt;code&gt;__host__&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;设备函数&lt;/h3&gt;
&lt;p&gt;设备函数（device function）只能在 GPU 设备上执行，用 &lt;code&gt;__device__&lt;/code&gt; 修饰。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只能在设备端执行。&lt;/li&gt;
&lt;li&gt;只能被 kernel 函数或其他设备函数调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__device__ float add_device(float a, float b) {
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核函数&lt;/h3&gt;
&lt;p&gt;核函数（kernel function）用 &lt;code&gt;__global__&lt;/code&gt; 修饰。它通常由主机端调用，在设备端执行。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用时需要使用 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;&lt;/code&gt; 配置执行维度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__global__&lt;/code&gt; 不能和 &lt;code&gt;__host__&lt;/code&gt; 同时使用，也不能和 &lt;code&gt;__device__&lt;/code&gt; 同时使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void kernel_fun(float *out) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    out[idx] = add_device(out[idx], 1.0f);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主机函数&lt;/h3&gt;
&lt;p&gt;主机函数（host function）在 CPU 端执行，可以用 &lt;code&gt;__host__&lt;/code&gt; 修饰。普通 C++ 函数默认就是主机函数，因此 &lt;code&gt;__host__&lt;/code&gt; 通常可以省略。&lt;/p&gt;
&lt;p&gt;同一个辅助函数也可以同时用 &lt;code&gt;__host__&lt;/code&gt; 和 &lt;code&gt;__device__&lt;/code&gt; 修饰，表示编译器分别生成主机端和设备端版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__host__ __device__ float add_common(float a, float b) {
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;18. nvcc 即时编译&lt;/h2&gt;
&lt;p&gt;即时编译（JIT）指的是：在运行可执行文件时，CUDA Runtime 可以从可执行文件中保留的 PTX 代码临时编译出适配当前 GPU 的 &lt;code&gt;cubin&lt;/code&gt; 代码。&lt;/p&gt;
&lt;p&gt;如果希望在可执行文件中保留 PTX 代码，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-gencode arch=compute_XY,code=compute_XY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这里的两个计算能力都是虚拟架构计算能力。&lt;/li&gt;
&lt;li&gt;两个虚拟架构计算能力必须一致。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;常见写法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-gencode arch=compute_35,code=sm_35
-gencode arch=compute_50,code=sm_50
-gencode arch=compute_61,code=sm_61
-gencode arch=compute_61,code=compute_61
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中前三行生成指定真实架构的二进制代码，最后一行保留 &lt;code&gt;compute_61&lt;/code&gt; 的 PTX，供运行时 JIT 编译。&lt;/p&gt;
&lt;p&gt;简化写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-arch=sm_XY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常可以理解为同时生成当前 &lt;code&gt;sm_XY&lt;/code&gt; 的机器码，并保留对应 &lt;code&gt;compute_XY&lt;/code&gt; 的 PTX，以便运行时进行兼容处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-06.Bz34k4qH_1rdnlp.webp&quot; alt=&quot;Diagram 06&quot;&gt;&lt;/p&gt;
&lt;h2&gt;19. CUDA 事件计时&lt;/h2&gt;
&lt;p&gt;程序执行时间计时是分析 CUDA 程序性能的重要方式。CUDA 中可以使用 event 进行计时。&lt;/p&gt;
&lt;p&gt;基本流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建开始事件和结束事件。&lt;/li&gt;
&lt;li&gt;在待计时代码前记录 &lt;code&gt;start&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;执行待计时代码。&lt;/li&gt;
&lt;li&gt;在待计时代码后记录 &lt;code&gt;stop&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同步 &lt;code&gt;stop&lt;/code&gt;，确保 GPU 端工作完成。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;cudaEventElapsedTime&lt;/code&gt; 计算两个事件之间的时间。&lt;/li&gt;
&lt;li&gt;销毁事件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaEvent_t start, stop;

cudaEventCreate(&amp;#x26;start);
cudaEventCreate(&amp;#x26;stop);

cudaEventRecord(start);

// 待计时的 CUDA 代码，例如 kernel 调用
// kernel_fun&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;(...);

cudaEventRecord(stop);
cudaEventSynchronize(stop);

float elapsed_time = 0.0f;
cudaEventElapsedTime(&amp;#x26;elapsed_time, start, stop);

printf(&quot;Time = %g ms.\n&quot;, elapsed_time);

cudaEventDestroy(start);
cudaEventDestroy(stop);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 kernel 启动通常是异步的，记录结束事件后需要同步，才能得到可靠的 GPU 执行时间。&lt;/p&gt;
&lt;h2&gt;20. CUDA 内存模型&lt;/h2&gt;
&lt;p&gt;CUDA 内存模型中常见的内存类型包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;寄存器（register）&lt;/li&gt;
&lt;li&gt;共享内存（shared memory）&lt;/li&gt;
&lt;li&gt;本地内存（local memory）&lt;/li&gt;
&lt;li&gt;常量内存（constant memory）&lt;/li&gt;
&lt;li&gt;纹理内存（texture memory）&lt;/li&gt;
&lt;li&gt;全局内存（global memory）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;抽象关系如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/diagram-07.CQ2fj8oV_veWzf.webp&quot; alt=&quot;Diagram 07&quot;&gt;
核心理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寄存器和本地内存属于单个线程。&lt;/li&gt;
&lt;li&gt;共享内存属于单个线程块。&lt;/li&gt;
&lt;li&gt;全局内存、常量内存、纹理内存对整个设备上的线程可见。&lt;/li&gt;
&lt;li&gt;主机端需要通过 CUDA API 与设备端全局内存交换数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;21. CUDA 内存的主要特征&lt;/h2&gt;
&lt;p&gt;CUDA 内存可以从四个角度理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;物理位置&lt;/li&gt;
&lt;li&gt;访问权限&lt;/li&gt;
&lt;li&gt;可见范围&lt;/li&gt;
&lt;li&gt;生命周期&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;| 内存类型 | 物理位置 | 访问权限 | 可见范围 | 生命周期 |
|---|---|---|---|---|
| 全局内存 | 芯片外 | 可读可写 | 所有线程和主机端 | 由主机分配与释放 |
| 常量内存 | 芯片外 | 只读 | 所有线程和主机端 | 由主机分配与释放 |
| 纹理和表面内存 | 芯片外 | 一般只读 | 所有线程和主机端 | 由主机分配与释放 |
| 寄存器内存 | 芯片内 | 可读可写 | 单个线程 | 所在线程 |
| 本地内存 | 芯片外 | 可读可写 | 单个线程 | 所在线程 |
| 共享内存 | 芯片内 | 可读可写 | 单个线程块 | 所在线程块 |&lt;/p&gt;
&lt;p&gt;从性能角度看，芯片内资源通常更快，但容量更有限；芯片外内存容量更大，但访问延迟更高。&lt;/p&gt;
&lt;h2&gt;22. 不同 CUDA 内存层次的具体使用方法&lt;/h2&gt;
&lt;p&gt;CUDA 内存层次的选择，本质上是在三个目标之间做平衡：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让访问尽量靠近计算单元。&lt;/li&gt;
&lt;li&gt;让同一份数据被尽量多的线程复用。&lt;/li&gt;
&lt;li&gt;让 warp 中相邻线程的访问尽量规则、连续、合并。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;寄存器&lt;/h3&gt;
&lt;p&gt;寄存器是每个线程私有的最快存储资源。设备端函数中的普通标量局部变量通常会被编译器放入寄存器。&lt;/p&gt;
&lt;p&gt;适合放在寄存器中的数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前线程频繁使用的临时变量。&lt;/li&gt;
&lt;li&gt;循环中的累加器。&lt;/li&gt;
&lt;li&gt;不需要与其他线程共享的小规模标量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void saxpy(float *y, const float *x, float a, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx &amp;#x3C; n) {
        float xi = x[idx];        // 通常进入寄存器
        float yi = y[idx];        // 通常进入寄存器
        y[idx] = a * xi + yi;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用寄存器时要注意寄存器压力。每个线程使用的寄存器越多，同一个 SM 上能同时驻留的线程束可能越少，进而影响 occupancy 和延迟隐藏能力。&lt;/p&gt;
&lt;p&gt;常用检查方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nvcc -Xptxas -v kernel.cu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译输出中可以看到每个 kernel 使用的 register 数量、shared memory 数量和 local memory 数量。&lt;/p&gt;
&lt;h3&gt;本地内存&lt;/h3&gt;
&lt;p&gt;本地内存（local memory）名字里有“本地”，但它通常位于芯片外的设备内存中，只是访问范围属于单个线程。&lt;/p&gt;
&lt;p&gt;本地内存常见来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单个线程里的大数组。&lt;/li&gt;
&lt;li&gt;下标无法在编译期确定的数组访问。&lt;/li&gt;
&lt;li&gt;寄存器不够时发生 register spilling。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void example(float *out) {
    float tmp[128];  // 可能进入 local memory
    int idx = threadIdx.x;
    tmp[idx] = 1.0f;
    out[idx] = tmp[idx];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地内存的使用原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽量避免在单个线程中定义过大的局部数组。&lt;/li&gt;
&lt;li&gt;如果数据会被同一个 block 内多个线程复用，优先考虑共享内存。&lt;/li&gt;
&lt;li&gt;如果只是少量标量，尽量保持为普通局部变量，让编译器放入寄存器。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;nvcc -Xptxas -v&lt;/code&gt; 或 Nsight Compute 观察是否出现较多 local memory 访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;共享内存&lt;/h3&gt;
&lt;p&gt;共享内存位于 SM 上，由同一个 block 内的线程共享，生命周期为该 block 的执行期间。&lt;/p&gt;
&lt;p&gt;适合使用共享内存的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个 block 内多个线程会重复使用同一批全局内存数据。&lt;/li&gt;
&lt;li&gt;需要在线程块内部进行协作，例如规约、扫描、矩阵分块计算。&lt;/li&gt;
&lt;li&gt;需要把非连续的全局内存访问重排为更规则的访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态共享内存声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void kernel(float *out, const float *in) {
    __shared__ float tile[256];

    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + tid;

    tile[tid] = in[idx];
    __syncthreads();

    out[idx] = tile[tid] * 2.0f;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态共享内存声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void kernel(float *out, const float *in) {
    extern __shared__ float tile[];

    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + tid;

    tile[tid] = in[idx];
    __syncthreads();

    out[idx] = tile[tid];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动动态共享内存 kernel 时，第三个配置参数表示每个 block 动态分配的共享内存字节数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int threads = 256;
int shared_bytes = threads * sizeof(float);
kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, threads, shared_bytes&gt;&gt;&gt;(out, in);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;共享内存使用要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写入共享内存后，如果其他线程要读取这些数据，需要使用 &lt;code&gt;__syncthreads()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__syncthreads()&lt;/code&gt; 是 block 级同步，不能同步不同 block。&lt;/li&gt;
&lt;li&gt;不要在部分线程进入、部分线程不进入的分支中随意调用 &lt;code&gt;__syncthreads()&lt;/code&gt;，否则可能死锁。&lt;/li&gt;
&lt;li&gt;共享内存容量有限，使用过多会降低每个 SM 可驻留的 block 数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;共享内存 bank conflict&lt;/h3&gt;
&lt;p&gt;共享内存被分成多个 bank。一个 warp 中多个线程如果访问落在同一个 bank 的不同地址，就可能产生 bank conflict，访问会被串行化。&lt;/p&gt;
&lt;p&gt;常见规避方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让相邻线程访问相邻地址。&lt;/li&gt;
&lt;li&gt;对二维 tile 增加 padding。&lt;/li&gt;
&lt;li&gt;访问转置数据时尤其注意 bank conflict。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;二维 tile padding 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define TILE_DIM 32

__shared__ float tile[TILE_DIM][TILE_DIM + 1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里多出的 &lt;code&gt;+ 1&lt;/code&gt; 可以改变每一行的起始 bank 分布，常用于减少转置访问时的 bank conflict。&lt;/p&gt;
&lt;h3&gt;全局内存&lt;/h3&gt;
&lt;p&gt;全局内存位于芯片外，容量大、生命周期由主机端分配和释放，所有线程都可以访问。它是 GPU 上最常用的数据存储位置，但访问延迟也较高。&lt;/p&gt;
&lt;p&gt;设备端全局内存分配：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *d_x = nullptr;
cudaMalloc((void **)&amp;#x26;d_x, n * sizeof(float));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主机和设备之间传输数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaMemcpy(d_x, h_x, n * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(h_x, d_x, n * sizeof(float), cudaMemcpyDeviceToHost);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放设备内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaFree(d_x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全局内存访问最重要的性能原则是合并访问（coalesced access）。理想情况下，一个 warp 中相邻线程访问连续地址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = blockIdx.x * blockDim.x + threadIdx.x;
float v = x[idx];  // 相邻线程读取相邻元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应尽量避免大跨度访问：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = (blockIdx.x * blockDim.x + threadIdx.x) * stride;
float v = x[idx];  // stride 较大时，访存效率通常较差
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常量内存&lt;/h3&gt;
&lt;p&gt;常量内存适合保存所有线程只读的小型数据，例如模型参数、滤波器系数、查找表和配置常量。&lt;/p&gt;
&lt;p&gt;声明常量内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__constant__ float c_coeff[64];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主机端写入常量内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float h_coeff[64];
cudaMemcpyToSymbol(c_coeff, h_coeff, sizeof(h_coeff));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设备端读取：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void kernel(float *out, const float *in) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    out[idx] = in[idx] * c_coeff[0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常量内存的最佳使用场景是：同一个 warp 中很多线程读取同一个常量地址。此时硬件可以广播读取结果。若 warp 中每个线程读取不同常量地址，效果通常会变差。&lt;/p&gt;
&lt;h3&gt;纹理内存和表面内存&lt;/h3&gt;
&lt;p&gt;纹理内存和表面内存适合图像、二维数组、空间局部性明显或访问模式不完全连续的数据。&lt;/p&gt;
&lt;p&gt;常见用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图像处理中的二维邻域访问。&lt;/li&gt;
&lt;li&gt;访问具有空间局部性的只读数据。&lt;/li&gt;
&lt;li&gt;利用纹理硬件支持的地址模式、过滤模式和缓存行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单的一维数组计算通常优先使用全局内存加合并访问；只有当访问模式、缓存特征或图像语义适合时，再考虑纹理或表面对象。&lt;/p&gt;
&lt;h3&gt;页锁定主机内存&lt;/h3&gt;
&lt;p&gt;普通主机内存是 pageable memory。CUDA 数据传输时，如果主机内存是页锁定内存（pinned memory），通常可以获得更高的主机和设备传输带宽，也可以配合异步拷贝使用。&lt;/p&gt;
&lt;p&gt;分配页锁定主机内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *h_x = nullptr;
cudaMallocHost((void **)&amp;#x26;h_x, n * sizeof(float));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaFreeHost(h_x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异步拷贝示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cudaStream_t stream;
cudaStreamCreate(&amp;#x26;stream);

cudaMemcpyAsync(d_x, h_x, n * sizeof(float),
                cudaMemcpyHostToDevice, stream);

kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size, 0, stream&gt;&gt;&gt;(d_x);

cudaStreamSynchronize(stream);
cudaStreamDestroy(stream);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：页锁定内存会减少操作系统可分页内存，分配过多会影响系统整体性能。一般只对频繁传输的大块缓冲区使用。&lt;/p&gt;
&lt;h3&gt;统一内存&lt;/h3&gt;
&lt;p&gt;统一内存（Unified Memory）用 &lt;code&gt;cudaMallocManaged&lt;/code&gt; 分配，主机和设备可以通过同一个指针访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *x = nullptr;
cudaMallocManaged(&amp;#x26;x, n * sizeof(float));

for (int i = 0; i &amp;#x3C; n; ++i) {
    x[i] = 1.0f;        // Host 访问
}

kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;(x);
cudaDeviceSynchronize();

cudaFree(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;统一内存适合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速原型开发。&lt;/li&gt;
&lt;li&gt;数据结构复杂、手动管理 Host / Device 副本成本较高的场景。&lt;/li&gt;
&lt;li&gt;不想显式写大量 &lt;code&gt;cudaMemcpy&lt;/code&gt; 的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能敏感代码中，建议配合预取使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int device = 0;
cudaMemPrefetchAsync(x, n * sizeof(float), device);
kernel&amp;#x3C;&amp;#x3C;&amp;#x3C;grid_size, block_size&gt;&gt;&gt;(x);
cudaDeviceSynchronize();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;统一内存简化了编程模型，但并不等于没有数据移动成本。页面迁移、缺页和访问位置变化仍然会影响性能。&lt;/p&gt;
&lt;h3&gt;不同内存层次的选择表&lt;/h3&gt;
&lt;p&gt;| 使用场景 | 优先考虑的内存 |
|---|---|
| 单个线程频繁使用的小标量 | 寄存器 |
| 单个线程私有但较大的临时数组 | 尽量避免，必要时为本地内存 |
| 同一 block 内线程共享和复用数据 | 共享内存 |
| 大数组、输入输出张量、跨 block 可见数据 | 全局内存 |
| 所有线程只读且很多线程读同一地址的小表 | 常量内存 |
| 图像、二维空间局部性强的只读访问 | 纹理内存 |
| 图像或二维数据写入，有专门 surface 语义 | 表面内存 |
| 频繁 Host / Device 传输的大缓冲区 | 页锁定主机内存 |
| 需要简化内存管理或快速原型 | 统一内存 |&lt;/p&gt;
&lt;h2&gt;23. CUDA 内存访问最佳实践&lt;/h2&gt;
&lt;h3&gt;优先优化全局内存访问&lt;/h3&gt;
&lt;p&gt;很多 CUDA kernel 的瓶颈不是计算，而是全局内存带宽。优化时应先检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相邻线程是否访问相邻地址。&lt;/li&gt;
&lt;li&gt;是否存在 stride 很大的访问。&lt;/li&gt;
&lt;li&gt;是否存在重复读取但没有复用。&lt;/li&gt;
&lt;li&gt;是否有不必要的 Host / Device 数据传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好的访问模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = blockIdx.x * blockDim.x + threadIdx.x;
y[idx] = x[idx] * 2.0f;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;较差的访问模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int idx = (blockIdx.x * blockDim.x + threadIdx.x) * stride;
y[idx] = x[idx] * 2.0f;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果算法天然需要跨步访问，可以考虑先用共享内存做 tile 缓存和重排。&lt;/p&gt;
&lt;h3&gt;使用共享内存提高数据复用&lt;/h3&gt;
&lt;p&gt;共享内存适合把全局内存中会被多次使用的数据搬到 SM 上。典型例子是矩阵乘法中的 tile：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define TILE 16

__global__ void matmul_tiled(const float *A, const float *B, float *C, int N) {
    __shared__ float As[TILE][TILE];
    __shared__ float Bs[TILE][TILE];

    int tx = threadIdx.x;
    int ty = threadIdx.y;
    int row = blockIdx.y * TILE + ty;
    int col = blockIdx.x * TILE + tx;

    float sum = 0.0f;

    for (int t = 0; t &amp;#x3C; N; t += TILE) {
        As[ty][tx] = A[row * N + (t + tx)];
        Bs[ty][tx] = B[(t + ty) * N + col];
        __syncthreads();

        for (int k = 0; k &amp;#x3C; TILE; ++k) {
            sum += As[ty][k] * Bs[k][tx];
        }
        __syncthreads();
    }

    C[row * N + col] = sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际代码还需要处理 &lt;code&gt;N&lt;/code&gt; 不能整除 &lt;code&gt;TILE&lt;/code&gt; 的边界情况。这里重点是模式：全局内存加载一次，block 内多个线程在共享内存中重复使用。&lt;/p&gt;
&lt;h3&gt;减少 Host 和 Device 之间的数据传输&lt;/h3&gt;
&lt;p&gt;PCIe 或 NVLink 传输通常比 GPU 内部带宽慢。实践中应尽量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把多个小传输合并成大传输。&lt;/li&gt;
&lt;li&gt;数据能留在 GPU 上就不要频繁拷回 CPU。&lt;/li&gt;
&lt;li&gt;使用页锁定主机内存提升传输带宽。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;cudaMemcpyAsync&lt;/code&gt; 和 stream 尝试重叠传输与计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见流程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Host 初始化数据
-&gt; 一次性拷贝到 Device
-&gt; 多个 kernel 在 Device 上连续处理
-&gt; 最后只把必要结果拷回 Host
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;区分缓存、复用和同步&lt;/h3&gt;
&lt;p&gt;共享内存不是越多越好。它真正有价值的前提是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据从全局内存读入后会被复用。&lt;/li&gt;
&lt;li&gt;复用发生在同一个 block 内。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__syncthreads()&lt;/code&gt; 的同步成本小于减少的全局内存访问成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果每个数据只读一次，且访问本来就是合并的，把它搬进共享内存可能没有收益，甚至会变慢。&lt;/p&gt;
&lt;h3&gt;控制寄存器压力&lt;/h3&gt;
&lt;p&gt;过多寄存器会降低 SM 上可同时驻留的 warp 数量。优化时需要在“每个线程更快”和“更多 warp 隐藏延迟”之间平衡。&lt;/p&gt;
&lt;p&gt;常见方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拆分过大的 kernel。&lt;/li&gt;
&lt;li&gt;减少过长的临时变量生命周期。&lt;/li&gt;
&lt;li&gt;避免单线程维护过多中间结果。&lt;/li&gt;
&lt;li&gt;查看编译器报告中的 register 和 local memory 使用量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以辅助使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nvcc -Xptxas -v kernel.cu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必要时也可以尝试 &lt;code&gt;__launch_bounds__&lt;/code&gt; 或 &lt;code&gt;-maxrregcount&lt;/code&gt;，但这类手段可能导致 register spilling，需要结合 profiling 判断。&lt;/p&gt;
&lt;h3&gt;使用 &lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;__restrict__&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;如果输入指针只读，可以声明为 &lt;code&gt;const&lt;/code&gt;。如果能保证多个指针不指向同一片内存，可以使用 &lt;code&gt;__restrict__&lt;/code&gt; 帮助编译器优化访存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;__global__ void add_kernel(const float *__restrict__ a,
                           const float *__restrict__ b,
                           float *__restrict__ c,
                           int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx &amp;#x3C; n) {
        c[idx] = a[idx] + b[idx];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__restrict__&lt;/code&gt; 需要程序员保证别名关系正确。如果实际上传入了重叠内存，结果可能不符合预期。&lt;/p&gt;
&lt;h3&gt;注意二维数据的布局&lt;/h3&gt;
&lt;p&gt;二维数据如果按行主序存储，相邻线程最好沿 &lt;code&gt;x&lt;/code&gt; 方向访问连续元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int idx = y * width + x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于二维数组分配，可以考虑 &lt;code&gt;cudaMallocPitch&lt;/code&gt;，让每一行满足更合适的对齐：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float *d_img = nullptr;
size_t pitch = 0;
cudaMallocPitch((void **)&amp;#x26;d_img, &amp;#x26;pitch,
                width * sizeof(float), height);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问时要用 &lt;code&gt;pitch&lt;/code&gt; 计算行起始地址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;char *row_ptr = (char *)d_img + y * pitch;
float *row = (float *)row_ptr;
float value = row[x];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;先测量再优化&lt;/h3&gt;
&lt;p&gt;内存优化不要只凭直觉。建议使用 profiling 工具观察：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局内存 load / store efficiency。&lt;/li&gt;
&lt;li&gt;是否存在非合并访问。&lt;/li&gt;
&lt;li&gt;shared memory bank conflict。&lt;/li&gt;
&lt;li&gt;register 数量和 occupancy。&lt;/li&gt;
&lt;li&gt;Host / Device memcpy 时间。&lt;/li&gt;
&lt;li&gt;kernel 是否被内存带宽限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nsight Systems：看 CPU、GPU、数据传输和 kernel 时间线。&lt;/li&gt;
&lt;li&gt;Nsight Compute：看单个 kernel 的访存效率、occupancy、warp stall 原因等细节。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内存优化检查清单&lt;/h3&gt;
&lt;p&gt;写 CUDA kernel 时可以按下面顺序检查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;全局内存访问是否合并。&lt;/li&gt;
&lt;li&gt;Host / Device 拷贝是否最少化。&lt;/li&gt;
&lt;li&gt;数据是否能在 GPU 上连续处理，避免频繁拷回 CPU。&lt;/li&gt;
&lt;li&gt;block 内重复使用的数据是否适合放入共享内存。&lt;/li&gt;
&lt;li&gt;共享内存访问是否存在明显 bank conflict。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__syncthreads()&lt;/code&gt; 是否必要，是否所有线程都会到达。&lt;/li&gt;
&lt;li&gt;常量数据是否适合放入 &lt;code&gt;__constant__&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;单线程局部数组是否导致 local memory 访问。&lt;/li&gt;
&lt;li&gt;寄存器数量是否过多，是否影响 occupancy。&lt;/li&gt;
&lt;li&gt;是否用 profiling 结果验证优化确实有效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;24. 延迟隐藏的概念&lt;/h2&gt;
&lt;p&gt;指令延迟指一条指令从发出到完成之间经历的时钟周期数。&lt;/p&gt;
&lt;p&gt;如果每个时钟周期中，所有 warp 调度器都有一个满足条件、可以发射的线程束，那么计算资源就可以保持较高利用率。&lt;/p&gt;
&lt;p&gt;GPU 会通过调度其他线程束的计算来隐藏当前线程束的指令延迟，这种机制称为延迟隐藏。&lt;/p&gt;
&lt;p&gt;典型情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某个 warp 因为等待内存访问或数据依赖而停顿。&lt;/li&gt;
&lt;li&gt;SM 的 warp scheduler 切换到另一个已经准备好的 warp。&lt;/li&gt;
&lt;li&gt;只要可调度 warp 足够多，硬件执行单元就不容易空闲。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指令可以粗略分为两种基本类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;算术指令&lt;/li&gt;
&lt;li&gt;内存指令&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;内存指令通常延迟更高，因此充足的活跃 warp 数量对隐藏内存访问延迟尤其重要。&lt;/p&gt;
&lt;h2&gt;25. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/cuda/cuda-c-programming-guide/&quot;&gt;NVIDIA CUDA C++ Programming Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html&quot;&gt;NVIDIA CUDA C++ Best Practices Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>最优化笔记</title><link>https://iaohr9.github.io/blog/%E6%9C%80%E4%BC%98%E5%8C%96%E7%AC%94%E8%AE%B0</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E6%9C%80%E4%BC%98%E5%8C%96%E7%AC%94%E8%AE%B0</guid><description>最优化课程笔记 PDF 在线阅读。</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import PdfViewer from &apos;@/components/blog/PdfViewer.astro&apos;&lt;/p&gt;
&lt;p&gt;import optimizationNoteUrl from &apos;./optimization-note.pdf?url&apos;&lt;/p&gt;
&lt;h1&gt;最优化笔记&lt;/h1&gt;
&lt;p&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>数学分析（下）</title><link>https://iaohr9.github.io/blog/%E6%95%B0%E5%AD%A6%E5%88%86%E6%9E%90-%E4%B8%8B</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E6%95%B0%E5%AD%A6%E5%88%86%E6%9E%90-%E4%B8%8B</guid><description>数学分析复习笔记</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数学分析（下）&lt;/h1&gt;
&lt;h2&gt;&lt;strong&gt;广义积分&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P46&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;无穷限&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义为定积分的极限&lt;/li&gt;
&lt;li&gt;积分第二中值定理P54&lt;/li&gt;
&lt;li&gt;判断敛散性
&lt;ul&gt;
&lt;li&gt;可以积分的直接积分来观察极限是否存在&lt;/li&gt;
&lt;li&gt;柯西收敛&lt;/li&gt;
&lt;li&gt;比较判别法（记得绝对值）&lt;/li&gt;
&lt;li&gt;与$1/x^p$比较（p&gt;1 收敛，$p \leq 1$发散）&lt;/li&gt;
&lt;li&gt;迪利克雷判别法P57
&lt;ul&gt;
&lt;li&gt;被积函数=$f(x)*g(x)$&lt;/li&gt;
&lt;li&gt;f 积分有界&lt;/li&gt;
&lt;li&gt;g单调趋于零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阿贝尔判别法
&lt;ul&gt;
&lt;li&gt;被积函数=$f(x)*g(x)$&lt;/li&gt;
&lt;li&gt;f收敛&lt;/li&gt;
&lt;li&gt;g单调有界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;瑕积分&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;P61&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判断敛散性
&lt;ul&gt;
&lt;li&gt;柯西收敛&lt;/li&gt;
&lt;li&gt;比较判别法&lt;/li&gt;
&lt;li&gt;与$1/(x-a)^p$比较（$p&amp;#x3C;1$ 收敛，$p \geq 1$发散）&lt;/li&gt;
&lt;li&gt;迪利克雷判别法
&lt;ul&gt;
&lt;li&gt;被积函数=$f(x)*g(x)$&lt;/li&gt;
&lt;li&gt;f 积分有界&lt;/li&gt;
&lt;li&gt;g单调趋于零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阿贝尔判别法
&lt;ul&gt;
&lt;li&gt;被积函数=$f(x)*g(x)$&lt;/li&gt;
&lt;li&gt;f收敛&lt;/li&gt;
&lt;li&gt;g单调有界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;函数项级数&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P70&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数序列
&lt;ul&gt;
&lt;li&gt;一串函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数项级数
&lt;ul&gt;
&lt;li&gt;函数序列对于每个确定x组成的数列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;极限函数
&lt;ul&gt;
&lt;li&gt;$\lim_{n \to \infty} f_n(x) = f(x)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;部分和序列
&lt;ul&gt;
&lt;li&gt;部分和&lt;/li&gt;
&lt;li&gt;是n的函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;和函数
&lt;ul&gt;
&lt;li&gt;部分和序列的极限&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一致收敛
&lt;ul&gt;
&lt;li&gt;级数极限=极限函数在x的取值&lt;/li&gt;
&lt;li&gt;放心地交换极限次序（$x \to x_0$ / $n \to +\infty$）&lt;/li&gt;
&lt;li&gt;放心地交换积分和极限的次序&lt;/li&gt;
&lt;li&gt;放心地交换极限与微商的顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;判断敛散性（部分和的思想）
&lt;ul&gt;
&lt;li&gt;可以积分的直接积分&lt;/li&gt;
&lt;li&gt;达朗贝尔判别法
&lt;ul&gt;
&lt;li&gt;前后两项相除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;柯西收敛&lt;/li&gt;
&lt;li&gt;维尔斯特拉斯判别法（M判别法）
&lt;ul&gt;
&lt;li&gt;$|u_k(x)| \leq M_k$&lt;/li&gt;
&lt;li&gt;$M_k$收敛&lt;/li&gt;
&lt;li&gt;迪利克雷判别法
&lt;ul&gt;
&lt;li&gt;$a_n b_n$&lt;/li&gt;
&lt;li&gt;a一致有界&lt;/li&gt;
&lt;li&gt;b单调趋于零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阿贝尔判别法
&lt;ul&gt;
&lt;li&gt;$a_n b_n$&lt;/li&gt;
&lt;li&gt;a一致收敛&lt;/li&gt;
&lt;li&gt;b一致有界，每个b都单调&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分析性质
&lt;ul&gt;
&lt;li&gt;逐项可积
&lt;ul&gt;
&lt;li&gt;求和和积分可交换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;逐项求导&lt;/li&gt;
&lt;li&gt;做题：求和函数
&lt;ol&gt;
&lt;li&gt;求收敛域2~3‘&lt;/li&gt;
&lt;li&gt;看模板&lt;/li&gt;
&lt;li&gt;有分母先导后积，无分母先积后导&lt;/li&gt;
&lt;li&gt;写出和函数，记得结合实际情况把收敛域写出来&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;幂级数&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P96&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿贝尔引理
&lt;ul&gt;
&lt;li&gt;收敛点之内收敛&lt;/li&gt;
&lt;li&gt;发散点之外发散&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收敛半径唯一&lt;/li&gt;
&lt;li&gt;求收敛半径
&lt;ul&gt;
&lt;li&gt;相邻系数之比的极限
&lt;ul&gt;
&lt;li&gt;达朗贝尔判别法&lt;/li&gt;
&lt;li&gt;求极限确定r&lt;/li&gt;
&lt;li&gt;考虑边界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阿贝尔第二引理P101&lt;/li&gt;
&lt;li&gt;幂级数的和函数S在收敛区域内
&lt;ul&gt;
&lt;li&gt;逐项微商&lt;/li&gt;
&lt;li&gt;逐项积分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数的幂级数展开&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;傅里叶级数&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P114&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三角函数系
&lt;ul&gt;
&lt;li&gt;正交性（任意两个不同函数的乘积在[-\pi, \pi]积分为0）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收敛到傅里叶级数的条件
&lt;ul&gt;
&lt;li&gt;逐段可微&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;计算傅里叶系数(考题基本上都来自这里P142)
&lt;ul&gt;
&lt;li&gt;对称的2\pi区间上的
&lt;ul&gt;
&lt;li&gt;判断奇偶——简化&lt;/li&gt;
&lt;li&gt;$a_0 = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x) , dx$&lt;/li&gt;
&lt;li&gt;$a_n = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x) \cos(nx) , dx$&lt;/li&gt;
&lt;li&gt;$b_n = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x) \sin(nx) , dx$&lt;/li&gt;
&lt;li&gt;$f(x) = a_0 + \sum_{n=1}^{\infty} (a_n \cos(nx) + b_n \sin(nx))$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非对称的
&lt;ul&gt;
&lt;li&gt;周期为2L——变量代换x = \frac{l}{x}t&lt;/li&gt;
&lt;li&gt;记住延拓完之后，要把x代入回原来的f(x)当中&lt;/li&gt;
&lt;li&gt;没有周期
&lt;ul&gt;
&lt;li&gt;奇延拓&lt;/li&gt;
&lt;li&gt;偶延拓&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;平面点集与多元函数&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P158&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;平面点集&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;概念明确
&lt;ul&gt;
&lt;li&gt;内点：存在邻域在E内&lt;/li&gt;
&lt;li&gt;外点：存在邻域不在E内&lt;/li&gt;
&lt;li&gt;边界点：任意邻域都有E的点也有E外的点&lt;/li&gt;
&lt;li&gt;聚点：任意空心邻域有E内的点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关系
&lt;ul&gt;
&lt;li&gt;内点一定是聚点&lt;/li&gt;
&lt;li&gt;边界点可能是聚点，也可能是孤立点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;常见平面点集
&lt;ul&gt;
&lt;li&gt;开集：所有点都是内点&lt;/li&gt;
&lt;li&gt;闭集：所有聚点都属于E&lt;/li&gt;
&lt;li&gt;连通集：任意两点用有线条直线段相连&lt;/li&gt;
&lt;li&gt;（开）区域：连通的开集&lt;/li&gt;
&lt;li&gt;闭区域：连通的闭集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关系
&lt;ul&gt;
&lt;li&gt;区域总是开集，反之不一定&lt;/li&gt;
&lt;li&gt;闭区域总是闭集，反之不一定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;（威尔斯特拉斯）W.T定理
&lt;ul&gt;
&lt;li&gt;如果点列{(P_n)}有界，那必有收敛子列&lt;/li&gt;
&lt;li&gt;证明：连续用两次实数的致密性定理来证明（注意下标）&lt;/li&gt;
&lt;li&gt;$P_n = (x_n, y_n)$ 有界，$x_n, y_n$ 都有界。$x_n$ 有收敛子列 $x_{n_k} \rightarrow x_0$，那么 $y_{n_k}$ 也有界，有收敛子列 $y_{n_{k_l}} \rightarrow y_0$，那么 $x_{n_{k_l}}$ 是 $x_{n_k}$ 的子列，也收敛于 $x_0$。所以有收敛子列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;矩形套定理
&lt;ul&gt;
&lt;li&gt;一直缩小，最后所有的矩形交于一点&lt;/li&gt;
&lt;li&gt;证明：对两个坐标方向分别用区间套定理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有限覆盖定理
&lt;ul&gt;
&lt;li&gt;如果有界闭集F被无限个开区间的集合\sum所覆盖，那么必可从\sum中选取有限个开区间所组成的\sum^*也覆盖F。&lt;/li&gt;
&lt;li&gt;将无限转化为有限&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;二元函数&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;二元函数的极限问题（全面极限）
&lt;ul&gt;
&lt;li&gt;证明的时候要用极限的定义来证明&lt;/li&gt;
&lt;li&gt;可以用三角换元，但是要满足保证在所有方向上趋近于原点（角度要取完）&lt;/li&gt;
&lt;li&gt;证明极限不存在只需要找一个特例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;累次极限
&lt;ul&gt;
&lt;li&gt;全面极限和两个累次极限的&lt;strong&gt;存在性&lt;/strong&gt;并无必然联系&lt;/li&gt;
&lt;li&gt;但当它们存在的时候，极限值有一定的关系
&lt;ul&gt;
&lt;li&gt;如果三个都存在，那么三者必相等&lt;/li&gt;
&lt;li&gt;如果两个累次极限存在但是不相等，那么全面极限必不存在（沿着特殊方向的极限值不同）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二元函数的连续性
&lt;ul&gt;
&lt;li&gt;对于一个二元函数，如果对于任意固定的y是x的一元连续函数，同理，对于任意固定的x是y的一元连续函数，并&lt;strong&gt;不能推出这个二元函数是连续&lt;/strong&gt;的&lt;/li&gt;
&lt;li&gt;讨论连续性的问题
&lt;ul&gt;
&lt;li&gt;对于一般点，用定义，初等函数直接带入极限值&lt;/li&gt;
&lt;li&gt;对于特殊点（没有定义的点），则要计算在该点的极限，然后看看要不要补充定义&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;复合函数的连续性（同时连续才算连续）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有界闭区域上的连续函数
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有界&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用反证法，假设无界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有最大值和最小值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一致连续&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设D是一个有界闭集，f(x,y)是定义在D上的二元连续函数。&lt;/li&gt;
&lt;li&gt;根据有界闭集的性质，我们知道D上的任意点列都有收敛的子列，并且子列的极限点仍然在D&lt;/li&gt;
&lt;li&gt;假设 $f(x,y)$ 在 $D$ 上不一致连续，那么存在 $\epsilon_0 &gt; 0$，以及点列 ${(x_n, y_n)}$ 和 ${(x_n&apos;, y_n&apos;)}$（$n=1,2,3,\ldots$），满足&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$\lim_{n \to \infty} \sqrt{(x_n - x_n&apos;)^2 + (y_n - y_n&apos;)^2} = 0$，但&lt;/p&gt;
&lt;p&gt;$|f(x_n, y_n) - f(x_n&apos;, y_n&apos;)| \geq \epsilon_0。$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于 $D$ 是有界闭集，点列 ${(x_n, y_n)}$ 和 ${(x_n&apos;, y_n&apos;)}$ 都有收敛的子列。设 ${(x_{n_k}, y_{n_k})}$ 和 ${(x_{n_k}&apos;, y_{n_k}&apos;)}$ 是收敛的子列，且它们的极限分别为 $(x_0, y_0)$ 和 $(x_0&apos;, y_0&apos;)$。&lt;/li&gt;
&lt;li&gt;根据极限的性质，我们有&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$\lim_{k \to \infty} \sqrt{(x_{n_k} - x_{n_k}&apos;)^2 + (y_{n_k} - y_{n_k}&apos;)^2} = 0$，即 $(x_0, y_0) = (x_0&apos;, y_0&apos;)$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于f(x,y)在D上连续，根据连续函数的性质，我们有&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$\lim_{k \to \infty} f(x_{n_k}, y_{n_k}) = f(x_0, y_0) = f(x_0&apos;, y_0&apos;) = \lim_{k \to \infty} f(x_{n_k}&apos;, y_{n_k}&apos;)$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;但是这与第三步中的结论 $|f(x_n, y_n) - f(x_n&apos;, y_n&apos;)| \geq \epsilon_0$ 矛盾，因为当 $k$ 足够大时，$|f(x_{n_k}, y_{n_k}) - f(x_{n_k}&apos;, y_{n_k}&apos;)|$ 应该小于 $\epsilon_0$。&lt;/li&gt;
&lt;li&gt;因此，我们的假设——f(x,y)在D上不一致连续——是错误的，所以f(x,y)在D上一致连续。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;介值定理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;偏导数与全微分&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P180&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;偏导数&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;本质：把曲面取一个截面变成曲线，来研究曲线在某个方向的导数&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与一元函数可导就连续不同，二元函数偏导数存在不能完全反映曲面性质&lt;/li&gt;
&lt;li&gt;与一元函数可导即可微不同，二元函数必须偏导数连续，才可微&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;全微分&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;$\Delta z = f(x_0 + \Delta x, y_0 + \Delta y) - f(x_0, y_0) = A \Delta x + B \Delta y + o(\rho)$&lt;/p&gt;
&lt;p&gt;$dz = f_x(x_0, y_0) \Delta x + f_y(x_0, y_0) \Delta y$&lt;/p&gt;
&lt;p&gt;二元函数的全微分是函数全该变量的线性主部（这后面在证明的时候有很大用，看变化量与全微分之差是不是距离的无穷小量）&lt;/p&gt;
&lt;p&gt;dz其实是关于x, y, dx, dy的四元线性函数&lt;/p&gt;
&lt;p&gt;而四者是独立的&lt;/p&gt;
&lt;p&gt;所以在求二阶全微分的时候，有d(dx) = d(dy) = 0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;题型：判断函数在某一点的可微性
&lt;ol&gt;
&lt;li&gt;用定理：偏导数存在且在这一点连续&lt;/li&gt;
&lt;li&gt;用定义：（用定义）先求出偏导数，注意区分x_0, \Delta x，用全变化减去微分，看是不是o(\rho)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;题型：求近似值
&lt;ol&gt;
&lt;li&gt;确定x_0, y_0, \Delta x, \Delta y&lt;/li&gt;
&lt;li&gt;根据微分的定义，写出 $f(x_0 + \Delta x, y_0 + \Delta y) = f(x_0, y_0) + f_x(x_0, y_0) \Delta x + f_y(x_0, y_0) \Delta y$&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;高阶全微分&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;画出树状图&lt;/p&gt;
&lt;p&gt;若f_{xy}与f_{yx}都连续则相等&lt;/p&gt;
&lt;p&gt;$d^n u = \left( \frac{\partial}{\partial x} dx + \frac{\partial}{\partial y} dy \right)^n f(x, y)$&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;复合函数与隐函数微分&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;题型：直接考察求导
&lt;ul&gt;
&lt;li&gt;画出树状图，用&lt;strong&gt;链式法则&lt;/strong&gt;，注意区分乘法的求导法则&lt;/li&gt;
&lt;li&gt;$du = \frac{\partial u}{\partial s} ds + \frac{\partial u}{\partial t} dt = \left( \frac{\partial f}{\partial x} \frac{\partial x}{\partial s} + \frac{\partial f}{\partial y} \frac{\partial y}{\partial s} \right) ds + \left( \frac{\partial f}{\partial x} \frac{\partial x}{\partial t} + \frac{\partial f}{\partial y} \frac{\partial y}{\partial t} \right) dt$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;题型：复合函数
&lt;ul&gt;
&lt;li&gt;例如 $F(xy, y+z, xz) = 0$，求 $\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y}$
&lt;ol&gt;
&lt;li&gt;假设确定隐函数z = z(x, y)&lt;/li&gt;
&lt;li&gt;方程两边对x和y分别求偏导&lt;/li&gt;
&lt;li&gt;联立方程&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;几何应用&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;求切向量法向量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;曲线
&lt;ul&gt;
&lt;li&gt;曲线只有切向量和法平面&lt;/li&gt;
&lt;li&gt;切向量 $\overrightarrow{\gamma} = (x&apos;(t_0), y&apos;(t_0), z&apos;(t_0)) = (A, B, C)$&lt;/li&gt;
&lt;li&gt;如果曲线由两个曲面 $F, G$ 的交线来定义，那么 $\overrightarrow{\gamma} = \left( \frac{\partial(F, G)}{\partial(y, z)}, \frac{\partial(F, G)}{\partial(z, x)}, \frac{\partial(F, G)}{\partial(x, y)} \right)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;曲面
&lt;ul&gt;
&lt;li&gt;曲面只有切平面和法向量&lt;/li&gt;
&lt;li&gt;法向量 $\overrightarrow{n} = (F_x, F_y, F_z) = (A, B, C)$&lt;/li&gt;
&lt;li&gt;特别的，如果z = f(x, y)那么&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;共同的部分
&lt;ul&gt;
&lt;li&gt;由一个向量求所在直线方程和垂直的平面方程&lt;/li&gt;
&lt;li&gt;直线方程（自由度为1，所以要两个方程来限定）：$\frac{x - x_0}{A} = \frac{y - y_0}{B} = \frac{z - z_0}{C}$&lt;/li&gt;
&lt;li&gt;平面方程（自由度为2，所以只需要一个方程）：$A(x - x_0) + B(y - y_0) + C(z - z_0) = 0$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;方向导数&lt;/h3&gt;
&lt;p&gt;沿着 $l$ 方向的方向余弦 $l_0 = (\cos \alpha, \cos \beta, \cos \gamma)$&lt;/p&gt;
&lt;p&gt;梯度 $\nabla = \left( \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}, \frac{\partial f}{\partial z} \right)$&lt;/p&gt;
&lt;p&gt;方向导数就是给梯度赋权，权值就是方向余弦&lt;/p&gt;
&lt;p&gt;所以 $\frac{\partial f}{\partial l} = \nabla \cdot (\cos \alpha, \cos \beta, \cos \gamma)$&lt;/p&gt;
&lt;h3&gt;泰勒展开&lt;/h3&gt;
&lt;p&gt;$f(x_0 + \Delta x, y_0 + \Delta y) = \sum_{k=0}^n \frac{1}{k!} \left( \frac{\partial}{\partial x} dx + \frac{\partial}{\partial y} dy \right)^k f(x_0, y_0) + R$&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;隐函数存在定理&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P225&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;单个方程的形式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果 $F(x, y)$ 在某一点 $P_0(x_0, y_0)$ 附近满足&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$F_x, F_y$ 连续&lt;/li&gt;
&lt;li&gt;$F(P_0) = 0$（通常为初始条件）&lt;/li&gt;
&lt;li&gt;$F_y(P_0) \ne 0$，那么在 $P_0$ 点附近存在唯一的隐函数 $y = f(x)$，且在 $x_0$ 邻域连续，且有连续的导数&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;方程组的形式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;$F(x, y, u, v), G(x, y, u, v)$ 满足&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对各变元有一阶连续偏导,&lt;/li&gt;
&lt;li&gt;$F(P_0) = G(P_0) = 0,$&lt;/li&gt;
&lt;li&gt;$J|_{P_0} = \frac{\partial(F, G)}{\partial(u, v)} \ne 0$，那么在 $P_0$ 附近唯一确定连续的隐函数 $u = u(x, y), v = v(x, y)$，且有连续的导数&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;$\frac{\partial(x, y)}{\partial(u, v)} \cdot \frac{\partial(u, v)}{\partial(x, y)} = 1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;极值与条件极值&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P241&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;多元函数的极值&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;稳定点——所有偏导数都为0&lt;/li&gt;
&lt;li&gt;极值的&lt;strong&gt;必要条件&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;所有偏导数都为0&lt;/li&gt;
&lt;li&gt;注意不是充分条件（z = xy在(0, 0)偏导数都为0，但是在该点附近都有异号的函数值）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;D的定义：
&lt;ul&gt;
&lt;li&gt;$a_{11} = f_{xx}(x_0, y_0), \space a_{12} = f_{xy}(x_0, y_0), \space a_{22} = f_{yy}(x_0, y_0), \space \ D = \left| \begin{matrix} a_{11} &amp;#x26; a_{12} \ a_{12} &amp;#x26; a_{22} \end{matrix} \right|$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;极值的判别（考察）
&lt;ul&gt;
&lt;li&gt;（在该点的领域内有二阶连续偏导数）$f_x(x_0, y_0) = f_y(x_0, y_0) = 0$
&lt;ul&gt;
&lt;li&gt;若 $D &gt; 0$，当 $a_{11}(a_{22}) &gt; 0$ 取得极小值；当 $a_{11}(a_{22}) &amp;#x3C; 0$ 取得极大值。&lt;/li&gt;
&lt;li&gt;若D &amp;#x3C; 0 不是极值点&lt;/li&gt;
&lt;li&gt;若D = 0 不能判断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;证明：用泰勒公式&lt;/li&gt;
&lt;li&gt;做题：
&lt;ol&gt;
&lt;li&gt;求一阶偏导，令偏导为0得到稳定点（注意在求解方程的时候，可能会得到两个未知数之间的关系，这个时候一定要带回方程里完全解出来）&lt;/li&gt;
&lt;li&gt;求二阶偏导，算D&lt;/li&gt;
&lt;li&gt;看$a_{11}$的符号&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;最小二乘法&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;本质上就是给了一个误差函数，求这个误差函数的极小值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f(a, b) = \sum_{i=1}^{n} (ax_i + b - y_i)^2$
&lt;ol&gt;
&lt;li&gt;对他求一阶导，令一阶导数为0，得到方程组&lt;/li&gt;
&lt;li&gt;用克拉默法则解方程（注意a和b才是未知数）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;多元函数的最值&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;一句话：可能的最值点包括可能的极值点和边界点&lt;/p&gt;
&lt;p&gt;所以先求出极值点，以及边界值，然后做对比即可&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;条件极值&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;什么叫条件极值
&lt;ul&gt;
&lt;li&gt;在整个空间里的叫做无条件极值&lt;/li&gt;
&lt;li&gt;有方程约束范围的叫做条件极值
&lt;ul&gt;
&lt;li&gt;每个（方程）条件就相当于一个隐函数，可以带入消元&lt;/li&gt;
&lt;li&gt;思想：化为无条件极值求解&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;拉格朗日乘数法&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;对于上面提到的条件极值&lt;/p&gt;
&lt;p&gt;假设 $f(x, y, u, v)$ 在约束条件&lt;/p&gt;
&lt;p&gt;$$
\begin{cases} F(x, y, u, v) = 0,\ G(x, y, u, v) = 0\ (F, G \text{ 不重}) \end{cases}
$$&lt;/p&gt;
&lt;p&gt;下在 $P_0(x_0, y_0, u_0, v_0)$ 点取极值，那么存在唯一的 $\lambda_1, \lambda_2$，使得&lt;/p&gt;
&lt;p&gt;$$
\begin{cases} L_x(P_0) = L_y(P_0) = L_u(P_0) = L_v(P_0) = 0,\ F(P_0) = G(P_0) = 0 \end{cases}
$$&lt;/p&gt;
&lt;p&gt;其中 $L = f + \lambda_1 F + \lambda_2 G$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;做题
&lt;ol&gt;
&lt;li&gt;写出拉格朗日函数&lt;/li&gt;
&lt;li&gt;求导，令导函数为0&lt;/li&gt;
&lt;li&gt;解方程得到稳定点&lt;/li&gt;
&lt;li&gt;通过极值的判断条件来判断
&lt;ol&gt;
&lt;li&gt;求二阶导数（求之前要用隐函数存在定理确定导数存在）&lt;/li&gt;
&lt;li&gt;D&lt;/li&gt;
&lt;li&gt;$a_{11}$&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;含参变量的积分&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P270&lt;/p&gt;
&lt;h3&gt;一般情况&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;首先我们要理解什么叫含参变量的积分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一个 $[a, b]$ 上固定的 $x_0$，对应的 $\int_c^d f(x_0, y) dy$ 都是一个【数】；当 $x$ 变动的时候，就定义了一个【函数】&lt;/p&gt;
&lt;p&gt;$$I(x) = \int_c^d f(x, y) dy, \quad x \in [a, b]$$&lt;/p&gt;
&lt;p&gt;参变量为 $x$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性质
&lt;ul&gt;
&lt;li&gt;f 连续，I 就连续&lt;/li&gt;
&lt;li&gt;放心交换积分和极限&lt;/li&gt;
&lt;li&gt;只要偏导数连续就放心求导I&apos;(x) = \int_c^d f_x(x, y) dy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;题型：给你一个含参变量的函数，要求把其他变量消掉
&lt;ul&gt;
&lt;li&gt;大胆对内求导&lt;/li&gt;
&lt;li&gt;把关于y的积分算出来（消掉y）&lt;/li&gt;
&lt;li&gt;求x的积分&lt;/li&gt;
&lt;li&gt;确定常数C&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;题型：要求主动引入参变量的积分（压轴题）
&lt;ul&gt;
&lt;li&gt;引入参变量\alpha \in [0, 1]，原来的I = I(1)，最好保证I(0) = 0（有利于后面构造积分）&lt;/li&gt;
&lt;li&gt;对内求导数&lt;/li&gt;
&lt;li&gt;把关于x的积分算出来&lt;/li&gt;
&lt;li&gt;$I = I(1) = I(1) - I(0) = \int_0^1 I&apos;(\alpha) d\alpha$&lt;/li&gt;
&lt;li&gt;求出积分即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;积分上下限也依赖于参数x P265&lt;/h3&gt;
&lt;p&gt;$I(x, u) = \int_c^u f(x, y) dy$&lt;/p&gt;
&lt;p&gt;请尤其关注这个式子&lt;/p&gt;
&lt;p&gt;$$\frac{d \left(\int_c^x f(t) dt\right)}{dx} = f(x)$$&lt;/p&gt;
&lt;p&gt;变上限积分的导数为原函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;f连续则I连续，放心求导，导函数存在且连续&lt;/li&gt;
&lt;li&gt;不用担心连续问题：&lt;/li&gt;
&lt;li&gt;$f$ 在 $[a, b] \times [c, d]$ 连续，且 $c(x), d(x)$ 也在 $[a, b]$ 连续，有导数，$c &amp;#x3C; c(x), d(x) &amp;#x3C; d$，则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$F(x) = \int_{c(x)}^{d(x)} f(x, y) dy$$&lt;/p&gt;
&lt;p&gt;连续，且&lt;/p&gt;
&lt;p&gt;$$F&apos;(x) = \int_{c(x)}^{d(x)} f_x(x, y) dy + f(x, d(x)) d&apos;(x) - f(x, c(x)) c&apos;(x)$$&lt;/p&gt;
&lt;p&gt;（链式法则：分别对 $x$，$c$，$d$ 求导 + 复合函数求导）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;做题：求正常求不出来的积分的导数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性质：放心积分交换次序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\int_a^b dx \int_c^d f(x, y) dy = \int_c^d dy \int_a^b f(x, y) dx$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;重积分&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;P292&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;三重积分交换次序&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对于 $\int_a^b dx \int_c^d dy \int_e^f f(x, y, z) dz$ 的两种理解
&lt;ul&gt;
&lt;li&gt;$\int_a^b \left[\iint_{D_z} dx dy\right] dz$，即先对特定 $z$ 求切片面积，再对 $z$ 求积分&lt;/li&gt;
&lt;li&gt;$\left[\iint_{D} dx dy\right] \int_{\psi(x, y)}^{\varphi(x, y)} f(x, y, z) dz$，即在 $xOy$ 平面投影，作垂线，两个交点，做差，再对投影面积积分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不需要在意原理，只需知道两两可以交换次序（变成二重积分）
&lt;ul&gt;
&lt;li&gt;$(\int_a^b dx \int_c^d dy) \int_e^f dz$&lt;/li&gt;
&lt;li&gt;$\int_a^b dx (\int_c^d dy \int_e^f dz)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;三重积分的换元&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;关键是雅可比行列式 $\frac{\partial(x, y)}{\partial(u, v)}$&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;曲线与曲面积分&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;第一型曲线积分&lt;/h3&gt;
&lt;p&gt;可以理解成线密度质量的模型&lt;/p&gt;
&lt;p&gt;由于微分转化的时候用的是平方，无方向&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;做计算题，一般给出的形式为 $\int_L f(x, y, z) ds$
&lt;ul&gt;
&lt;li&gt;用参数来表示&lt;/li&gt;
&lt;li&gt;$ds = \sqrt{x&apos;^2(t) + y&apos;^2(t) + z&apos;^2(t)} dt$&lt;/li&gt;
&lt;li&gt;转化为普通定积分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二型曲线积分&lt;/h3&gt;
&lt;p&gt;可以理解成变力做功的模型&lt;/p&gt;
&lt;p&gt;dx/dy/dz给定了方向&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;做计算题，一般给出的形式为 $\int_{L_{ab}} (x^2 + y^2) dx + 4xy dy$
&lt;ul&gt;
&lt;li&gt;寻找参数（可以是极坐标也可以是曲线方程）&lt;/li&gt;
&lt;li&gt;对参数求导，转化微分&lt;/li&gt;
&lt;li&gt;带入，转化为普通定积分求解&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第一型曲面积分&lt;/h3&gt;
&lt;p&gt;最显著的特征在于，积分区域变了，原来是XoY平面，现在变成了某一曲面&lt;/p&gt;
&lt;p&gt;可以将函数看作是曲面的面密度（与方向没有任何关系）&lt;/p&gt;
&lt;p&gt;思想：将曲面投影到XoY平面，曲面上的一点唯一对应XoY上的一点，面积之比为这个点的梯度。从而转化为熟知的二重积分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;做计算题，一般给出的形式为 $\iint_S xyz ds$
&lt;ul&gt;
&lt;li&gt;$\iint_S f(x, y, z) ds = \iint_D f(x, y, z(x, y)) \sqrt{1 + z_x^2(x, y) + z_y^2(x, y)} dx dy$&lt;/li&gt;
&lt;li&gt;转化为重积分解决&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果用参数表示，只需要
&lt;ul&gt;
&lt;li&gt;$r_u = \left( \frac{\partial x}{\partial u}, \frac{\partial y}{\partial u}, \frac{\partial z}{\partial u} \right) \ r_v = \left( \frac{\partial x}{\partial v}, \frac{\partial y}{\partial v}, \frac{\partial z}{\partial v} \right)$&lt;/li&gt;
&lt;li&gt;$E = r_u \cdot r_u, F = r_u \cdot r_v, G = r_v \cdot r_v$&lt;/li&gt;
&lt;li&gt;$ds = \sqrt{EG - F^2} dx dy$&lt;/li&gt;
&lt;li&gt;转化为重积分解决&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特别地，如果三维的积分区域退化到二维，偏导数项为0，$\iint_S f(x, y, z) ds = \iint_D f(x, y, z(x, y)) dx dy$，也就是一个普通的二重积分&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二型曲面积分&lt;/h3&gt;
&lt;p&gt;本质上是通过法向量 $\overrightarrow{n} = (\cos \alpha, \cos \beta, \cos \gamma)$ 来链接，$\overrightarrow{n} \cdot d\overrightarrow{s} = (dy dz, dz dx, dx dy)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;做计算题，一般给出的形式为 $I = \iint_S x dy dz + y dz dx + z dx dy$
&lt;ul&gt;
&lt;li&gt;看有无对称性&lt;/li&gt;
&lt;li&gt;投影到对应平面&lt;/li&gt;
&lt;li&gt;消元（用已知量或者方程带入）&lt;/li&gt;
&lt;li&gt;转化为重积分解决&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;各种积分之间的联系&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;格林公式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;由逐段光滑的曲线围成的单连通区域，$P$、$Q$ 有一阶连续偏导数&lt;/p&gt;
&lt;p&gt;$$\iint_D \left( \frac{\partial Q}{\partial x} - \frac{\partial P}{\partial y} \right) dx dy = \oint_L P dx + Q dy$$&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;补充&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;补充克拉默法则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个二元一次方程组：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$a_1 x + b_1 y = c_1$$
$$a_2 x + b_2 y = c_2$$&lt;/p&gt;
&lt;p&gt;其中 $a_1$、$b_1$、$c_1$、$a_2$、$b_2$、$c_2$ 都是已知的常数，而 $x$ 和 $y$ 是未知数。根据克拉默法则，方程组的解可以通过以下公式来表示：&lt;/p&gt;
&lt;p&gt;$$x = \frac{D_1}{D}, \quad y = \frac{D_2}{D}$$&lt;/p&gt;
&lt;p&gt;其中 $D$ 是方程组的系数行列式，$D_1$ 是将方程组的常数列替换掉 $x$ 的系数列所得到的行列式，$D_2$ 是将方程组的常数列替换掉 $y$ 的系数列所得到的行列式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理$\int \sec^3 x dx$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们用分部积分，可以实现降次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$\int \sec^3 x dx = \int \sec x , d(\tan x) = \sec x \tan x - \int \tan x , d(\sec x)$$&lt;/p&gt;
&lt;p&gt;而&lt;/p&gt;
&lt;p&gt;$$\int \tan x , d(\sec x) = \int \tan^2 x \sec x , dx = \int (\sec^2 x - 1) \sec x , dx = \int (\sec^3 x - \sec x) dx$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接下来移项即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;期末考试押题&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2*广义积分
&lt;ul&gt;
&lt;li&gt;迪利克雷/Abel&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数项级数的收敛域
&lt;ul&gt;
&lt;li&gt;根值法&lt;/li&gt;
&lt;li&gt;达朗贝尔&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;幂级数的和函数（展开）
&lt;ol&gt;
&lt;li&gt;收敛域&lt;/li&gt;
&lt;li&gt;看模板&lt;/li&gt;
&lt;li&gt;有分母微分，无分母积分&lt;/li&gt;
&lt;li&gt;结合实际情况写出收敛域（否则扣分&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;傅里叶级数的展开
&lt;ul&gt;
&lt;li&gt;延拓&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;求偏导数
&lt;ul&gt;
&lt;li&gt;链式法则&lt;/li&gt;
&lt;li&gt;方程组&lt;/li&gt;
&lt;li&gt;隐函数求解法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;极值与条件极值
&lt;ul&gt;
&lt;li&gt;拉格朗日乘数法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二元的微分中值定理&lt;/li&gt;
&lt;li&gt;重积分&lt;/li&gt;
&lt;li&gt;第一、二型曲线积分
&lt;ul&gt;
&lt;li&gt;参数化表示&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;格林公式
&lt;ul&gt;
&lt;li&gt;积分与路径无关&lt;/li&gt;
&lt;li&gt;求原函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第一型曲面积分&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>GDB常用指令</title><link>https://iaohr9.github.io/blog/gdb%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4</link><guid isPermaLink="true">https://iaohr9.github.io/blog/gdb%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4</guid><description>GDB（GNU Debugger）调试命令大全，涵盖断点设置、单步执行、变量查看等常用操作。</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GDB（GNU Debugger）提供了丰富的调试命令，涵盖代码运行、断点管理、变量查看、堆栈分析等多个方面。以下是 GDB 的所有主要调试命令，按类别分类：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. &lt;strong&gt;启动和退出&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;| 命令                             | 说明                |
| ------------------------------ | ----------------- |
| &lt;code&gt;gdb program&lt;/code&gt;                  | 调试可执行文件 &lt;code&gt;program&lt;/code&gt; |
| &lt;code&gt;gdb --args program arg1 arg2&lt;/code&gt; | 传递参数给 &lt;code&gt;program&lt;/code&gt;   |
| &lt;code&gt;gdb -p PID&lt;/code&gt;                   | 附加到进程 PID         |
| &lt;code&gt;q&lt;/code&gt; / &lt;code&gt;quit&lt;/code&gt;                   | 退出 GDB            |&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. &lt;strong&gt;运行程序&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;r&lt;/code&gt; / &lt;code&gt;run [args]&lt;/code&gt;|运行程序，可传递参数|
|&lt;code&gt;start&lt;/code&gt;|启动程序并停在 &lt;code&gt;main()&lt;/code&gt; 之前|
|&lt;code&gt;kill&lt;/code&gt;|终止正在调试的程序|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. &lt;strong&gt;断点（Breakpoints）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;b&lt;/code&gt; / &lt;code&gt;break func_name&lt;/code&gt;|在函数 &lt;code&gt;func_name&lt;/code&gt; 处设置断点|
|&lt;code&gt;b line_number&lt;/code&gt;|在当前源文件的 &lt;code&gt;line_number&lt;/code&gt; 处设置断点|
|&lt;code&gt;b file:line&lt;/code&gt;|在指定文件 &lt;code&gt;file&lt;/code&gt; 的 &lt;code&gt;line&lt;/code&gt; 行处设置断点|
|&lt;code&gt;b *address&lt;/code&gt;|在内存地址 &lt;code&gt;address&lt;/code&gt; 处设置断点|
|&lt;code&gt;info breakpoints&lt;/code&gt;|显示所有断点|
|&lt;code&gt;delete [num]&lt;/code&gt;|删除编号为 &lt;code&gt;num&lt;/code&gt; 的断点（默认删除所有断点）|
|&lt;code&gt;clear func_name&lt;/code&gt;|清除函数 &lt;code&gt;func_name&lt;/code&gt; 的断点|
|&lt;code&gt;clear file:line&lt;/code&gt;|清除 &lt;code&gt;file&lt;/code&gt; 文件 &lt;code&gt;line&lt;/code&gt; 行的断点|
|&lt;code&gt;enable [num]&lt;/code&gt;|启用编号为 &lt;code&gt;num&lt;/code&gt; 的断点|
|&lt;code&gt;disable [num]&lt;/code&gt;|禁用编号为 &lt;code&gt;num&lt;/code&gt; 的断点|
|&lt;code&gt;condition num expr&lt;/code&gt;|仅当 &lt;code&gt;expr&lt;/code&gt; 为真时，触发编号为 &lt;code&gt;num&lt;/code&gt; 的断点|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. &lt;strong&gt;观察点（Watchpoints）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;watch var&lt;/code&gt;|监视变量 &lt;code&gt;var&lt;/code&gt; 变化时暂停|
|&lt;code&gt;rwatch var&lt;/code&gt;|变量 &lt;code&gt;var&lt;/code&gt; 被读取时暂停|
|&lt;code&gt;awatch var&lt;/code&gt;|变量 &lt;code&gt;var&lt;/code&gt; 被读/写时暂停|
|&lt;code&gt;info watchpoints&lt;/code&gt;|显示所有监视点|
|&lt;code&gt;delete watch num&lt;/code&gt;|删除编号为 &lt;code&gt;num&lt;/code&gt; 的监视点|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. &lt;strong&gt;单步执行（Stepping）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;s&lt;/code&gt; / &lt;code&gt;step&lt;/code&gt;|单步执行代码（进入函数内部）|
|&lt;code&gt;n&lt;/code&gt; / &lt;code&gt;next&lt;/code&gt;|单步执行代码（不进入函数内部）|
|&lt;code&gt;finish&lt;/code&gt;|运行到当前函数返回|
|&lt;code&gt;until&lt;/code&gt;|运行到当前代码块结束|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. &lt;strong&gt;继续执行&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;c&lt;/code&gt; / &lt;code&gt;continue&lt;/code&gt;|继续执行到下一个断点|
|&lt;code&gt;jump line&lt;/code&gt;|跳转到 &lt;code&gt;line&lt;/code&gt; 行执行|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. &lt;strong&gt;堆栈（Stack）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;bt&lt;/code&gt; / &lt;code&gt;backtrace&lt;/code&gt;|显示当前调用栈|
|&lt;code&gt;bt full&lt;/code&gt;|显示完整的调用栈（包含参数和变量）|
|&lt;code&gt;frame num&lt;/code&gt;|切换到编号 &lt;code&gt;num&lt;/code&gt; 的栈帧|
|&lt;code&gt;up&lt;/code&gt;|向上移动一层栈帧|
|&lt;code&gt;down&lt;/code&gt;|向下移动一层栈帧|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. &lt;strong&gt;打印变量&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;p&lt;/code&gt; / &lt;code&gt;print expr&lt;/code&gt;|打印表达式 &lt;code&gt;expr&lt;/code&gt; 的值|
|&lt;code&gt;ptype var&lt;/code&gt;|显示变量 &lt;code&gt;var&lt;/code&gt; 的类型|
|&lt;code&gt;whatis var&lt;/code&gt;|显示变量 &lt;code&gt;var&lt;/code&gt; 的类型|
|&lt;code&gt;x addr&lt;/code&gt;|以十六进制格式查看 &lt;code&gt;addr&lt;/code&gt; 处的内存|
|&lt;code&gt;x/4x addr&lt;/code&gt;|以十六进制显示 &lt;code&gt;addr&lt;/code&gt; 处的 4 个单位数据|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. &lt;strong&gt;寄存器&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;info registers&lt;/code&gt;|显示所有寄存器的值|
|&lt;code&gt;p $eax&lt;/code&gt;|打印寄存器 &lt;code&gt;eax&lt;/code&gt; 的值|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. &lt;strong&gt;信号（Signals）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;handle SIGSEGV stop&lt;/code&gt;|在 &lt;code&gt;SIGSEGV&lt;/code&gt; 信号触发时暂停|
|&lt;code&gt;handle SIGINT nostop&lt;/code&gt;|忽略 &lt;code&gt;SIGINT&lt;/code&gt; 信号|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. &lt;strong&gt;调试共享库&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;info sharedlibrary&lt;/code&gt;|显示加载的共享库|
|&lt;code&gt;set solib-search-path path&lt;/code&gt;|设置共享库搜索路径|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. &lt;strong&gt;修改变量&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;set var = value&lt;/code&gt;|修改变量值|
|&lt;code&gt;set $eax = 0x1234&lt;/code&gt;|修改寄存器值|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. &lt;strong&gt;附加进程&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;attach PID&lt;/code&gt;|附加到进程 &lt;code&gt;PID&lt;/code&gt;|
|&lt;code&gt;detach&lt;/code&gt;|断开调试进程|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;14. &lt;strong&gt;线程调试&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;info threads&lt;/code&gt;|显示所有线程|
|&lt;code&gt;thread num&lt;/code&gt;|切换到 &lt;code&gt;num&lt;/code&gt; 号线程|
|&lt;code&gt;thread apply all cmd&lt;/code&gt;|对所有线程执行 &lt;code&gt;cmd&lt;/code&gt;|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;15. &lt;strong&gt;脚本&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;source file&lt;/code&gt;|运行 &lt;code&gt;file&lt;/code&gt; 中的 GDB 命令|
|&lt;code&gt;define cmd&lt;/code&gt;|自定义命令|
|&lt;code&gt;document cmd&lt;/code&gt;|添加自定义命令的文档|
|&lt;code&gt;save breakpoints file&lt;/code&gt;|将断点信息保存到 &lt;code&gt;file&lt;/code&gt;|&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;16. &lt;strong&gt;GDB 调试 TUI 模式&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;|命令|说明|
|---|---|
|&lt;code&gt;layout src&lt;/code&gt;|显示源码窗口|
|&lt;code&gt;layout asm&lt;/code&gt;|显示汇编窗口|
|&lt;code&gt;layout regs&lt;/code&gt;|显示寄存器窗口|
|&lt;code&gt;tui enable&lt;/code&gt;|启用 TUI 模式|
|&lt;code&gt;tui disable&lt;/code&gt;|禁用 TUI 模式|&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>离散数学</title><link>https://iaohr9.github.io/blog/%E7%A6%BB%E6%95%A3%E6%95%B0%E5%AD%A6</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E7%A6%BB%E6%95%A3%E6%95%B0%E5%AD%A6</guid><description>中山大学周晓聪老师离散数学课堂笔记</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;目录&lt;/h1&gt;
&lt;h1&gt;第一章：略&lt;/h1&gt;
&lt;h1&gt;第二章：命题逻辑&lt;/h1&gt;
&lt;h2&gt;自然语言符号化&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image.CpUpyYUF_wUaeb.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;什么叫逆命题、否命题和逆否命题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_1.trfA3CAX_2ckQoE.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;基本等值式&lt;/h2&gt;
&lt;p&gt;什么叫做等值式&lt;/p&gt;
&lt;p&gt;称命题逻辑公式A和B逻辑等值(logically equivalent)，简称等值，记为A=B，如果&lt;br&gt;
对任意真值赋值函数$\sigma:Var→2$，A和B在下的真值都相同，即都有$\sigma(A）=\sigma(B)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如何证明呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;证明方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_2.CEy4avWt_21cL73.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非形式化&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_3.DzlXUk10_DfeHc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模式与实例&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_4.DhwVHRAE_1Yp8jX.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_5.G7O3Lgql_ZJVpIT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_6.BIWI5H-w_ID2u6.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;题型：
&lt;ul&gt;
&lt;li&gt;判断命题逻辑公式类型（永真式、矛盾式、可满足式）
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;利用等值演算来证明逻辑等值式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_7.CXncDHga_1zB1BD.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;析取、合取范式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_8.B8qPUk-k_1PnNbb.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;==单个的文字可以看做是一个合取式或一个析取式，单个的合取式可看做是析取范式，单个的析取式也可看做是合取范式==&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;易错！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_9.DP6U66gY_2nj9KS.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：上例子只给出了一个或两个的合、析取范式，但实际上也可以有多个，不要没见过大蛇o屎&lt;/p&gt;
&lt;p&gt;范式中==只出现与、或、非运算，且否定只出现在命题变量的前面==&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;题型：给出一个公式，让你求合取、析取范式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求与公式逻辑等值的析取范式&lt;br&gt;
先通过蕴涵等值式和双蕴涵等值式转换为不含→和↔的公式&lt;br&gt;
然后用德摩尔根律将所有否定运算符移到命题变量的前面&lt;br&gt;
最后用分配律将合取运算符放到括号里的文字间，析取运算符放到括号外的合取式之间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_10.nECxO9QS_Z6U2gO.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_11.Bhm9sHHl_ZG1ejY.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;极小项、极大项&lt;/h2&gt;
&lt;p&gt;合取会变小，析取会变大&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_12.BFdDl3wH_TjQTB.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;超级易错点：极大项是成假赋值&lt;/p&gt;
&lt;p&gt;也就是说，命名的下标是通过成真和成假赋值的二进制值来确定的&lt;/p&gt;
&lt;p&gt;只是我们在写出对应最大项公式的时候，要反过来&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_13.9kTuyNiZ_1Kg45I.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_14.Cup9Q-g__qtAPj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;题型
&lt;ul&gt;
&lt;li&gt;给一个公式求主合取范式和主析取范式&lt;/li&gt;
&lt;li&gt;方法：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;真值表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;画出真值表，1的就是$m_i$，0的就是$M_i$&lt;/li&gt;
&lt;li&gt;如果要写出具体的字母，对m成真赋值，对M成假赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等值演算法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先求出主析取范式，遇到不是的，拓展（与一个1，然后用排中律，再用分配律）&lt;/li&gt;
&lt;li&gt;然后成真赋值&lt;/li&gt;
&lt;li&gt;得到主析取范式（m表示）&lt;/li&gt;
&lt;li&gt;不是最小项的就是极大项&lt;/li&gt;
&lt;li&gt;例子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_15.DYFZVnmS_1IXajf.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_16.DQt0ScZv_Z1tLJQV.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;推理系统&lt;/h2&gt;
&lt;p&gt;首先需要注意的是，推理有效与否与公式真假没有关系&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_17.DNodDvU7_ZvojXT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;题型一：形式化证明&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_18.DEoYnlPP_Z1yBI9Q.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;附加前提法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_19.Nm74Lhuf_Z1zA2rT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;归谬法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_20.DsXv4ZHC_Z2mLPgB.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;题型二：自然语言的证明&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_21.Q35nQ-2q_1b422S.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;易错点：&lt;/h2&gt;
&lt;h3&gt;置换和替换&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_22.CmMNWoOR_ZP4brm.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_23.Dds-Hpwk_Z1Svxu1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;替换就是完全替代，所以要全部换掉。置换弱一点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_24.FRg3ZKn4_ZNjK2E.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h1&gt;第三章：一阶逻辑&lt;/h1&gt;
&lt;h2&gt;谓词逻辑命题符号化&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_25.C0gyJhaD_sxBUi.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;全称量词和存在量词&lt;/h2&gt;
&lt;p&gt;易错点！！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_26.Be654mgT_24Aq41.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;谓词逻辑命题符号化&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_27.BOqBwqJ1_1USLU9.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;谓词逻辑公式及其解释&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_28.CcAggoRI_34B2E.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;更多的，只要自由出现了一次，就是自由出现的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_29.C2rBlO17_AAM2m.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;改名&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;约束变量改名&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_30.CxLMOVec_Z1iAfDo.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意：不能换约束出现的，如H中的x&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自由变量改名&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_31.Ds_cBw5j_8QdDw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;量词消去和真值&lt;/h3&gt;
&lt;p&gt;求公式在一定解释下的真值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_32.D9ovkCox_ZQncp1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_33.B29ld39t_gwSe2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;一阶逻辑的等值演算&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_34.QXkMnKop_27xzfH.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_35.BKmbW8rm_aTByr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;前束范式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_36.CUVL_0Ku_1bfF5k.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;替换实例&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_37.BTEyQf_Q_ZALTvH.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为什么单独放出来呢，是因为我觉得：&lt;/p&gt;
&lt;p&gt;他其实就是告诉你&lt;br&gt;
命题逻辑公式那些等值式&lt;br&gt;
全都可以替换&lt;br&gt;
也就是告诉你，全都可以继续用&lt;br&gt;
从而把第二章一下子联系起来了&lt;/p&gt;
&lt;h2&gt;推理系统&lt;/h2&gt;
&lt;p&gt;推理系统和命题逻辑的类似&lt;/p&gt;
&lt;h3&gt;量词引入和消除&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_38.CAaIU1wI_Z1cB9lm.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;易错点！！！&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;量词例化规则只能针对辖域是整个公式的量词使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_39.DI0Q8KXY_2h08lt.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;必须先使用存在例化规则引入个体常量c，然后使用全称例化规则时可使用个体常量c&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_40.8-touxtg_1WwiDr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h1&gt;第五章：集合&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;181定义集合的方法&lt;/li&gt;
&lt;li&gt;183重要的 定义集合 的定义&lt;/li&gt;
&lt;li&gt;187交的定义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_41.D6IPBeaY_svyMv.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_42.Bt74dpln_Z2pEy4T.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集合并保持子集关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_43.BJf8bdPi_Z278Aa8.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_44.B1u1_mKd_Z1kFHA.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;192差和补&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_45.CqWXxNFX_2tEoQI.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_46.DQxqei_T_Z1LtMSt.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一些定理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_47.W2h5c0qn_PFy8.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;集合运算及其元素定义法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_48.CgBoRf_5_Z1IiuOv.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;幂集&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;195空集的幂集相关的易错点、保持子集关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_49.BLR9qX6d_1t9KTq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_50.6od30alI_Z1KWUnm.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_51.DSuIX6qg_Z2trxuL.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;证明的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过定义证明&lt;/li&gt;
&lt;li&gt;通过集合等式&lt;/li&gt;
&lt;li&gt;子集的包含关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_52.BVmtUOoe_1juB6M.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集合的容斥原理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_53.V8XLfyO9_Z5efK2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;==基本集合等式表==&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_54.B0xLw-Dr_ZMzl0M.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;记住以下这些可以加快选择题做题的速度&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_55.DmFnouKq_2aERN6.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;证明题的时候，优先从有减号的一边入手，将减号化成交&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;==203集合运算与子集关系==&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第六章：关系&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;211有序对笛卡尔积的定义（用笛卡尔积的定义来证明简单的集合等式）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;特殊的关系&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_56.DayE5WDA_ZLwBH7.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;212关系的定义&lt;/li&gt;
&lt;li&gt;214关系图和关系矩阵&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;关系的运算&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;关系逆的定义，关系复合的定义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_57.B-QpEpSo_21hvYX.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_58.C3uTUHLn_spq2V.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意记住符合的定义，本质上是在找中介&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_59.CAE14WDh_1JlSpq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;218关系的运算，关系矩阵的运算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;219关系逆的基本性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逆的逆等于自身&lt;/li&gt;
&lt;li&gt;关系逆保持子集关系&lt;/li&gt;
&lt;li&gt;逆和与或是可交换的（谁先谁后）&lt;/li&gt;
&lt;li&gt;关系逆和关系复合交换次序，括号内要反转&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_60.DqmdFstD_ZoEuMM.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;什么叫做保持子集关系呢，就是&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_61.BSK3C11n_Z1r4RSn.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这在许多证明中很有用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_62.-i8dRiyB_1eOkLN.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_63.As7YZWfH_Z2ul4Wg.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;220-221关系复合的基本性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换律❌&lt;/li&gt;
&lt;li&gt;结合律✅&lt;/li&gt;
&lt;li&gt;恒等关系是单位元&lt;/li&gt;
&lt;li&gt;关系复合保持子集关系&lt;/li&gt;
&lt;li&gt;关系复合对并分配，对与不分配（本质上是存在量词对析取分配而对合取不分配）
&lt;ul&gt;
&lt;li&gt;复合在外，或在括号内&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;关系的性质&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;要明确定义：元素定义法，还有性质：集合运算法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自反&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;223自反的定义&lt;/li&gt;
&lt;li&gt;224上面，易错点，自反与非自反的互斥但非对立关系&lt;/li&gt;
&lt;li&gt;224下面定理6.8定理的充分必要条件（集合运算）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;225对称的定义&lt;/li&gt;
&lt;li&gt;226下面定理6.9对称的充分必要条件（集合运算）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;228传递的定义&lt;/li&gt;
&lt;li&gt;229下面定理6.10传递的充分必要条件（集合运算 ）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;汇总&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;231关系定义和性质&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_64.Ce-_ZKZ8_DJ1sz.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意如果只有一个元素，那么前件为假，蕴含式为真，所以只含有一个元素的关系也是传递的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;231关系运算与关系性质之间的联系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;做题技巧，用关系图来判断关系的性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;画出一个关系的关系图，如果每个点都有自回路，那么就是自反的&lt;/li&gt;
&lt;li&gt;如果每两个点之间，要么没有直接通路，要么有双向直接通路，那么就是对称的&lt;/li&gt;
&lt;li&gt;如果能形成自闭环，那么是传递的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_65.Bx19oqcs_Z1LP8ra.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;易错点——下面的都是✔&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一个==非空==关系如果是自反的，那它一定不是反自反的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存在既不是自反，也不是反自反的关系——存在某些&amp;#x3C;x, x&gt;成立，也有些不成立&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_66.CQDrgVFn_2pC0Do.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存在既是对称也是反对称的关系——ΔA&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存在既不是对称也不是反对称的关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恒等关系除了不是反自反的，其他都是&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;给出参与运算的关系都有列对应的性质，那么运算的结果关系是否也有列对应的性质&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|   |   |   |   |   |   |
|---|---|---|---|---|---|
|&lt;strong&gt;关系运算&lt;/strong&gt;|&lt;strong&gt;自反性&lt;/strong&gt;|&lt;strong&gt;反自反性&lt;/strong&gt;|&lt;strong&gt;对称性&lt;/strong&gt;|&lt;strong&gt;反对称性&lt;/strong&gt;|&lt;strong&gt;传递性&lt;/strong&gt;|
|==&lt;strong&gt;关系逆运算R^(-1)&lt;/strong&gt;==|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|
|==&lt;strong&gt;集合交运算R∩S&lt;/strong&gt;==|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|
|&lt;strong&gt;集合并运算R∪S&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|
|&lt;strong&gt;集合差运算R-S&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|
|&lt;strong&gt;关系复合运算R∘S&lt;/strong&gt;|&lt;strong&gt;是&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;strong&gt;否&lt;/strong&gt;|&lt;/p&gt;
&lt;p&gt;可以分别用元素法和集合法证明a&lt;/p&gt;
&lt;h2&gt;关系的闭包&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;所谓闭包，就是要用最少的元素补充我们的集合，使其满足基本的性质（自反，对称，传递）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_67.DdGR5yKy_19odrT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;闭包的定义和性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;233关系闭包的定义&lt;/li&gt;
&lt;li&gt;234定理，若R为XX，则XX闭包是自己&lt;/li&gt;
&lt;li&gt;234定理，&lt;strong&gt;闭包保持子集关系&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_68.DZOXgp_P_Z1YQjvx.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_69.CKsOZMIl_2g9jfD.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;ABD都在考察同一件事情，也就是定义里的“包含R”&lt;/p&gt;
&lt;p&gt;C的话考察的是闭包的最小性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;闭包的运算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;236定理：如何补充以变成自反和对称闭包&lt;/li&gt;
&lt;li&gt;237关系的幂运算&lt;/li&gt;
&lt;li&gt;237定理：传递如何补充成为闭包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|   |   |
|---|---|
|&lt;strong&gt;关系代数中的概念&lt;/strong&gt;|&lt;strong&gt;图论中的概念&lt;/strong&gt;|
|二元关系 R|有向图的边集 E|
|关系的传递性|图中节点间的路径可达性|
|关系幂 Rn|从一个节点到另一个节点的长度为 &lt;em&gt;n&lt;/em&gt; 的路径|
|传递闭包 R∗|图的传递闭包（所有可能路径）|&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何补充以变成传递闭包
&lt;ul&gt;
&lt;li&gt;通过逻辑积计算关系矩阵
&lt;ul&gt;
&lt;li&gt;求幂&lt;/li&gt;
&lt;li&gt;所有幂的并&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Warshall算法
&lt;ul&gt;
&lt;li&gt;每次画出第x行为1的列和第x列为1的行，然后这些线的交点置为一&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等价关系
&lt;ul&gt;
&lt;li&gt;242定义
&lt;ul&gt;
&lt;li&gt;满足自反、对称、传递&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等价类
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;244定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记作$[a]_R$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总有$a∈[a]_R$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;理解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就他妈求连通分量，画出图来，圈就行了&lt;/li&gt;
&lt;li&gt;也可以直接看跟x有关系的所有值，这些值构成的集合就是x的等价类&lt;/li&gt;
&lt;li&gt;$[a]_R= [b]_R当且仅当⟨a, b⟩∈R$&lt;/li&gt;
&lt;li&gt;以一个模7同余为例子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_70.U1tyjPlx_Z1Tqsbk.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们可以将整数集拆分成这样子7组&lt;/li&gt;
&lt;li&gt;每一组里面的元素，都是彼此之间满足模7同余的&lt;/li&gt;
&lt;li&gt;任意不同组别的元素都是不满足模7同余的&lt;/li&gt;
&lt;li&gt;这样的一组就叫一个等价类&lt;/li&gt;
&lt;li&gt;所有的等价类构成这个全集的一个划分&lt;/li&gt;
&lt;li&gt;把每个等价类作为一个元素，那么这个集合就是👇&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;商集
&lt;ul&gt;
&lt;li&gt;所有等价类的集合$A/R$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;偏序关系
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;247定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;满足自反、反对称、传递&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;偏序集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;偏序集&lt;/strong&gt;（Partially Ordered Set，简称 &lt;strong&gt;poset&lt;/strong&gt;）描述了一个集合中元素之间的一种“部分”顺序关系。与完全排序的集合（例如自然数集合上的大小关系）不同，偏序集中的元素==不一定能够比较大小==，也就是说，==有些元素==之间没有明确的顺序关系。&lt;/li&gt;
&lt;li&gt;可比
&lt;ul&gt;
&lt;li&gt;虽然是用&amp;#x3C;符号来表示，但其实表达的还是满足偏序关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全序（链）
&lt;ul&gt;
&lt;li&gt;全都可比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;偏序
&lt;ul&gt;
&lt;li&gt;部分不可比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;覆盖&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$a&amp;#x3C;b$且不存在c使得$a &amp;#x3C; c, c &amp;#x3C; b$（两个不等式都要满足才行）&lt;/li&gt;
&lt;li&gt;也就是紧紧挨着，中间不再有更多的元素满足偏序关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哈斯图 （用来找极大元和极小元）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉自环&lt;/li&gt;
&lt;li&gt;只保留覆盖的边（无第三边）&lt;/li&gt;
&lt;li&gt;从下往上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最大元、最小元&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与所有元素都可比才行，不一定存在&lt;/li&gt;
&lt;li&gt;就是哈斯图上，连接所有分叉的唯一最高点&lt;/li&gt;
&lt;li&gt;例子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_71.NaNok2Ms_5CKny.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;12和8、10、9、11都不可比，所以不存在最大元&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;极大元、极小元&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没有元素可以a更大/小了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可能有元素和它不可比&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可能也不存在，如整数集$\Z$，四个都不存在&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例子—同上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8、12、10、9、11、7都是极大元&lt;/li&gt;
&lt;li&gt;就是哈希图上面没有分叉了的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_72.CUUAdv0l_Z2wsUke.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上界、下界&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象是S的子集&lt;/li&gt;
&lt;li&gt;上界b不一定在S中&lt;/li&gt;
&lt;li&gt;但是b&gt;S中任意元素，==都要能够比较==才行&lt;/li&gt;
&lt;li&gt;可以有很多个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_73.HyCmb1zT_1n0KWL.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上确界、下确界&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最小的上界&lt;/li&gt;
&lt;li&gt;最大的下界&lt;/li&gt;
&lt;li&gt;如果对于全集
&lt;ul&gt;
&lt;li&gt;上确界就是最大元&lt;/li&gt;
&lt;li&gt;下确界就是最小元&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于空集
&lt;ul&gt;
&lt;li&gt;上确界就是最小元&lt;/li&gt;
&lt;li&gt;下确界就是最大元&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_74.RcxMU3Z3_ZYO9Ck.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;关系证明题&lt;/h2&gt;
&lt;p&gt;第一步考虑能不能用集合语言来叙述，因为集合语言是一阶逻辑语言更高级的抽象&lt;/p&gt;
&lt;p&gt;如果集合语言行不通，不要傻傻发呆，可以尝试符号语言，老老实实抓住定义去做&lt;/p&gt;
&lt;h1&gt;第七章 函数&lt;/h1&gt;
&lt;h2&gt;260函数的定义&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_75.Bt-UPDen_kcPh4.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意是对==所有==的A中的元素，B中都有==唯一==的元素与其对应&lt;/li&gt;
&lt;li&gt;260定义域、陪域的定义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_76.CAaU8aKn_26iHqh.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;260像集和逆像集的定义
&lt;ul&gt;
&lt;li&gt;像集就是给出定义域的子集求值域&lt;/li&gt;
&lt;li&gt;逆像集就是给出值域范围反求定义域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;262性质
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$f$ 保持子集关系$S\subseteq U→f(S)\subseteq f(U)$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_77.C7-FzzGZ_Z2a0vmY.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数与集合交可以交换次序&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_78.VE446WTq_3LNMw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;单函数、满函数、双函数的定义&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单函数
&lt;ul&gt;
&lt;li&gt;B中每个元素==至多==有一个A中的元素与其对应&lt;/li&gt;
&lt;li&gt;可以没有对应&lt;/li&gt;
&lt;li&gt;做题的时候带入$f(x)=f(y$) 看有无 $x=y$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;满函数
&lt;ul&gt;
&lt;li&gt;B中每个元素==至少==有一个A中的元素与其对应&lt;/li&gt;
&lt;li&gt;必须要囊括值域的每一个元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;双函数
&lt;ul&gt;
&lt;li&gt;既是单函数也是满函数&lt;/li&gt;
&lt;li&gt;B中每个元素有==唯一==一个A中的元素与其对应&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_79.CagZUc0U_Y4MhC.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;266函数的复合
&lt;ul&gt;
&lt;li&gt;其实就是关系的复合&lt;/li&gt;
&lt;li&gt;保持子集关系&lt;/li&gt;
&lt;li&gt;保持单满性&lt;/li&gt;
&lt;li&gt;满足结合律&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_80.DhMYwyre_Hsacr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;267逆函数的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意前提是双函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_81.Cr5e52Cv_2oojOk.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合基数的基本&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这一部分不会出题考试&lt;/li&gt;
&lt;li&gt;但是面试的时候可能会问到&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;函数增长和算法效率分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;281定义大O记号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存在k和C当 $x&gt;k$ 有$|f(x)| ≤ C|g(x)|$&lt;/li&gt;
&lt;li&gt;这一部分出题：证明某个函数是$O(x^3)$而不是$O(x^2)$&lt;/li&gt;
&lt;li&gt;只需要找到对应的k和C即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大O记号的传递性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;O的加法定理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f_1(x) + f_2(x) \in O(max(g_1, g_2))$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;O的乘法定理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f_1(x). f_2(x) \in O(g_1 . g_2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;284定义大$\Omega$记号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$存在k和C当x&gt;k有|f(x)| ≥ C|g(x)|$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;O的除法定理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f_1(x)是O(g_1), f_2是\Omega (g_2) 那么f_1/f_2 是O(g_1/g_2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_82.Der4SnKn_1u7i2s.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第八章：计数组合&lt;/h1&gt;
&lt;h2&gt;计数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;302加法原理乘法原理
&lt;ul&gt;
&lt;li&gt;独立性
&lt;ul&gt;
&lt;li&gt;无论前面选择什么，后面的==方法数==（注意仅仅是数字，究竟是什么方法不care）==不变==&lt;/li&gt;
&lt;li&gt;也可以理解成，如果可以==直接用乘法原理==的，那就是有独立性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;相关性
&lt;ul&gt;
&lt;li&gt;前面的选择就对后面有影响&lt;/li&gt;
&lt;li&gt;易错
&lt;ul&gt;
&lt;li&gt;可以同时有相关性和独立性&lt;/li&gt;
&lt;li&gt;不相关→独立&lt;/li&gt;
&lt;li&gt;独立 !→ 不相关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;305减法原理
&lt;ul&gt;
&lt;li&gt;正难则反&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;306除法原理（不是重点）
&lt;ul&gt;
&lt;li&gt;必须保证是满射&lt;/li&gt;
&lt;li&gt;例子：圆桌排列、苯环取代&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;306一一对应原理
&lt;ul&gt;
&lt;li&gt;提供了一种通过建立两个集合之间的一一对应关系来计算集合大小的方法。具体来说，如果两个集合之间存在一个一一对应（双射）关系，那么这两个集合的元素个数必然相等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;308容斥原理&lt;/li&gt;
&lt;li&gt;集合差计数公式&lt;/li&gt;
&lt;li&gt;两集合容斥原理&lt;/li&gt;
&lt;li&gt;三集合容斥原理&lt;/li&gt;
&lt;li&gt;鸽笼原理
&lt;ul&gt;
&lt;li&gt;很容易指导什么是鸽子&lt;/li&gt;
&lt;li&gt;但是什么是鸽笼才是最重要的&lt;/li&gt;
&lt;li&gt;[[组合计数|组合计数]] 例题&lt;/li&gt;
&lt;li&gt;不会是考试重点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_83.DdKxz3WJ_ZTkQey.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;排列组合&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/6f056eb8f5784342d97d809a03696762.arn2mWdk_Z1kf1LV.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_84._ZgM319M_Z25Fxxi.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;==不允许重复的==&lt;/li&gt;
&lt;li&gt;316全排列
&lt;ul&gt;
&lt;li&gt;从n个里选择 r 排列，与顺序有关&lt;/li&gt;
&lt;li&gt;记作$P_n^r$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;318组合数
&lt;ul&gt;
&lt;li&gt;从n个里选择 r 与顺序无关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;318证明方法
&lt;ul&gt;
&lt;li&gt;组合证明
&lt;ul&gt;
&lt;li&gt;举一个针对同一集合的不同计数例子&lt;/li&gt;
&lt;li&gt;通常可以举例：学生选学生会，学生会选主席、长度为n的字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代数证明
&lt;ul&gt;
&lt;li&gt;通过数学归纳法和组合数计算公式证明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;技巧
&lt;ul&gt;
&lt;li&gt;至少有一名男生——全集减去一名男生都没有的&lt;/li&gt;
&lt;li&gt;奇数偶数、是否重复，注意判断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;323二项式定理
&lt;ul&gt;
&lt;li&gt;$(x + 1)^n = \Sigma_{i= 0}^n(^n_i)x^i$&lt;/li&gt;
&lt;li&gt;令$x=1$可得所有项之和&lt;/li&gt;
&lt;li&gt;令$x=-1$，结合上面，得到奇偶项关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;325帕斯卡等式
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_85.B49DlmkZ_1SEHjy.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在用数学归纳法证明一些等式，归纳递推的时候很好用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;326经常用到的公式&lt;/li&gt;
&lt;li&gt;==328允许重复的排列组合==&lt;/li&gt;
&lt;li&gt;328$每类物体分别有m_1、m_2…m_n个的n类物体的重复度为m_1+m_2+…+m_n=r的排列数为\\binom{r}{m_1}\binom{r-m_1}{m_2}...\binom{r - m_1 - m_2 - ...-m_{n-1}}{m_n} = \frac{r!}{m_1!m_2!...m_n!}$
&lt;ul&gt;
&lt;li&gt;理解1：多次分步来选择&lt;/li&gt;
&lt;li&gt;理解2：先全排列，即$r！$然后每个类里面都是无关次序的，所以分别除以他们的阶乘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;331物体个数不限的n类物体允许重复地选择r个的方案数——$\binom{n+r-1}{r}$
&lt;ul&gt;
&lt;li&gt;理解：
&lt;ul&gt;
&lt;li&gt;首先可以理解为不定方程的解&lt;/li&gt;
&lt;li&gt;和高中的隔板法很像，但是高中的隔板法至少要有一个&lt;/li&gt;
&lt;li&gt;而这里可以一个都没有，所以要把隔板也考虑占一个位置&lt;/li&gt;
&lt;li&gt;理解1：二进制串，用0来分隔开&lt;/li&gt;
&lt;li&gt;理解2：不定方程，那就相当于用n - 1个加号隔开r个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;trick
&lt;ul&gt;
&lt;li&gt;x的≥限定
&lt;ul&gt;
&lt;li&gt;如：苹果至少要a个， 香蕉至少b个&lt;/li&gt;
&lt;li&gt;令$x&lt;code&gt; = x + a$，那么$x&lt;/code&gt;&gt;0$，只需要转化为物体个数不限的n类物体允许重复地选择$r - a - b$个的方案数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;r的 ≤ 限定
&lt;ul&gt;
&lt;li&gt;例如：$x1+x2+x3+x4 ≤ 15$&lt;/li&gt;
&lt;li&gt;等价于$x1+x2+x3+x4+x5 = 15$&lt;/li&gt;
&lt;li&gt;添加一个未知数即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;x的≤限定
&lt;ul&gt;
&lt;li&gt;例如：$x1+x2+x3+x4 ≤ 15 且 x1≤ 5, x2≤ 3$&lt;/li&gt;
&lt;li&gt;方法：容斥原理&lt;/li&gt;
&lt;li&gt;定义事件：
&lt;ul&gt;
&lt;li&gt;U为没有限制$\binom{4+15-1}{15}$&lt;/li&gt;
&lt;li&gt;$P1为x1 ≥ 6\ r= r- 6\ N(P1) = \binom{4+9-1}{9}$&lt;/li&gt;
&lt;li&gt;$P2为x2 ≥ 4\ r= r-4\ N(P2) = \binom{4+11-1}{11}$&lt;/li&gt;
&lt;li&gt;$P1P2为x1≥6,x2 ≥ 4\ r= r-10\ N(P1P2) = \binom{4+5-1}{5}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$由容斥原理\N(\overline{P1}\overline{P2}) = U - N(P1∪ P2) = U - N(P1) - N(P2) + N(P1P2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;==338排列组合生成算法==
&lt;ul&gt;
&lt;li&gt;339在字典序下覆盖a的全排列
&lt;ul&gt;
&lt;li&gt;例如63285741&lt;/li&gt;
&lt;li&gt;从右往左找到第一个不是递增的，即5&lt;/li&gt;
&lt;li&gt;与他右边的换 5 和 7 换&lt;/li&gt;
&lt;li&gt;剩下的升序排列（保证字典序）&lt;/li&gt;
&lt;li&gt;得到63287145&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;341给定集合S求S的r组合
&lt;ul&gt;
&lt;li&gt;由于组合的无序性，所以要求生成的数组严格单调递增&lt;/li&gt;
&lt;li&gt;例如S={1,2,…,8} a = 13478&lt;/li&gt;
&lt;li&gt;从右往左找到第一个可以变大的数字（8、7不能了，但是4可以变大为5、6）&lt;/li&gt;
&lt;li&gt;将这个数字变为能变大的最小值，即5&lt;/li&gt;
&lt;li&gt;后面同理&lt;/li&gt;
&lt;li&gt;13567&lt;/li&gt;
&lt;li&gt;继续&lt;/li&gt;
&lt;li&gt;13568&lt;/li&gt;
&lt;li&gt;13578&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;==342递推关系式==
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;递推关系：必须由==前面==的项推出来&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/90043ac875f2974e6f1963c6d91291c6.D4ZoCUjS_Z4ViVP.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通项公式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;封闭公式解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$a_n$能使用==不包含序列中任意项==的通项公式表示&lt;/li&gt;
&lt;li&gt;给出一个即可，不明确初始特解的情况下可能有很多封闭公式解&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分类的递推，类似于动态规划，一定要相信它满足条件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意最后写出递归式的时候说明n的范围，特殊情况，如n=0、1要单独说明&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意分情况讨论，要充分，然后再加起来的时候合并&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;==349线性递推关系式求解==
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常系数线性齐次递推关系式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_86.DGio2UHf_w9GYU.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_87.CXXsiQY0_29zg4p.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\sum &lt;em&gt;{i = 1}^{t} (r_i^n.\sum&lt;/em&gt;{j = 0}^{m_i - 1}\beta_{ij}n^j)$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常系数线性非齐次递推关系式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解的形式定理——这个定理给出了解是由齐次解+一个特解的结构组成的，所以关键是要找到一个特解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_88.BEVo1gYh_ZDGflH.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特解构造定理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_89.BB6_uEPX_P65t2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总结：做题步骤&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求解伴随齐次特征多项式得到特征根r&lt;/li&gt;
&lt;li&gt;将F(n)写成上图标黄的形式&lt;/li&gt;
&lt;li&gt;确定最高次数和s&lt;/li&gt;
&lt;li&gt;根据s是否是特征根分情况讨论得到特解&lt;/li&gt;
&lt;li&gt;将特解带入递推式&lt;/li&gt;
&lt;li&gt;用特殊值带入求解方程&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;==分治算法及其效率分析==&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主定理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_90.eyC_3svB_Z1UbEcO.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第九章：图与树&lt;/h1&gt;
&lt;h2&gt;图&lt;/h2&gt;
&lt;h3&gt;基本概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;362无向图的定义&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;363有向图的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有向图的==基图==是不考虑方向得到的无向图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;364&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;n阶图：顶点数为n&lt;/li&gt;
&lt;li&gt;平凡图：顶点数为1&lt;/li&gt;
&lt;li&gt;空图：顶点数为0&lt;/li&gt;
&lt;li&gt;零图：边数为0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;364环、简单图的定义&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_91.D6Re-mjx_Z26Mypf.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;365度的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意记号$d(G)$&lt;/li&gt;
&lt;li&gt;出度是$d^+(G)$入度是$d^-(G)$&lt;/li&gt;
&lt;li&gt;==图与简单图中对于顶点度的要求==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_92.OfMjUxOf_23zuQi.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;==365握手定理==&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;总度数为顶点数的两倍，入度等于出度等于顶点数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_93.Ygbr5596_pUDKe.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这在后面的证明中非常重要，常常矛盾的情况是用握手定理推出来的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推论：总度数为偶数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例题：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_94.C0cc5RMq_1OSm3c.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推论：任何图的奇度顶点个数是偶数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;==推论：简单图的最大度====$\Delta(G)≤ n - 1$==&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;366正则图和完全图的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;k正则图：任意顶点的度数都是k&lt;/li&gt;
&lt;li&gt;n完全图：任意两个顶点之间都有边
&lt;ul&gt;
&lt;li&gt;边数就是$\frac{n(n - 1)}{2}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_95.CExgMwiz_jV81.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;366二部图的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;二部图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_96.DaqUlCA2_Z29bAyf.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;其实很像一个神经网络的全连接层&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;367生成子图和导出子图的定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成子图：指从原始图中选取部分&lt;strong&gt;边&lt;/strong&gt;，但保留所有&lt;strong&gt;顶点&lt;/strong&gt;，形成的子图。&lt;/li&gt;
&lt;li&gt;导出子图：从原始图中选取部分&lt;strong&gt;顶点&lt;/strong&gt;，并保留这些顶点之间在原始图中存在的所有&lt;strong&gt;边&lt;/strong&gt;，形成的子图。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;367&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除顶点集：从图中移除指定的一组顶点及其相关联的边。&lt;/li&gt;
&lt;li&gt;删除边集：从图中移除指定的一组边，同时==保留所有顶点==。&lt;/li&gt;
&lt;li&gt;删除子图：顶点集不变，但是删除边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;==368图的连通性==&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通路、回路、简单（有向）通路、简单（有向）回路的定义&lt;/li&gt;
&lt;li&gt;回路、初级回路（没有重点）、简单回路（没有重边）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_97.D81Tc77F_1MpcGn.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_98.unIznouE_46Xhn.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;长度为n的通路、回路条数计算&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_99.Sx8JU09y_ZhllOI.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;370&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可达与连通分支&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_100.CyL0xTYl_Z2jQSfb.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类似于握手定理，下面的引理也可以作为一个反证法的矛盾出发点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_101.CuuDMfes_Z11sgKF.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;证明方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_102.CvAg7tK9_ZhPDv0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以上面定理为例子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_103.BOrcXPRB_2dBGWd.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_104.Cjo3s4VM_Z1BO5jf.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;371&lt;strong&gt;点割集&lt;/strong&gt;和**边割集——**用于描述破坏图连通性的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**点割集：**一组顶点的集合，若将这些顶点（少一个都不行）及其关联的边移除后，图会被分割成多个连通分量（或不再连通）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;割点：如果点割集只有一个顶点，这个点就叫做割点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**边割集：**一组边的集合，若将这些边（少一条都不行）移除后，图会被分割成多个连通分量（或不再连通）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;割边（桥）：如果边割集只包含一条边，这条边就叫做割边或桥&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点连通度：最少需要删除多少个顶点才能使图不连通或者成为平凡图——顶点数最少的点割集的顶点数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;边连通度：最少需要删除多少条边才能使图不连通——边数最少的边割集的边数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_105.CK3RtKNm_2aaOv5.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;372强连通和单项连通&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;强连通：在有向图中，任意两个顶点之间都存在双向可达的路径&lt;/li&gt;
&lt;li&gt;单向连通：在有向图中，从某个顶点出发可以到达其他所有顶点，但不一定存在返回路径&lt;/li&gt;
&lt;li&gt;强连通分量：有向图中的最大强连通子图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;欧拉图&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一定要注意！！欧拉图是==对边做讨论==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_106.CBvC2tck_ZrcVOk.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;补充：无向连通图G只有两个度数为奇数的顶点，则G存在欧拉通路，则G是半欧拉图&lt;/li&gt;
&lt;li&gt;注意：==欧拉图没有强调是简单图！！==例如一个图如果是欧拉图，然后你添上一条重边，必然会使得他有两个顶点的度变成奇数，那就变成了欧拉通路，半欧拉图。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;哈密顿图&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一定要注意！！哈密顿图是==对顶点做讨论==&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_107.CqpwWiXq_Z1ONtJ1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;澄清哈密顿回路的定义&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;哈密顿回路的定义是：
&lt;ul&gt;
&lt;li&gt;路径必须经过图中的&lt;strong&gt;所有顶点一次且仅一次&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由于回路的起点和终点是同一个顶点，因此==起点会在路径中出现====&lt;strong&gt;两次&lt;/strong&gt;==，这是合法的。这并不违背哈密顿回路的定义。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无向树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无向树没有根这个说法，只包含内部节点和叶子：内部节点就是度为≥2；叶子就是度为1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_108.CWKyick1_ZgI8L3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_109.BsqBmeKv_15F4LA.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无向树的每一条边都是桥（删掉就不连通了）&lt;/li&gt;
&lt;li&gt;无向树是简单图
&lt;ul&gt;
&lt;li&gt;对于3的证明，用反证法+握手定理注意啊手动阀&lt;/li&gt;
&lt;li&gt;对于3，如果是又向根树，是不成立的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;==注意：数据结构中树是有向树，其节点的度的定义是孩子的数量，但是这的度的定义是无向图中的度的定义==&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无向树的证明，遇到度数的，往握手定理靠&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_110.b5EbN5Y7_ywXDS.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有向树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基图是无向树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_111.BeR0iWUS_Z1oNs1W.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;+有向树内部只含有两种节点，内部和叶子&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有向树有且仅有一个顶点的入度为0&lt;/li&gt;
&lt;li&gt;m元树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_112.CT_oV3t3_OwCU2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_113.CMJooNRf_1mn8U0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;典型的题型：利用握手定理和树中顶点与边数的关系来列方程求解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈夫曼树
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;例题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_114.uH5438Vl_Z1sDKl1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;==平面图==&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_115.BPI4UOV6_WCcPC.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_116.uRqRFXNC_243oHY.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;握手定理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_117.DxfAOttN_ZhR5lg.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;欧拉公式（用正方体来记忆 ）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_118.CbWfO6Et_lYHU1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;平面图的边数上界&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_119.CziFSfMN_1D3iEN.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_120.CHUEYkJs_ZMya51.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_121.DQqUuZoZ_IqqsK.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;应用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iaohr9.github.io/_astro/image_122.Cghm298I_Z1QAVWJ.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;考试&lt;/h1&gt;
&lt;h2&gt;题型：&lt;/h2&gt;
&lt;p&gt;单选 15*2 30分&lt;/p&gt;
&lt;p&gt;论证题：16 10分&lt;/p&gt;
&lt;p&gt;计算题：17-20 34分&lt;/p&gt;
&lt;p&gt;证明题：21-23 26分&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>C++理论题总结</title><link>https://iaohr9.github.io/blog/c%E7%90%86%E8%AE%BA%E9%A2%98</link><guid isPermaLink="true">https://iaohr9.github.io/blog/c%E7%90%86%E8%AE%BA%E9%A2%98</guid><description>C++理论题总结</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++理论题总结&lt;/h1&gt;
&lt;p&gt;By lhr 2024/7/7&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;p&gt;[toc]&lt;/p&gt;
&lt;h1&gt;第一周&lt;/h1&gt;
&lt;h2&gt;PPT&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;namespace
&lt;ul&gt;
&lt;li&gt;所有全局变量共享一个命名空间——默认namespace&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Question 4 - 单选题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当x = 24, y = 4 时，语句cout&amp;#x3C;&amp;#x3C;( x &gt; 23 &amp;#x26;&amp;#x26; x - y &amp;#x3C;=20) &amp;#x3C;&amp;#x3C; endl; 的执行结果为：&lt;/p&gt;
&lt;p&gt;A.true&lt;/p&gt;
&lt;p&gt;B.1&lt;/p&gt;
&lt;p&gt;C.0&lt;/p&gt;
&lt;p&gt;D.false&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;只能记住，C++条件运算输出的是1/0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Question 5 - 单选题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在Linux系统下，选择与下述代码输出相同的选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;cout &amp;#x3C;&amp;#x3C; &quot;The answer is:&quot; &amp;#x3C;&amp;#x3C; ends &amp;#x3C;&amp;#x3C; 3.14 * 4 &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.&lt;code&gt;double a = 3.14 * 4; printf(&quot;The answer is: %f&quot;, a); &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.&lt;code&gt;cout &amp;#x3C;&amp;#x3C; &quot;The answer is: &quot;; cout &amp;#x3C;&amp;#x3C; 3.14 * 4 &amp;#x3C;&amp;#x3C; endl; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;C.&lt;code&gt;printf(&quot;The answer is:&quot;); putchar(&apos;\0&apos;); printf(&quot;%.2f\n&quot;, 3.14 * 4); &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;D.&lt;code&gt;printf(&quot;The answer is: %d\n&quot;, 3.14 * 4); &lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;==&lt;code&gt;std::ends&lt;/code&gt; 的主要功能是向输出流中插入一个空字符（null character）==，即 ASCII 码为 0 的字符。&lt;/li&gt;
&lt;li&gt;这个空字符通常用于标记 C 风格字符串的结束。在 C 和 C++ 中，字符串通常以空字符结尾。&lt;/li&gt;
&lt;li&gt;不同于 &lt;code&gt;std::endl&lt;/code&gt;，&lt;code&gt;std::ends&lt;/code&gt; 不会刷新输出流的缓冲区。它只是简单地插入一个空字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Question 7 - 单选题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下述语句的输出是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;cout &amp;#x3C;&amp;#x3C; 1 + &quot;20.24&quot; &amp;#x3C;&amp;#x3C; endl &amp;#x3C;&amp;#x3C; 20.24;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.编译错误&lt;/p&gt;
&lt;p&gt;B.21.24&lt;/p&gt;
&lt;p&gt;20.24&lt;/p&gt;
&lt;p&gt;C.120.24 20.24&lt;/p&gt;
&lt;p&gt;D.0.24\n20.24&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：一个整型加一个字符串，其实是地址+1，从0开始输出&lt;/p&gt;
&lt;p&gt;Question 8 - 单选题&lt;/p&gt;
&lt;p&gt;当输入为&lt;code&gt;x&lt;/code&gt;时下面语句的输出是？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int x;
cin&gt;&gt;x;
cout&amp;#x3C;&amp;#x3C;x;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.0&lt;/p&gt;
&lt;p&gt;B.x&lt;/p&gt;
&lt;p&gt;C.编译错误&lt;/p&gt;
&lt;p&gt;D.字符x的ASKII码&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;p&gt;在C++中，如果你运行了上面的代码并尝试输入一个非整数值（如字母&lt;code&gt;x&lt;/code&gt;），那么&lt;code&gt;cin &gt;&gt; x;&lt;/code&gt;这一行将会失败，因为它期待的是一个整数。当输入流&lt;code&gt;cin&lt;/code&gt;遇到它不能解析为整数的字符时，它会停止读取，并设置一个错误状态（failbit），并将x设置为0。&lt;/p&gt;
&lt;p&gt;Question 12 - 单选题&lt;/p&gt;
&lt;p&gt;下面代码在输入2以后，输出为？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include&amp;#x3C;iostream&gt;
using namespace std; 
int main(){
	int cout;
	cin&gt;&gt;cout;
	std::cout&amp;#x3C;&amp;#x3C;(cout&amp;#x3C;&amp;#x3C;cout);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.2&lt;/p&gt;
&lt;p&gt;B.编译错误&lt;/p&gt;
&lt;p&gt;C.4&lt;/p&gt;
&lt;p&gt;D.8&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;p&gt;注意命名空间之间的嵌套，全局cout覆盖了std的cout，&amp;#x3C;&amp;#x3C; 左操作数是整数，所以移位&lt;/p&gt;
&lt;p&gt;Question 13 - 单选题&lt;/p&gt;
&lt;p&gt;下面代码在输入2以后，输出为？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include&amp;#x3C;iostream&gt;
using namespace std; 
int main(){
	int cout;
	cin&gt;&gt;cout;
	cout&amp;#x3C;&amp;#x3C;cout;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.没有任何输出&lt;/p&gt;
&lt;p&gt;B.编译错误&lt;/p&gt;
&lt;p&gt;C.运行错误&lt;/p&gt;
&lt;p&gt;D.2&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;Question 17 - 单选题&lt;/p&gt;
&lt;p&gt;判断：引用就是某一变量（目标）的一个别名，对引用的操作与对变量直接操作完全一样。&lt;/p&gt;
&lt;p&gt;A.正确&lt;/p&gt;
&lt;p&gt;B.错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;h1&gt;第二周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Question 4 - 单选题当输入内容是“ Hello World! ”时（注意开头和结尾处各有一个空格），下列语句的输出结果是？&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main() {
	string str1, str2;
	cin &gt;&gt; str1 &gt;&gt; str2;
	cout &amp;#x3C;&amp;#x3C; str1 &amp;#x3C;&amp;#x3C; str2;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.&lt;code&gt;HelloWorld!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.&lt;code&gt; Hello&lt;/code&gt;（开头处有一空格）&lt;/p&gt;
&lt;p&gt;C.&lt;code&gt;Hello &lt;/code&gt;（结尾处有一空格）&lt;/p&gt;
&lt;p&gt;D.&lt;code&gt;Hello World&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;无论输入多少个空格如“                     hello        world        ”都会输出helloworld&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 5 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;当输入内容是“ Hello World! ”时（注意开头和结尾处各有一个空格），下列语句的输出结果是？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main(){
	string str;
	getline(cin, str);
	cout &amp;#x3C;&amp;#x3C; str;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.&lt;code&gt;HelloWorld!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.&lt;code&gt;Hello World! &lt;/code&gt;（注意结尾处有一空格）&lt;/p&gt;
&lt;p&gt;C.&lt;code&gt; Hello World!&lt;/code&gt;（注意开头处有一空格）&lt;/p&gt;
&lt;p&gt;D.&lt;code&gt;Hello World!&lt;/code&gt;（注意开头和结尾处各有一个空格）&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 6 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;下列选项的操作符中，不属于string对象的比较运算符的是？&lt;/p&gt;
&lt;p&gt;A.!=&lt;/p&gt;
&lt;p&gt;B.&amp;#x3C;=&lt;/p&gt;
&lt;p&gt;C.=&lt;/p&gt;
&lt;p&gt;D.&amp;#x3C;=&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;注意审题：比较运算符，C是赋值运算符&lt;/p&gt;
&lt;p&gt;在C++中，&lt;code&gt;&amp;#x3C;=&gt;&lt;/code&gt; 是一个称为“三向比较运算符”（也称为“spaceship operator”）的C++20引入的新特性。这个运算符用于返回一个整数，该整数表示两个操作数之间的相对顺序。&lt;/p&gt;
&lt;p&gt;具体来说，对于&lt;code&gt;a &amp;#x3C;=&gt; b&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;a&lt;/code&gt; 小于 &lt;code&gt;b&lt;/code&gt;，则返回负整数。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;a&lt;/code&gt; 等于 &lt;code&gt;b&lt;/code&gt;，则返回零。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;a&lt;/code&gt; 大于 &lt;code&gt;b&lt;/code&gt;，则返回正整数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;std::string&lt;/code&gt;，&lt;code&gt;&amp;#x3C;=&gt;&lt;/code&gt;运算符允许你以一种简洁且类型安全的方式比较两个字符串。&lt;/p&gt;
&lt;p&gt;例如，如果你有两个&lt;code&gt;std::string&lt;/code&gt;对象&lt;code&gt;str1&lt;/code&gt;和&lt;code&gt;str2&lt;/code&gt;，你可以这样使用&lt;code&gt;&amp;#x3C;=&gt;&lt;/code&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;```cpp
#include &amp;#x3C;string&gt;
#include &amp;#x3C;iostream&gt;

int main() {
    std::string str1 = &quot;apple&quot;;
    std::string str2 = &quot;banana&quot;;

    auto result = str1 &amp;#x3C;=&gt; str2;

    if (result &amp;#x3C; 0) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;str1 is less than str2\n&quot;;
    } else if (result == 0) {
        std::cout &amp;#x3C;&amp;#x3C; &quot;str1 is equal to str2\n&quot;;
    } else {
        std::cout &amp;#x3C;&amp;#x3C; &quot;str1 is greater than str2\n&quot;;
    }

    return 0;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 8 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;现有如下三个string对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;string s1 = &quot;Hello&quot;, s2 = &quot;, &quot;, s3 = &quot;C++!&quot;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则下列哪个选项的语句中string对象的初始化会出错？&lt;/p&gt;
&lt;p&gt;A.&lt;code&gt;string s4 = s1 + &quot;, &quot; + s3; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.&lt;code&gt;string s5 = s1 + &quot;, &quot; + &quot;C++!&quot;; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;C.&lt;code&gt;string s6 = &quot;Hello&quot; + s2 + &quot;C++!&quot;; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;D.&lt;code&gt;string s7 = &quot;Hello&quot; + &quot;, &quot; + s3; &lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：没有定义两个字符串数组的加法，而前面三个选项都有一个字符串跟在运算符的两边，所以可以运算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 10 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;下列语句的执行结果是？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main()
{
	string str = &quot;sysu-computer&quot;;
	int pos1 = str.find(&apos;u&apos;);
	int pos2 = str.find(&apos;u&apos;, 10);
	int pos3 = str.rfind(&apos;u&apos;);
	cout &amp;#x3C;&amp;#x3C; pos1 &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; pos2 &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; pos3;
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.3 9 9&lt;/p&gt;
&lt;p&gt;B.3 -1 9&lt;/p&gt;
&lt;p&gt;C.3 -1 -4&lt;/p&gt;
&lt;p&gt;D.3 -1 3&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：这个题容易错误的地方在于，无论从左边还是从右边——都是返回他的下标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for(auto &amp;#x26;c : str)&lt;/code&gt;注意使用你用和不用引用的区别。如果不用引用的话是复制一个拷贝出来，不会改变原来的对象。如果是用引用的话，可以直接修改原来的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 12 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;现有一个长度大于等于 4 的string对象&lt;code&gt;str&lt;/code&gt;，需要判断&lt;code&gt;str&lt;/code&gt;是否以“Sysu”开头（即，前 4 个字符所构成的子串内容是否为“Sysu”）。&lt;/p&gt;
&lt;p&gt;下列选项中的表达式，单独作为判断条件，无法实现上述功能的是？&lt;/p&gt;
&lt;p&gt;A.str.find(&quot;Sysu&quot;) == 0&lt;/p&gt;
&lt;p&gt;B.str &gt;= &quot;Sysu&quot;&lt;/p&gt;
&lt;p&gt;C.str.substr(0, 4) == &quot;Sysu&quot;&lt;/p&gt;
&lt;p&gt;D.str.compare(0, 4, &quot;Sysu&quot;) == 0&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：想要得到答案非常容易。但是想强调的是C选项里面，它是左闭右开区间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 14 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;下列两段程序的运行情况为：
程序一：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;void func1(const int n){
	array &amp;#x3C;bool, n&gt; arr;
	arr[0] = true;
}
int main()
{
	func1(2);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序二：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void func2(){
	const int n = 2;
	array &amp;#x3C;bool, n&gt; arr;
	arr[0] = true;
}
int main()
{
	func2();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.程序一和程序二均可正常运行&lt;/p&gt;
&lt;p&gt;B.程序一和程序二均不可正常运行&lt;/p&gt;
&lt;p&gt;C.程序一可正常运行，程序二不可正常运行&lt;/p&gt;
&lt;p&gt;D.程序一不可正常运行，程序二可正常运行&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：程序一不可以运行的原因是——他要到运行时传参的时候才可以确定尖括号里面的值。但是程序二，它在编译的时候就可以确定尖括号里面的值是常量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 18 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;关于下列语句，说法正确的是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;const int func1(int arg){
    return 2 * arg;
}
constexpr int func2(int arg){
    return 2 * arg;
}
int main()
{
    array&amp;#x3C;int, func1(10)&gt; a;
    array&amp;#x3C;int, func2(10)&gt; b;
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.a, b 均能完成初始化&lt;/p&gt;
&lt;p&gt;B.a 能完成初始化， b 不能完成初始化&lt;/p&gt;
&lt;p&gt;C.a 不能完成初始化， b 能完成初始化&lt;/p&gt;
&lt;p&gt;D.a, b 均不能完成初始化&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：同理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 C++11 标准中，建议按照以下方式将 const 和 constexpr 的功能区分开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;凡是表达“只读”语义的场景都使用 const&lt;/li&gt;
&lt;li&gt;凡是表达“常量”语义的场景都使用 constexpr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 16 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;下列语句的运行结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main(void) {
    int a = 1;
    const int &amp;#x26; const_b = a;
    cout &amp;#x3C;&amp;#x3C; const_b &amp;#x3C;&amp;#x3C; endl;
    a = 2;
    cout &amp;#x3C;&amp;#x3C; const_b &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.1
1&lt;/p&gt;
&lt;p&gt;B.1
2&lt;/p&gt;
&lt;p&gt;C.2
1&lt;/p&gt;
&lt;p&gt;D.2
2&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：他只是不能通过const_b来改变a&lt;/p&gt;
&lt;p&gt;但是注意：是绝对不可以反过来的&lt;/p&gt;
&lt;p&gt;变式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main() {
	double a = 3.14;
    const double &amp;#x26; b = a;
    const int &amp;#x26; c = a;
    
    cout &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; endl;
    // output:3.14, 3.14, 3
    
    a *= 2;
    
    cout &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; endl;
    // output:6.28, 6.28, 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么c不会跟着变呢，因为&lt;code&gt;const int &amp;#x26; c&lt;/code&gt;必须要指向一个&lt;code&gt;int&lt;/code&gt;的变量，所以在第4行的时候，创建了一个临时变量，然后c指向这个临时变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 17 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;关于下列语句（1）和语句（2），说法正确的是：&lt;/p&gt;
&lt;p&gt;（1）const int a = 3 + 6;
（2）constexpr int a = 3 + 6;&lt;/p&gt;
&lt;p&gt;A.（1）和（2）均合法&lt;/p&gt;
&lt;p&gt;B.（1）和（2）均不合法&lt;/p&gt;
&lt;p&gt;C.（1）合法，（2）不合法&lt;/p&gt;
&lt;p&gt;D.（1）不合法，（2）合法&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;在C++中，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;constexpr&lt;/code&gt; 都用于声明常量，但它们之间有一些关键的区别。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;编译时和运行时&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;const&lt;/code&gt; 声明的常量在运行时具有确定的值。这意味着这个值在编译时可能不知道，但在程序执行时它必须有一个确定的值。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;constexpr&lt;/code&gt; 声明的常量在编译时就有确定的值。编译器在编译阶段就会计算其值，并将其嵌入到生成的代码中。因此，&lt;code&gt;constexpr&lt;/code&gt; 常量可以用于那些需要常量表达式的地方，比如数组的大小、模板参数等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;初始化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 常量可以使用任何在编译时或运行时能够确定的表达式进行初始化。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constexpr&lt;/code&gt; 常量必须使用常量表达式进行初始化，这通常意味着它只能包含编译时常量、文字值以及只调用其他 &lt;code&gt;constexpr&lt;/code&gt; 函数的函数调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数和类构造函数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;除了变量，你还可以将 &lt;code&gt;constexpr&lt;/code&gt; 应用于函数或类的构造函数。这表示这些函数或构造函数在编译时就能计算出结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 不能这样使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存位置&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;constexpr&lt;/code&gt; 在内存中的位置没有固定的区别。它们都可能存储在只读数据段中，但这不是由 &lt;code&gt;const&lt;/code&gt; 或 &lt;code&gt;constexpr&lt;/code&gt; 关键字直接决定的，而是由它们的使用方式和上下文决定的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于你给出的两个例子：&lt;/p&gt;
&lt;p&gt;（1）&lt;code&gt;const int a = 3 + 6;&lt;/code&gt;
这里，&lt;code&gt;a&lt;/code&gt; 是一个常量，它的值在运行时是确定的（尽管在这种情况下，编译器可能在编译时就能计算出它的值）。&lt;/p&gt;
&lt;p&gt;（2）&lt;code&gt;constexpr int a = 3 + 6;&lt;/code&gt;
这里，&lt;code&gt;a&lt;/code&gt; 是一个编译时常量。它的值在编译时就被确定为9，并且这个值会被嵌入到生成的代码中。由于它是一个编译时常量，它可以用于那些需要常量表达式的地方。&lt;/p&gt;
&lt;p&gt;注意：虽然在这个例子中，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;constexpr&lt;/code&gt; 的效果看起来是一样的，但在更复杂的场景中，它们之间的区别可能会更加明显。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 19 - 单选题&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;下列关于 auto 关键字的说法，错误的是：&lt;/p&gt;
&lt;p&gt;A.使用 auto 声明的变量必须初始化&lt;/p&gt;
&lt;p&gt;B.函数和模板参数不能被声明为 auto&lt;/p&gt;
&lt;p&gt;C.auto 不能用于类型转换或其他一些操作，如 sizeof 和 typeid 操作&lt;/p&gt;
&lt;p&gt;D.定义在一个 auto 序列的变量不必始终推导成同一类型&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：auto的一些知识点，通过这个题来熟悉&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第三周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类内定义的函数一定是内联的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断：sizeof(引用)是指所指向变量的大小；sizeof(指针)结果为对象地址的大小&lt;/p&gt;
&lt;p&gt;A.正确&lt;/p&gt;
&lt;p&gt;B.错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;解释：在C++中，&lt;code&gt;sizeof&lt;/code&gt; 是一个操作符，用于获取对象或类型在内存中所占用的字节大小。但是，当 &lt;code&gt;sizeof&lt;/code&gt; 应用于引用（reference）时，情况有些特殊。&lt;/p&gt;
&lt;p&gt;引用本身并不是一个对象，它只是一个别名，或者说是一个已存在对象的另一个名字。因此，你不能直接取一个引用的 &lt;code&gt;sizeof&lt;/code&gt;，因为引用本身并不占用额外的内存空间（除了在编译时用于一些内部处理，但这与 &lt;code&gt;sizeof&lt;/code&gt; 无关）。&lt;/p&gt;
&lt;p&gt;当你写 &lt;code&gt;sizeof(引用)&lt;/code&gt; 时，你实际上是在获取该引用所指向的对象类型的大小。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第四周&lt;/h1&gt;
&lt;h2&gt;PPT&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;默认参数只能在普通参数的右边&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;析构函数不能直接调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;友元函数没有this指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;static成员没有this指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有动态成员才有this指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;this指针是所有成员函数的隐含参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;static成员函数不接收this指针做参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当成员参数与成员数据重名时，必须用this访问成员数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在C++中，&lt;code&gt;static&lt;/code&gt; 成员（无论是数据成员还是成员函数）都是与类本身关联的，而不是与类的任何特定实例（对象）关联的。因此，它们不需要通过类的实例（即对象）来访问，而是可以直接通过类名来访问。&lt;/p&gt;
&lt;p&gt;当你尝试在 &lt;code&gt;static&lt;/code&gt; 成员函数中使用 &lt;code&gt;this&lt;/code&gt; 指针时，编译器会报错，因为 &lt;code&gt;this&lt;/code&gt; 指针是指向调用该函数的对象的指针，而 &lt;code&gt;static&lt;/code&gt; 成员函数并不与任何特定对象关联。&lt;/p&gt;
&lt;p&gt;以下是一些关于 &lt;code&gt;static&lt;/code&gt; 成员的基本点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态数据成员&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态数据成员在类的所有实例之间共享。&lt;/li&gt;
&lt;li&gt;静态数据成员需要在类定义之外进行有且只有一次的初始化。注意格式为 Typename ClassName::VarName = 0;(不要再加static)&lt;/li&gt;
&lt;li&gt;可以通过类名或对象名来访问静态数据成员，但推荐使用类名。ClassName::VarName = 1;&lt;/li&gt;
&lt;li&gt;具有全局生存期，所有对象共享的储存空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态成员函数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态成员函数只能访问静态数据成员、其他静态成员函数和全局变量。&lt;/li&gt;
&lt;li&gt;静态成员函数没有 &lt;code&gt;this&lt;/code&gt; 指针。&lt;/li&gt;
&lt;li&gt;静态成员函数不能访问非静态数据成员或调用非静态成员函数（没有this指针做参数）（除非它们通过对象名或指针/引用显式传递）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    static int staticVar; // 静态数据成员

    static void staticFunc() {
        // this-&gt;staticVar; 	 // 错误：静态成员函数不能使用this指针
        MyClass::staticVar = 42; // 正确：使用类名访问静态数据成员
    }

    void nonStaticFunc() {
        MyClass::staticVar = 10; // 正确：也可以在非静态成员函数中通过类名访问静态数据成员
        this-&gt;nonStaticVar = 20; // 正确：非静态成员函数可以使用this指针访问非静态数据成员
    }

    int nonStaticVar; // 非静态数据成员
};

int MyClass::staticVar = 0; // 静态数据成员的初始化（不能再加static，否则报错）

int main() {
    MyClass obj;
    MyClass::staticFunc(); // 调用静态成员函数
    std::cout &amp;#x3C;&amp;#x3C; MyClass::staticVar &amp;#x3C;&amp;#x3C; std::endl; // 输出：42
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，&lt;code&gt;staticVar&lt;/code&gt; 是一个静态数据成员，而 &lt;code&gt;staticFunc&lt;/code&gt; 是一个静态成员函数。&lt;code&gt;staticFunc&lt;/code&gt; 使用类名 &lt;code&gt;MyClass&lt;/code&gt; 来访问和修改 &lt;code&gt;staticVar&lt;/code&gt;，而不是使用 &lt;code&gt;this&lt;/code&gt; 指针。同样，你也可以通过对象名（如 &lt;code&gt;obj.staticVar&lt;/code&gt;）来访问静态数据成员，但这通常不是推荐的做法，因为它可能会导致混淆。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mutable&lt;/code&gt; 关键字用于修饰类的非静态成员变量，表示即使该类的对象是 &lt;code&gt;const&lt;/code&gt; 的，这个成员变量也可以被修改。这在某些特定的场景下是有用的，比如当你需要在 &lt;code&gt;const&lt;/code&gt; 成员函数内部修改某个成员变量的值，但又不希望改变对象本身的逻辑状态时。&lt;/p&gt;
&lt;p&gt;==好好学这个，可以偷鸡！==&lt;/p&gt;
&lt;p&gt;以下是一个简单的C++示例，展示了 &lt;code&gt;mutable&lt;/code&gt; 的用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    int value;
    mutable int mutableValue;

    MyClass(int v, int mv) : value(v), mutableValue(mv) {}

    // 一个const成员函数，可以修改mutableValue但不能修改value
    void printAndIncrementMutable() const {
        std::cout &amp;#x3C;&amp;#x3C; &quot;Before increment: &quot; &amp;#x3C;&amp;#x3C; mutableValue &amp;#x3C;&amp;#x3C; std::endl;
        mutableValue++; // 这是允许的，因为mutableValue是可变的
        std::cout &amp;#x3C;&amp;#x3C; &quot;After increment: &quot; &amp;#x3C;&amp;#x3C; mutableValue &amp;#x3C;&amp;#x3C; std::endl;

        // value++; // 这是不允许的，因为value不是mutable的，且我们在一个const成员函数中
    }
};

int main() {
    const MyClass obj(10, 20);
    obj.printAndIncrementMutable(); // 输出: Before increment: 20, After increment: 21
    // obj.value = 30; // 这是不允许的，因为obj是const的
    obj.mutableValue = 1; // it&apos;s ok！
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的示例中，尽管 &lt;code&gt;obj&lt;/code&gt; 是一个 &lt;code&gt;const&lt;/code&gt; 对象，我们仍然可以在 &lt;code&gt;printAndIncrementMutable&lt;/code&gt; 函数中修改 &lt;code&gt;mutableValue&lt;/code&gt; 的值，因为 &lt;code&gt;mutableValue&lt;/code&gt; 被声明为 &lt;code&gt;mutable&lt;/code&gt;。但是，尝试修改 &lt;code&gt;value&lt;/code&gt; 的值会导致编译错误，因为 &lt;code&gt;value&lt;/code&gt; 不是 &lt;code&gt;mutable&lt;/code&gt; 的，且我们在一个 &lt;code&gt;const&lt;/code&gt; 成员函数中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 6 - 单选题&lt;/p&gt;
&lt;p&gt;关于构造函数和析构函数的区别，表述正确是&lt;/p&gt;
&lt;p&gt;A.它们具有不相同的函数名&lt;/p&gt;
&lt;p&gt;B.构造函数不会返回类型，而析构函数会&lt;/p&gt;
&lt;p&gt;C.构造函数允许传入函数参数，而析构函数不能&lt;/p&gt;
&lt;p&gt;D.构造函数不允许传入函数参数，而析构函数能&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：A: 根据网上，是同名的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 7 - 单选题&lt;/p&gt;
&lt;p&gt;下列哪个选项是程序的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;iostream&gt;

using namespace std;

class Foo {
public:
    int x;
    int y;
    Foo() {
        x = 1;
        y = 1;
    }
    Foo(int x_ = 10, int y_ = 10) {
        x = x_;
        y = y_;
    }

    void p() {
        int x = 20;  // local variable
        cout &amp;#x3C;&amp;#x3C; &quot;x is &quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; &quot; &quot;;
        cout &amp;#x3C;&amp;#x3C; &quot;y is &quot; &amp;#x3C;&amp;#x3C; y &amp;#x3C;&amp;#x3C; endl;
    }
};
int main() {
    Foo foo;  //&quot;message&quot;: &quot;类 \&quot;Foo\&quot; 包含多个默认构造函数&quot;,
    cout &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; y &amp;#x3C;&amp;#x3C; endl; // x was not declare in this scope
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.1 1&lt;/p&gt;
&lt;p&gt;B.10 10&lt;/p&gt;
&lt;p&gt;C.编译错误&lt;/p&gt;
&lt;p&gt;D.无法预期的值&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 10 - 单选题&lt;/p&gt;
&lt;p&gt;下列c++代码的输出是什么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &amp;#x3C;iostream&gt;  
using namespace std;
class A{
	A(){
		cout&amp;#x3C;&amp;#x3C;&quot;Constructor called&quot;;
	}
};
int main(int argc, char const *argv[]){
	A a;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.Constructor called&lt;/p&gt;
&lt;p&gt;B.程序没有输出&lt;/p&gt;
&lt;p&gt;C.编译错误&lt;/p&gt;
&lt;p&gt;D.Segmentation fault&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：一定要小心，默认是private&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 20 - 单选题&lt;/p&gt;
&lt;p&gt;下列c++代码的输出是什么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include&amp;#x3C;iostream&gt;
using namespace std;
 
class Test{
private:
  int x;
  int y;
public:
  Test(int x = 0, int y = 0) { this-&gt;x = x; this-&gt;y = y; }
  static void fun1() { cout &amp;#x3C;&amp;#x3C; &quot;Inside fun1()&quot;; }
  static void fun2() { cout &amp;#x3C;&amp;#x3C; &quot;Inside fun2()&quot;; this-&gt;fun1(); }
};
 
int main(){
  Test obj;
  obj.fun2();
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.Inside fun2() Inside fun1()&lt;/p&gt;
&lt;p&gt;B.Inside fun2()&lt;/p&gt;
&lt;p&gt;C.Inside fun1() Inside fun2()&lt;/p&gt;
&lt;p&gt;D.编译错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：小心！！static成员函数没有this指针&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第五周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 2 - 单选题&lt;/p&gt;
&lt;p&gt;下列说法正确的是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;iostream&gt;

#include &quot;Circle.h&quot;
using namespace std;
int main()
{
cout &amp;#x3C;&amp;#x3C; Circle(5).getArea() &amp;#x3C;&amp;#x3C; endl;
cout &amp;#x3C;&amp;#x3C; (new Circle(5))-&gt;getArea() &amp;#x3C;&amp;#x3C; endl;

return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.Circle(5).getArea() 编译出错&lt;/p&gt;
&lt;p&gt;B.new Circle(5).getArea() 编译出错&lt;/p&gt;
&lt;p&gt;C.该程序能够编译通过，但不能运行&lt;/p&gt;
&lt;p&gt;D.该程序能够通过编译，也能够运行，但 new Circle(5) 在堆上创建了匿名对象，这将导致内存泄漏。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 4 - 单选题&lt;/p&gt;
&lt;p&gt;删除空指针会发生什么？&lt;/p&gt;
&lt;p&gt;A.不会报错&lt;/p&gt;
&lt;p&gt;B.不会报错，但必须提前将空指针指定一个类型(例如&lt;code&gt;int * p = nullptr;&lt;/code&gt; &lt;code&gt;delete p;&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;C.编译错误&lt;/p&gt;
&lt;p&gt;D.运行时错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 7 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int* t = new int(5);
int* p = t;
delete t;
delete p;
cout &amp;#x3C;&amp;#x3C; *p &amp;#x3C;&amp;#x3C; endl;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该段代码的输出是&lt;/p&gt;
&lt;p&gt;A.不确定的脏值&lt;/p&gt;
&lt;p&gt;B.0&lt;/p&gt;
&lt;p&gt;C.5&lt;/p&gt;
&lt;p&gt;D.出现运行时错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：上一题是空指针nullptr，但是这里是有所指但是无法访达的指针，所以runtime error&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 6 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class A
{
public:
	int x;
	A() :x(0) { cout &amp;#x3C;&amp;#x3C; &quot;A::A() x=&quot; &amp;#x3C;&amp;#x3C;x&amp;#x3C;&amp;#x3C;endl; }
	~A() { cout &amp;#x3C;&amp;#x3C; &quot;A::~A() x=&quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; endl; }
};

int main()
{
	A* p = new A[3];
	for (int i = 0; i &amp;#x3C; 3; i++)
	{
		p[i].x = i;
	}
	delete []p;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果为？&lt;/p&gt;
&lt;p&gt;A.&lt;code&gt;A::A() x=0 &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A::A() x=0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; A::A() x=0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; A::~A() x=2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; A::~A() x=1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A::~A() x=0&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;就考了一件事情：析构顺序与构造顺序相反&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 20 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;A* arr[4]={new A(0),NULL,new A(0)};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该语句生成了几个A对象&lt;/p&gt;
&lt;p&gt;A.1&lt;/p&gt;
&lt;p&gt;B.2&lt;/p&gt;
&lt;p&gt;C.3&lt;/p&gt;
&lt;p&gt;D.4&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：上一题直接new了3个，这里只new了两个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 8 - 单选题&lt;/p&gt;
&lt;p&gt;以下程序段的执行结果是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class A{
	int x;
public:
	A() :x(0) { cout &amp;#x3C;&amp;#x3C; &quot;A::A()&quot; &amp;#x3C;&amp;#x3C; endl; }
	~A() { cout &amp;#x3C;&amp;#x3C; &quot;A::~A()&quot; &amp;#x3C;&amp;#x3C; endl; }
};

int main(){
	A* p = new A[10];
	p += 1;
	delete []p;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.编译不通过&lt;/p&gt;
&lt;p&gt;B.打印10个A::A(), 10个A::~A()&lt;/p&gt;
&lt;p&gt;C.打印10个A::A(), 9个A::~A()&lt;/p&gt;
&lt;p&gt;D.打印10个A::A()，运行异常终止&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：因为 &lt;code&gt;p&lt;/code&gt; 不再指向数组的起始地址，所以这里的行为是未定义的（Undefined Behavior, UB）。在大多数实现中，这会导致运行时异常终止，因为 &lt;code&gt;delete []&lt;/code&gt; 操作符期望一个指向数组第一个元素的指针，以便正确地释放整个数组的内存并调用每个对象的析构函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 13 - 不定项选择题&lt;/p&gt;
&lt;p&gt;在下列有关类、抽象、封装和数据隐藏的说法中，正确的有？&lt;/p&gt;
&lt;p&gt;A.类表示可以通过类方法的公共接口对类对象执行的操作；这是抽象。&lt;/p&gt;
&lt;p&gt;B.类隐藏了实现的细节，例如数据表示和方法代码；这就是封装。&lt;/p&gt;
&lt;p&gt;C.类可以对数据成员使用私有可见性，这意味着只能通过成员函数访问数据；这是数据隐藏。&lt;/p&gt;
&lt;p&gt;D.使用类是C++中可以轻松实现面向对象功能抽象、数据隐藏和封装的方式。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C, A, B, D&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第六周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 1 - 单选题&lt;/p&gt;
&lt;p&gt;**静态联编(static binding)**是指在运行阶段就能确定调用的动态对象的方法的技术。&lt;/p&gt;
&lt;p&gt;A.正确&lt;/p&gt;
&lt;p&gt;B.错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：也称为早期联编或编译时联编，是在编译阶段就确定了方法调用与其实现的对应关系。这通常发生在非虚方法（非多态方法）的调用中。&lt;/p&gt;
&lt;p&gt;在静态联编中，编译器在编译时会根据调用方法的对象的类型来确定要调用的方法。由于这种确定是在编译时进行的，因此它不会受到运行时对象实际类型的影响。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  //已经重写拷贝构造函数
  Test t1;
  Test t2 = t1;// 调用拷贝构造函数
  t2 = t1;	 // 只是调用=
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 3 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;A func()
{
	A tmp(1);
	return tmp;
}
int main()
{
	A a = func(); 
        A aa(a);
	return 0; 
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A是一个具有拷贝构造函数的类，如果&lt;strong&gt;考虑&lt;/strong&gt;编译优化，该段程序调用了几次拷贝构造函数。&lt;/p&gt;
&lt;p&gt;A.0&lt;/p&gt;
&lt;p&gt;B.1&lt;/p&gt;
&lt;p&gt;C.2&lt;/p&gt;
&lt;p&gt;D.3&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;func()&lt;/code&gt; 返回 &lt;code&gt;tmp&lt;/code&gt; 时，编译器可能会应用命名返回值优化（NRVO），直接在返回值的位置构造 &lt;code&gt;tmp&lt;/code&gt;，避免拷贝构造函数的调用。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;A a = func();&lt;/code&gt; 这行代码中，如果应用了（NRVO），则不会调用拷贝构造函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A aa(a);&lt;/code&gt; 这行代码会调用拷贝构造函数来初始化 &lt;code&gt;aa&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，在考虑编译器优化的情况下，这段程序最少可能调用拷贝构造函数&lt;strong&gt;1次&lt;/strong&gt;。这是因为编译器优化可以消除&lt;code&gt;func()&lt;/code&gt;返回时的拷贝构造函数调用，但&lt;code&gt;A aa(a);&lt;/code&gt;这行代码中的拷贝构造函数调用无法被优化掉。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 4 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;A func1(A a)
{
	return a;
}


A&amp;#x26; func2(A&amp;#x26; a)
{
	return a;
}



int main()
{
	A a(1); 
	func1(a); 
    func2(a);
	return 0; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A是一个具有拷贝构造函数的类，如果&lt;strong&gt;不考虑&lt;/strong&gt;编译优化，该段程序调用了几次拷贝构造函数，几次析构函数。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;2， 3     0,   1&lt;/p&gt;
&lt;p&gt;解释：注意，不考虑编译优化，返回的时候会先拷贝到一个新的临时对象上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 8 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double a = 3.14;
const int &amp;#x26; b = a;
a = 6.28;
cout&amp;#x3C;&amp;#x3C;a&amp;#x3C;&amp;#x3C;&quot;,&quot;&amp;#x3C;&amp;#x3C;b&amp;#x3C;&amp;#x3C;endl;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的输出是？&lt;/p&gt;
&lt;p&gt;A.6.28, 6&lt;/p&gt;
&lt;p&gt;B.6.28, 3&lt;/p&gt;
&lt;p&gt;C.6.28, 6.28&lt;/p&gt;
&lt;p&gt;D.6.28, 3.14&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;p&gt;解释：由于&lt;code&gt;const int &amp;#x26; b&lt;/code&gt;必须要指向一个&lt;code&gt;int&lt;/code&gt;的变量，所以第二行的时候会生成一个整型临时变量来给b指，所以a改变的时候，b不变&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 9 - 单选题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func(string&amp;#x26; s)
{
    cout &amp;#x3C;&amp;#x3C; s;
}

int main()
{
    func(&quot;123&quot;);
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该段代码的输出是&lt;/p&gt;
&lt;p&gt;A.编译不通过&lt;/p&gt;
&lt;p&gt;B.123&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;解释：这段代码不能编译通过。原因在于&lt;code&gt;func&lt;/code&gt;函数期望的参数是一个&lt;code&gt;string&amp;#x26;&lt;/code&gt;（非const引用），而在&lt;code&gt;main&lt;/code&gt;函数中调用&lt;code&gt;func(&quot;123&quot;);&lt;/code&gt;时，传递的是一个字符串字面量。字符串字面量是一个常量字符数组，它可以被隐式转换为&lt;code&gt;const string&lt;/code&gt;，但不能直接绑定到非const引用上。下面的代码同理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void func(int&amp;#x26; x){
    cout&amp;#x3C;&amp;#x3C;x&amp;#x3C;&amp;#x3C;endl;
}

int main(){
    int i = 1;
    func(i * 3);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;那反过来是不是就对了呢？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 14 - 不定项选择题&lt;/p&gt;
&lt;p&gt;关于const引用，以下说法正确的是&lt;/p&gt;
&lt;p&gt;A.const引用是一个比较万能的类型，可以初始化为左值，右值，类型转换的左值对象。&lt;/p&gt;
&lt;p&gt;B.把const引用作为函数参数，可以防止程序错误修改不该修改的实参。&lt;/p&gt;
&lt;p&gt;C.非const的引用可以初始化为右值&lt;/p&gt;
&lt;p&gt;D.把const引用作为函数参数，可以提高程序的运行效率&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D, B, A&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第八周&lt;/h1&gt;
&lt;p&gt;Question 1 - 不定项选择题&lt;/p&gt;
&lt;p&gt;若定义正常对象的形式为&lt;code&gt;A a(parameter);&lt;/code&gt;,那么定义常对象的形式为&lt;/p&gt;
&lt;p&gt;A.A const a(parameter);&lt;/p&gt;
&lt;p&gt;B.A a(parameter);&lt;/p&gt;
&lt;p&gt;C.A a(parameter) const;&lt;/p&gt;
&lt;p&gt;D.const A a(parameter);&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, D&lt;/p&gt;
&lt;p&gt;解析：注意！！AD等价&lt;/p&gt;
&lt;p&gt;Question 2 - 不定项选择题&lt;/p&gt;
&lt;p&gt;如果在一个类中声明以下4个重载函数，有哪两个是互相冲突的？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;Point fun1();  // 1
const Point fun1();  // 2
Point fun1() const;  // 3
const Point fun1() const;  // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.1、2&lt;/p&gt;
&lt;p&gt;B.1、3&lt;/p&gt;
&lt;p&gt;C.2、3&lt;/p&gt;
&lt;p&gt;D.3、4&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, D&lt;/p&gt;
&lt;p&gt;解析：&lt;/p&gt;
&lt;p&gt;前面那个&lt;code&gt;const&lt;/code&gt;只是表示返回值类型
括号里的&lt;code&gt;const&lt;/code&gt;表示&lt;code&gt;this指针&lt;/code&gt;的类型，限定&lt;code&gt;this指针&lt;/code&gt;的修改权限
分号前的&lt;code&gt;const&lt;/code&gt;表示所有成员的修改权限&lt;/p&gt;
&lt;p&gt;函数签名：名称，参数类型，不考虑返回值，也即是前面的&lt;code&gt;const&lt;/code&gt;不影响重载，后面反之&lt;/p&gt;
&lt;h1&gt;第九周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;下面关于继承说法不正确的是:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A.继承可以使用现有类的所有功能，并在无需重新编写原来类的情况下对这些功能进行扩展。&lt;/p&gt;
&lt;p&gt;B.继承体系中派生类应体现出与基类的不同。&lt;/p&gt;
&lt;p&gt;C.派生类对象一定比基类对象大。&lt;/p&gt;
&lt;p&gt;D.继承呈现了面向对象程序设计的层次结构，体现了由简单到复杂的认知过程。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解析：如果派生类没有新的成员变量，而且没有因为内存对齐而改变存储填充，派生类大小和基类相同；但是，如果派生类添加了新的成员变量或因为内存对齐而增加了额外的填充，那么派生类对象的大小就会比基类对象大。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;派生类析构函数的作用是什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A.释放派生类新增的资源&lt;/p&gt;
&lt;p&gt;B.释放基类的资源&lt;/p&gt;
&lt;p&gt;C.释放派生类和基类的所有资源&lt;/p&gt;
&lt;p&gt;D.不需要做任何操作&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;解析：派生类的构造函数和析构函数并不是释放派生类和基类的所有资源&lt;/p&gt;
&lt;p&gt;本质上都只是&lt;code&gt;构造/销毁 派生的变量 + 调用基类的构造/析构函数&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 8 - 单选题&lt;/p&gt;
&lt;p&gt;在派生类析构函数中，可以如何调用基类的析构函数？&lt;/p&gt;
&lt;p&gt;A.直接调用基类的析构函数&lt;/p&gt;
&lt;p&gt;B.使用基类的析构函数的名称作为成员初始化列表的一部分调用&lt;/p&gt;
&lt;p&gt;C.不需要调用，基类的析构函数会自动被调用&lt;/p&gt;
&lt;p&gt;D.只能调用公有基类的析构函数&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果派生类没有定义构造函数和析构函数，会发生什么？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A.编译错误&lt;/p&gt;
&lt;p&gt;B.调用基类的默认构造函数和析构函数&lt;/p&gt;
&lt;p&gt;C.调用派生类的默认构造函数和析构函数&lt;/p&gt;
&lt;p&gt;D.不做任何操作&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解析：调用默认构造/析构函数然后在这些默认函数里调用基类的函数&lt;/p&gt;
&lt;h1&gt;第十周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在C++中，重载（overloading）、覆盖（overriding）和隐藏（hiding）是三种不同的函数处理方式。它们在继承和多态性方面有着各自不同的作用和行为。以下是对这三者的详细解释和区别：&lt;/p&gt;
&lt;h3&gt;重载（Overloading）&lt;/h3&gt;
&lt;p&gt;重载指的是在同一个作用域中，定义多个具有相同名字但参数列表不同的函数。这些函数可以是普通函数，也可以是类成员函数。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Example {
public:
    void func(int x) {
        // 实现1
    }

    void func(double y) {
        // 实现2
    }

    void func(int x, double y) {
        // 实现3
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;func&lt;/code&gt;函数被重载了三次，每次都有不同的参数列表。编译器通过参数列表来区分这些函数。&lt;/p&gt;
&lt;h3&gt;覆盖（Overriding）&lt;/h3&gt;
&lt;p&gt;覆盖指的是在派生类中重新定义基类中已存在的虚函数。覆盖函数必须具有与基类中被覆盖函数相同的函数签名（参数列表和返回类型）。覆盖通常用于实现多态性。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    virtual void show() {
        // 基类实现
    }
};

class Derived : public Base {
public:
    void show() override {
        // 派生类实现
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;Derived&lt;/code&gt;类中的&lt;code&gt;show&lt;/code&gt;函数覆盖了&lt;code&gt;Base&lt;/code&gt;类中的&lt;code&gt;show&lt;/code&gt;函数。使用&lt;code&gt;override&lt;/code&gt;关键字可以显式地表示该函数是覆盖基类中的虚函数。&lt;/p&gt;
&lt;h3&gt;隐藏（Hiding）&lt;/h3&gt;
&lt;p&gt;隐藏指的是在派生类中定义一个与基类中同名的函数，这个函数可能具有不同的参数列表。此时，基类中所有同名的函数都会被隐藏，而不是被重载。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Base {
public:
    void func(int x) {
        // 基类实现
    }
};

class Derived : public Base {
public:
    void func(double y) {
        // 派生类实现
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;Derived&lt;/code&gt;类中的&lt;code&gt;func&lt;/code&gt;函数隐藏了&lt;code&gt;Base&lt;/code&gt;类中的&lt;code&gt;func&lt;/code&gt;函数。在派生类的对象上调用&lt;code&gt;func&lt;/code&gt;时，只有&lt;code&gt;Derived&lt;/code&gt;类中的&lt;code&gt;func&lt;/code&gt;函数是可见的。要调用基类的函数，可以使用作用域解析运算符。&lt;/p&gt;
&lt;h3&gt;区别总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重载&lt;/strong&gt;：在同一个作用域中，定义多个具有相同名字但参数列表不同的函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;覆盖&lt;/strong&gt;：在派生类中重新定义基类中的虚函数，必须具有相同的函数签名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏&lt;/strong&gt;：在派生类中定义一个与基类同名但参数列表不同的函数，这会隐藏基类中的所有同名函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理解这些概念对于正确使用C++中的继承和多态性非常重要。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于虚继承&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C++在设计虚继承机制时，会提供一个虚表（vtable）和一个虚指针（vptr）。虚指针指向虚表，虚表用于存放虚基类（祖父类）成员的地址。通过虚指针和虚表，可以在使用公共基类时快速找到正确的成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;菱形（钻石）继承的时候，编译器会默认调用父类的默认构造函数（记得提供），左右的类，不再调用父类的构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在类中，如果要使用父类的同名函数，要用域运算符: :&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;Human&amp;#x26; Human::operator=(const Human&amp;#x26; other) {
    Creature::operator=(other);
    // 基类的指针可以指向继承类——开头的结构是相同的
    delete [] languages;
    languages = new char[strlen(other.languages) + 1];
    strcpy(languages, other.languages);
    return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;virtual的范围只有上下两个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恢复访问方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;using  Base::data;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意是，恢复为原来的访问方式，不受继承的时候的protected和private影响‘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在派生类中显式调用基类构造函数的时候，如果有拷贝构造，可以直接把派生类对象作为参数，如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  Derived(const Derived &amp;#x26; other) : Base(other), z(other.z) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在C++中，当你在派生类的构造函数中显式调用基类的构造函数时，使用&lt;code&gt;Derived&lt;/code&gt;类的引用&lt;code&gt;other&lt;/code&gt;作为参数是合法的，原因如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;向上转型（Upcasting）&lt;/strong&gt;：在C++中，派生类的对象可以被隐式地转换（向上转型）为基类的引用或指针。这意味着，当你将派生类的引用或指针传递给需要基类引用或指针的函数或构造函数时，转换是自动进行的。这种转换是安全的，因为派生类对象包含了基类的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基类部分的初始化&lt;/strong&gt;：在派生类构造函数中显式调用基类构造函数是初始化派生类对象中基类部分的标准方式。通过将&lt;code&gt;Derived&lt;/code&gt;类的引用&lt;code&gt;other&lt;/code&gt;传递给基类的拷贝构造函数，你实际上是在告诉编译器：“请使用&lt;code&gt;other&lt;/code&gt;对象中的基类部分来初始化当前对象的基类部分。”这样做是必要的，因为基类可能有自己的成员变量需要根据&lt;code&gt;other&lt;/code&gt;对象的状态来初始化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持对象状态的一致性&lt;/strong&gt;：通过这种方式，你可以确保派生类对象的基类部分是通过基类的拷贝构造函数正确初始化的，这有助于保持对象状态的一致性，特别是当基类有自己的资源管理逻辑（如动态分配的内存）时&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第十一周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 1 - 单选题&lt;/p&gt;
&lt;p&gt;什么是多重继承？&lt;/p&gt;
&lt;p&gt;A.从派生类派生出基类&lt;/p&gt;
&lt;p&gt;B.从基类派生出派生类&lt;/p&gt;
&lt;p&gt;C.从多个基类派生出派生类&lt;/p&gt;
&lt;p&gt;D.派生出一个派生基类&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下面代码输出什么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;

class Base {
public:
    virtual void print() const = 0;
};

class DerivedOne : public Base {
public:
    void print() const {
        cout &amp;#x3C;&amp;#x3C; &quot;DerivedOne\n&quot;;
    }
};

class DerivedTwo : public Base {
public:
    void print() const {
        cout &amp;#x3C;&amp;#x3C; &quot;DerivedTwo\n&quot;;
    }
};

class Multiple : public DerivedOne, public DerivedTwo {
public:
    void print() const {
        DerivedTwo ::print();
    }
};

int main() {
    int i;
    Multiple both;
    DerivedOne one;
    DerivedTwo two;
    Base *array[3];
    array[0] = &amp;#x26;both; // error: 由于没有虚继承，基类的指向不明确
    array[1] = &amp;#x26;one;
    array[2] = &amp;#x26;two;
    array[i]-&gt;print();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 9 - 单选题&lt;/p&gt;
&lt;p&gt;从基类继承了哪些内容？&lt;/p&gt;
&lt;p&gt;A.构造函数及其析构函数&lt;/p&gt;
&lt;p&gt;B.Operator=() 成员&lt;/p&gt;
&lt;p&gt;C.友元&lt;/p&gt;
&lt;p&gt;D.以上所有均不是&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;在C++中，派生类从基类继承时，有几种成员和特性是不会被继承的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构造函数&lt;/strong&gt;：基类的构造函数不会被派生类继承。派生类需要定义自己的构造函数。如果需要，派生类的构造函数可以显式地调用基类的构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;析构函数&lt;/strong&gt;：基类的析构函数不会被派生类继承。派生类需要定义自己的析构函数。析构函数总是按照派生类到基类的顺序被调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拷贝构造函数和拷贝赋值运算符&lt;/strong&gt;：这些用于控制对象如何被复制的特殊成员函数不会被自动继承。派生类需要定义自己的拷贝构造函数和拷贝赋值运算符，如果需要的话，它们可以在其实现中调用基类的对应成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;友元函数&lt;/strong&gt;：友元函数是指定的外部函数，它可以访问类的所有私有和保护成员。友元关系不是继承的。如果派生类需要某个函数作为友元，需要在派生类中显式声明。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;私有成员&lt;/strong&gt;：基类的私有成员虽然被派生类继承，但是派生类不能直接访问它们。如果派生类需要访问基类的私有成员，可以通过基类提供的公共或保护的成员函数来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;默认参数的使用规则&lt;/strong&gt;：虽然派生类会继承基类的成员函数，但是成员函数的默认参数是静态绑定的，而不是动态绑定的。这意味着，如果通过基类的指针或引用调用一个继承自基类的函数，使用的默认参数值是基类中定义的值，而不是派生类中可能重新定义的值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些限制确保了对象的构造和析构、复制行为可以被适当地控制，并且保护了类的封装性，同时也避免了潜在的多态性相关的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 10 - 不定项选择题&lt;/p&gt;
&lt;p&gt;c++的类型兼容规则所指的替代包括以下情况:&lt;/p&gt;
&lt;p&gt;A.父类指针可以直接指向子类对象&lt;/p&gt;
&lt;p&gt;B.子类对象可以直接赋值给父类对象&lt;/p&gt;
&lt;p&gt;C.子类对象可以直接初始化父类对象&lt;/p&gt;
&lt;p&gt;D.将父类对象直接赋值给子类对象&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, B, C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 11 - 单选题&lt;/p&gt;
&lt;p&gt;在C++中，什么是虚拟继承？&lt;/p&gt;
&lt;p&gt;A.C++ 中增强多重继承的技术&lt;/p&gt;
&lt;p&gt;B.C++ 中确保基类的私有成员可以以某种方式被访问的技术&lt;/p&gt;
&lt;p&gt;C.C++ 中避免类的多重继承的技术&lt;/p&gt;
&lt;p&gt;D.C++ 中避免基类在子类/派生类中出现多个副本的技术&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 16 - 单选题&lt;/p&gt;
&lt;p&gt;下面叙述不正确的是&lt;/p&gt;
&lt;p&gt;A.在单一继承中，基类的构造函数不可被派生类直接继承或调用&lt;/p&gt;
&lt;p&gt;B.对基类成员的访问必须是无二义性的&lt;/p&gt;
&lt;p&gt;C.赋值兼容规则也适用于多重继承的组合&lt;/p&gt;
&lt;p&gt;D.基类的公有成员在派生类中仍然是公有的&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 20 - 不定项选择题&lt;/p&gt;
&lt;p&gt;哪种访问修饰符的成员会被继承？&lt;/p&gt;
&lt;p&gt;A.Public&lt;/p&gt;
&lt;p&gt;B.Protected&lt;/p&gt;
&lt;p&gt;C.Private&lt;/p&gt;
&lt;p&gt;D.以上都不会&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, B，C&lt;/p&gt;
&lt;p&gt;其实都能被继承的，只是访问权限上有不同&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;程序题出现的问题&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;/*
    private:
        char* sound;
        int age;
 */

//-----------------------------------------------------

/Creature::Creature(const char* _sound, int _age) : sound(_sound), age(_age) {} // error: invalid conversion from &apos;const char*&apos; to &apos;char*&apos; [-fpermissive]

Creature::Creature(const char* _sound, int _age) : age(_age) {
    sound = new char[strlen(_sound) + 1];
    strcpy(sound, _sound);
}	//correct

//-----------------------------------------------------

Human&amp;#x26; Human::operator=(const Human&amp;#x26; other) {
    if (this != &amp;#x26;other) {
        // this-&gt;Creature = other; // error: invalid use of &apos;Creature::Creature&apos;
        this-&gt;Creature::operator=(other); // 正确地调用基类的赋值运算符
        delete[] languages;
        languages = new char[strlen(other.languages) + 1];
        strcpy(languages, other.languages);
    }
    return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第十二周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 1 - 单选题&lt;/p&gt;
&lt;p&gt;在C++中，多态意味着什么？&lt;/p&gt;
&lt;p&gt;A.只具有单一形态的类&lt;/p&gt;
&lt;p&gt;B.编译时决定行为的类&lt;/p&gt;
&lt;p&gt;C.允许对象表现出多种行为形态的特性&lt;/p&gt;
&lt;p&gt;D.仅具备静态行为的类&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;概念题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;class Object {
private:
    int value;

public:
    Object(int x = 0) : value(x) {}
    void print() {
        cout &amp;#x3C;&amp;#x3C; &quot;Object::print&quot; &amp;#x3C;&amp;#x3C; endl;
        add(1);
    }
    virtual void add(int x) {
        cout &amp;#x3C;&amp;#x3C; &quot;Object::add&quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; endl;
    }
};

class Base : public Object {
private:
    int num;

public:
    Base(int x = 0) : Object(x + 10), num(x) {}
    void show() {
        cout &amp;#x3C;&amp;#x3C; &quot;Base::show&quot; &amp;#x3C;&amp;#x3C; endl;
        print();
    }
    virtual void add(int x) {
        cout &amp;#x3C;&amp;#x3C; &quot;Base::add:&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};
int main() {
    Base base;
    base.show();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在C++中，**构造函数和析构函数中调用虚函数不会发生多态**的原因是为了保证对象状态的安全和一致性。具体来说，有以下几个原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构造函数中的虚函数调用&lt;/strong&gt;：当构造函数被执行时，对象的派生类部分尚未被初始化。如果此时调用虚函数，并且该虚函数被派生类覆盖，那么该虚函数可能会操作派生类的成员变量，但这些成员变量此时还没有被初始化，这可能会导致不可预知的行为或错误。因此，在构造函数中，虚函数调用不会被动态绑定到派生类的实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;析构函数中的虚函数调用&lt;/strong&gt;：当析构函数被执行时，派生类的部分已经被销毁，对象被“降级”为其基类的状态。如果此时调用虚函数，并且该虚函数在派生类中有覆盖实现，那么调用派生类的实现就可能操作已经被销毁的成员变量，同样会导致不可预知的行为或错误。因此，在析构函数中，虚函数调用也不会被动态绑定到派生类的实现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种设计是为了确保在构造和析构过程中对象的完整性和一致性，避免在对象构造未完成或析构已开始时调用派生类的方法，这些方法可能会错误地操作尚未初始化或已经销毁的成员变量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第五题&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;class Object {
public:
    virtual void func(int a = 10) {
        cout &amp;#x3C;&amp;#x3C; &quot;Object::func:a&quot; &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; endl;
    }
};
//---------------------------------------------
class Base : public Object {
public:
    virtual void func(int b = 20) {
        cout &amp;#x3C;&amp;#x3C; &quot;Base::fun:b&quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; endl;
    }
};
//---------------------------------------------
int main() {
    Base base;
    Object* op = &amp;#x26;base;
    op-&gt;func();
    return 0;
}

//out put Base::fun:b10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个C++代码中，有几个关键点需要注意来解释为什么输出时&lt;code&gt;b&lt;/code&gt;等于10，而不是20。&lt;/p&gt;
&lt;p&gt;在C++中，当通过基类指针或引用来调用虚函数时，会发生动态绑定（或晚期绑定）。但是，这里的关键是==默认参数值是在编译时确定的==，基于函数声明的上下文。由于&lt;code&gt;op&lt;/code&gt;是&lt;code&gt;Object*&lt;/code&gt;类型，编译器只知道&lt;code&gt;Object&lt;/code&gt;类中的&lt;code&gt;func&lt;/code&gt;函数及其默认参数值（&lt;code&gt;a = 10&lt;/code&gt;），而不知道&lt;code&gt;Base&lt;/code&gt;类中的默认参数值（&lt;code&gt;b = 20&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;因此，当你执行&lt;code&gt;op-&gt;func();&lt;/code&gt;时，调用的是&lt;code&gt;Base&lt;/code&gt;类中的&lt;code&gt;func&lt;/code&gt;函数，但是使用了&lt;code&gt;Object&lt;/code&gt;类中定义的默认参数值10。这就是为什么输出是&quot;Base::func:a10&quot;。&lt;/p&gt;
&lt;p&gt;在C++中，当通过基类指针调用一个覆盖的虚函数时，会使用派生类的函数实现，这是多态的表现。然而，函数的**默认参数值是静态绑定**的，而不是动态绑定的。这意味着默认参数值的选择是根据指针或引用的静态类型来决定的，而不是对象的实际类型。&lt;/p&gt;
&lt;p&gt;在给定的代码中，[&lt;code&gt;op&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)是一个指向[&lt;code&gt;Base&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)对象的[&lt;code&gt;Object&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)类型指针。当调用[&lt;code&gt;op-&gt;func()&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)时，由于[&lt;code&gt;func&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)是一个虚函数，并且在[&lt;code&gt;Base&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)类中被覆盖，所以会调用[&lt;code&gt;Base&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)类中的[&lt;code&gt;func&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)实现。然而，因为[&lt;code&gt;func&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)的调用是通过一个类型为[&lt;code&gt;Object*&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)的指针[&lt;code&gt;op&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)进行的，所以使用的默认参数值是在[&lt;code&gt;Object&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)类中声明的[&lt;code&gt;func&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)函数的默认参数值&lt;code&gt;10&lt;/code&gt;，而不是[&lt;code&gt;Base&lt;/code&gt;](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)类中的默认参数值&lt;code&gt;20&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第8题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;==&lt;strong&gt;虚函数不能是静态成员函数&lt;/strong&gt;。==&lt;/p&gt;
&lt;p&gt;这是因为在C++中，虚函数和静态成员函数在语义和设计目的上存在根本的差异。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;虚函数&lt;/strong&gt;：虚函数主要用于实现动态多态性。当通过基类指针或引用调用虚函数时，实际调用的是指针或引用所指向的对象的实际类型（即派生类）中的虚函数版本（如果已经被重写）。这是通过虚函数表（&lt;code&gt;vtable&lt;/code&gt;）和虚指针（&lt;code&gt;vptr&lt;/code&gt;）实现的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态成员函数&lt;/strong&gt;：静态成员函数与类的一个特定实例无关，而是与类本身相关。它们不能访问类的非静态成员（因为它们不依赖于任何特定的对象实例），并且它们不能是虚函数。静态成员函数主要用于访问静态数据成员或执行与类相关的但与任何特定对象无关的操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于静态成员函数与类的特定实例无关，因此它们没有与对象的动态类型相关的概念。因此，将它们声明为虚函数是没有意义的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 10 - 不定项选择题&lt;/p&gt;
&lt;p&gt;以下哪些情况会导致虚函数表的创建？&lt;/p&gt;
&lt;p&gt;A.定义至少一个虚函数的类&lt;/p&gt;
&lt;p&gt;B.使用虚继承的类&lt;/p&gt;
&lt;p&gt;C.包含纯虚函数的类（抽象类）&lt;/p&gt;
&lt;p&gt;D.所有类都会自动创建虚函数表&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, C&lt;/p&gt;
&lt;p&gt;解释：好好区分虚继承的概念，虚继承是为了解决菱形继承的过程中出现的多个基类的问题，和虚函数表完全是两码事&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;13.以下哪些是实现运行时多态的方式？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A.函数重载&lt;/p&gt;
&lt;p&gt;B.虚函数&lt;/p&gt;
&lt;p&gt;C.重写（Override）&lt;/p&gt;
&lt;p&gt;D.模板函数&lt;/p&gt;
&lt;p&gt;---standard answer BC&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;p&gt;A. 函数重载（Overloading）虽然是实现多态的一种方式，但它实现的是编译时的多态性，而不是运行时的多态性。函数重载允许在同一个作用域内使用相同的函数名，但具有不同的参数列表（参数类型不同、参数个数不同或者二者都不同）。&lt;/p&gt;
&lt;p&gt;B. 虚函数（Virtual Functions）是实现运行时多态的关键机制。当基类中的成员函数被声明为虚函数时，它就可以在派生类中被重写（Override）。这样，通过基类指针或引用来调用虚函数时，就会根据指针或引用所指向的实际对象类型来调用相应的函数实现，从而实现运行时的多态性。&lt;/p&gt;
&lt;p&gt;C. 重写（Override）是子类对父类中虚函数的重新实现。通过重写，子类可以改变父类中虚函数的行为。这也是实现运行时多态的一种方式。&lt;/p&gt;
&lt;p&gt;D. 模板函数（Template Functions）主要用于实现泛型编程，它与运行时多态没有直接关系。模板函数在编译时根据提供的类型参数生成具体的函数实例，这属于**编译时的多态性**。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 14 - 不定项选择题&lt;/p&gt;
&lt;p&gt;关于动态类型，以下哪些说法是正确的？&lt;/p&gt;
&lt;p&gt;A.动态类型是在运行时确定的&lt;/p&gt;
&lt;p&gt;B.动态类型可以通过向下转型来改变&lt;/p&gt;
&lt;p&gt;C.动态类型决定了实际调用哪个函数版本&lt;/p&gt;
&lt;p&gt;D.动态类型与静态类型总是相同的&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, C&lt;/p&gt;
&lt;p&gt;解释：动态类型和静态类型的概念主要用于指针和引用的上下文中，尤其是在涉及到继承和多态时。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态类型&lt;/strong&gt;：是变量声明时的类型，或者说是编译时可知的类型。对于指针和引用，静态类型决定了你可以在该指针或引用上调用哪些成员函数，以及这些函数调用是如何被解析的（比如是否发生动态绑定）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动态类型&lt;/strong&gt;：是指针或引用实际指向的对象的类型。动态类型只有在运行时才能确定，它可能与静态类型相同，也可能是静态类型的派生类类型。在使用虚函数时，动态类型决定了哪个函数实现被调用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个概念是理解和实现多态性的关键。在非指针或引用的情况下，变量的类型在编译时是固定的，不存在动态类型的概念，因此通常不会讨论静态类型和动态类型。&lt;/p&gt;
&lt;p&gt;对于B：动态类型是指指针或引用所指向对象的实际类型。动态类型可以在运行时通过向下转型（downcasting）来“改变”，但实际上并不改变对象本身的类型，而是改变我们对该对象类型的解释或访问方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;17.以下哪种情况会发生动态类型转换？&lt;/p&gt;
&lt;p&gt;A.将基类对象赋值给派生类对象&lt;/p&gt;
&lt;p&gt;B.使用static_cast进行类型转换&lt;/p&gt;
&lt;p&gt;C.通过基类指针调用非虚成员函数&lt;/p&gt;
&lt;p&gt;D.显式地使用dynamic_cast进行转换&lt;/p&gt;
&lt;p&gt;---answer: D&lt;/p&gt;
&lt;p&gt;在C++中，关于动态类型转换的描述，我们来分析这些选项：&lt;/p&gt;
&lt;p&gt;A. 将基类对象赋值给派生类对象&lt;/p&gt;
&lt;p&gt;这是不允许的，因为基类对象通常不包含派生类可能添加的所有成员，因此无法直接将基类对象转换为派生类对象。这会导致编译错误。&lt;/p&gt;
&lt;p&gt;B. 使用static_cast进行类型转换&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static_cast&lt;/code&gt; 是一种编译时类型转换，它不会进行运行时检查。虽然它可以用于多种类型转换，包括基类和派生类之间的转换（当进行安全的上转型或明确知道转换是安全时），但它本身并不直接代表动态类型转换。&lt;/p&gt;
&lt;p&gt;C. 通过基类指针调用非虚成员函数&lt;/p&gt;
&lt;p&gt;通过基类指针调用非虚成员函数不会导致动态类型转换。这种调用将总是解析为基类中的函数版本，因为非虚函数是在编译时绑定的。&lt;/p&gt;
&lt;p&gt;D. 显式地使用dynamic_cast进行转换&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dynamic_cast&lt;/code&gt; 是C++中的一种类型转换运算符，它用于安全地执行运行时类型检查。它主要用于在类层次结构中进行向上转型（这通常是不必要的，因为自动转换就足够了）和向下转型（从基类指针或引用到派生类指针或引用）。当使用 &lt;code&gt;dynamic_cast&lt;/code&gt; 进行向下转型时，如果转换不安全（即基类指针不指向派生类对象），它将返回空指针（对于指针类型）或抛出异常（对于引用类型）。因此，这是动态类型转换的一个例子。&lt;/p&gt;
&lt;p&gt;所以，正确答案是 D：显式地使用dynamic_cast进行转换。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 20 - 不定项选择题&lt;/p&gt;
&lt;p&gt;下列关于函数重载的规则，哪些是正确的？&lt;/p&gt;
&lt;p&gt;A.参数类型或个数不同&lt;/p&gt;
&lt;p&gt;B.函数返回类型必须相同&lt;/p&gt;
&lt;p&gt;C.函数名称必须相同&lt;/p&gt;
&lt;p&gt;D.参数名称可以不同&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, C, D&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;补充&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;虚表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;from  &lt;a href=&quot;https://zhuanlan.zhihu.com/p/75172640&quot;&gt;C++ 虚函数表剖析 - 知乎 (zhihu.com)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚表是一个指针数组，其元素是虚函数的指针，每个元素对应一个虚函数的函数指针。需要指出的是，普通的函数即非虚函数，其调用并不需要经过虚表，所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目，即虚函数指针的赋值发生在&lt;strong&gt;编译器的编译阶段&lt;/strong&gt;，也就是说在代码的编译阶段，虚表就可以构造出来了。（解释上面的第五题）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚表是属于类的，而不是属于某个具体的对象，一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了指定对象的虚表，对象内部包含一个虚表的指针，来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针，编译器在类中添加了一个指针，&lt;code&gt;*__vptr&lt;/code&gt;，用来指向虚表。这样，当类的对象在创建时便拥有了这个指针，且这个指针的值会自动被设置为指向类的虚表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个继承类的基类如果包含虚函数，那个这个继承类也有拥有自己的虚表，故这个继承类的对象也包含一个虚表指针，用来指向它的虚表。（解释一旦为虚，永远为虚）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;==对象的虚表指针用来指向自己所属类的虚表，虚表中的指针会指向其继承的最近的一个类的虚函数==&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非虚函数的调用不用经过虚表，故不需要虚表中的指针指向这些函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;int main() 
{
    B bObject;
    A *p = &amp;#x26; bObject;
    p-&gt;vfun1();
}

//虽然p是基类的指针只能指向基类的部分，但是虚表指针亦属于基类部分，所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表，所以p可以访问到B vtbl.

//程序在执行p-&gt;vfunc1()时，会发现p是个指针，且调用的函数是虚函数，接下来便会进行以下的步骤。

//首先，根据虚表指针p-&gt;__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型，但是*__vptr也是基类的一部分，所以可以通过p-&gt;__vptr可以访问到对象对应的虚表。

//然后，在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了，所以可以根据所调用的函数定位到虚表中的对应条目。对于p-&gt;vfunc1()的调用，B vtbl的第一项即是vfunc1对应的条目。

//最后，根据虚表中找到的函数指针，调用函数。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们把经过虚表调用虚函数的过程称为动态绑定，其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用，传统的函数调用我们称之为静态绑定，即函数的调用在编译阶段就可以确定下来了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当对象指针被向上转型为基类指针时，如果没有涉及到多态（即没有使用虚函数机制），&lt;strong&gt;调用的是基类的函数&lt;/strong&gt;，原因在于编译时的静态类型决定了可以调用哪些成员函数。这种行为是基于静态绑定的，意味着函数调用在编译时就已经确定了，而不是在运行时。&lt;/p&gt;
&lt;p&gt;在没有多态的情况下，即使派生类隐藏了基类中的同名函数，当通过基类指针调用函数时，编译器只能看到指针的静态类型（即基类），因此它会调用基类中的函数。这是因为在编译时，编译器只能根据指针的类型（而不是指针所指向的对象的实际类型）来解析函数调用。&lt;/p&gt;
&lt;p&gt;多态通过虚函数实现，它允许在运行时根据对象的动态类型来决定调用哪个函数。当基类中的函数被声明为 &lt;code&gt;virtual&lt;/code&gt;，并且派生类提供了一个覆写版本时，通过基类指针调用该函数将会根据指针所指向的对象的实际类型来调用相应的函数实现，这是动态绑定的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向上转型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向上转型&lt;/strong&gt;是将派生类（或子类）的引用赋值给基类（或父类）的引用。这种转型是安全的，因为派生类是基类的一个特殊化版本，所以基类引用可以安全地引用派生类对象。在向上转型过程中，不需要进行显式转换，因为编译器会自动进行这种转换。&lt;/li&gt;
&lt;li&gt;收窄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果派生类中的函数与基类中的函数同名，不管参数列表是否相同，都会导致基类中同名函数的隐藏。这种现象在 C++ 中被称为“隐藏”（不是“覆写”或 overwrite，覆写一词通常用于虚函数的上下文）。隐藏发生在派生类中声明了一个与基类中某个函数同名的函数时，无论这两个函数的参数列表是否相同，基类中的所有同名函数都将在派生类的作用域中被隐藏。&lt;/p&gt;
&lt;p&gt;这意味着，如果你想在派生类中调用被隐藏的基类函数，你需要在派生类中显式地引用它们，通常是通过使用基类的作用域解析运算符（&lt;code&gt;::&lt;/code&gt;）来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;static&lt;/h3&gt;
&lt;p&gt;**静态成员变量是**C++中类的一个特殊特性，它不属于任何一个类实例，而是属于类本身。以下是使用静态成员变量时需要注意的一些关键事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定义与声明&lt;/strong&gt;：静态成员变量==必须在类定义之外进行定义和初始化==。在类定义中，你只能声明静态成员变量，而不能定义它。例如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MyClass {
public:
    static int myStaticVar;  // 声明
};

int MyClass::myStaticVar = 0;  // 定义和初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;访问方式&lt;/strong&gt;：你可以通过类名直接访问静态成员变量，而无需创建类的实例。同时，你也可以通过类的对象访问静态成员变量，但这不是推荐的做法，因为这可能会导致混淆。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;MyClass::myStaticVar = 10;  // 通过类名访问
MyClass obj;
obj.myStaticVar = 20;  // 通过对象访问，不推荐
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;初始化顺序&lt;/strong&gt;：静态成员变量的初始化顺序是按照它们在文件中出现的顺序进行的，而不是按照它们在类定义中的顺序。因此，如果静态成员变量的初始化依赖于其他静态成员变量或全局变量，那么你需要特别小心以确保正确的初始化顺序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期&lt;/strong&gt;：静态成员变量的生命周期与整个程序的生命周期相同。它们在程序开始执行时创建，在程序结束时销毁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程安全&lt;/strong&gt;：在多线程环境中，对静态成员变量的访问需要特别注意线程安全。如果没有适当的同步机制，多个线程可能同时修改静态成员变量，导致数据不一致或其他问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理&lt;/strong&gt;：静态成员变量存储在程序的静态存储区，而不是堆或栈上。因此，你不需要（也不能）使用&lt;code&gt;new&lt;/code&gt;或&lt;code&gt;delete&lt;/code&gt;来管理它们的内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承&lt;/strong&gt;：如果基类有静态成员变量，那么派生类不会继承这个静态成员变量。每个类都有自己的静态成员变量。但是，通过派生类的名字也可以访问基类的静态成员变量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**静态成员函数**是C++类中的一种特殊函数，它属于类本身而不是类的实例对象。以下是关于静态成员函数的一些主要特点和注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;声明与定义&lt;/strong&gt;：静态成员函数在类内部声明时，使用&lt;code&gt;static&lt;/code&gt;关键字进行修饰。其定义（实现）通常在类外部完成，且不需要&lt;code&gt;static&lt;/code&gt;关键字。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问方式&lt;/strong&gt;：静态成员函数可以通过类名直接调用，而不需要创建类的实例。这使得静态成员函数成为执行与类本身相关的操作而非特定于某个对象的操作的理想选择。同时，静态成员函数不能访问类的非静态成员变量和非静态成员函数，因为它们需要依赖于特定的对象实例。但是，静态成员函数可以访问静态成员变量和静态成员函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：静态成员函数通常用于执行与类相关但不依赖于对象实例的操作。例如，它们可以用于计算类的静态成员变量的值，或者执行一些全局性的操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程安全&lt;/strong&gt;：在多线程环境中，对静态成员函数的访问需要特别注意线程安全。如果没有适当的同步机制，多个线程可能同时调用静态成员函数，导致数据竞争或其他问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承&lt;/strong&gt;：如果基类有静态成员函数，派生类可以访问基类的静态成员函数。这是因为静态成员函数属于类本身，而不是类的实例。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请注意，虽然静态成员函数在类定义中声明时使用了&lt;code&gt;static&lt;/code&gt;关键字，但在其定义（实现）时==不需要再次使用&lt;code&gt;static&lt;/code&gt;关键字==。此外，静态成员函数由于没有隐式的&lt;code&gt;this&lt;/code&gt;指针，因此不能访问类的非静态成员。&lt;/p&gt;
&lt;p&gt;总的来说，静态成员函数是C++类的一个强大工具，可以在不需要特定对象实例的情况下执行与类相关的操作。然而，在使用它们时，需要注意它们的一些限制和特性，以确保代码的正确性和安全性。&lt;/p&gt;
&lt;p&gt;#第十三周&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 3 - 单选题&lt;/p&gt;
&lt;p&gt;以下关于虚函数和多态的说法中错误的是：&lt;/p&gt;
&lt;p&gt;A.只要基类的函数被声明为虚函数，则派生类的同名函数就能自动实现对基类函数的覆盖（override）。&lt;/p&gt;
&lt;p&gt;B.一般不建议在派生类中把从基类继承来的非虚函数声明为虚函数。&lt;/p&gt;
&lt;p&gt;C.当基类的某个成员函数在派生类中被隐藏（overwrite）时，程序将根据引用或指针的类型选择函数方法。&lt;/p&gt;
&lt;p&gt;D.关键字virtual只用于类声明的函数原型中，而不会用于类外的函数方法定义中。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;这句话的错误之处在于它忽略了函数签名的匹配要求和&lt;code&gt;override&lt;/code&gt;关键字的作用。要正确实现对基类虚函数的覆盖（override），派生类中的函数不仅需要与基类中的虚函数同名，还必须具有相同的参数列表（包括参数类型和数量）和兼容的返回类型。此外，C++11引入了&lt;code&gt;override&lt;/code&gt;关键字，虽然它不是必需的，但使用它可以让编译器帮助检查派生类的函数确实覆盖了基类的一个虚函数。&lt;/p&gt;
&lt;p&gt;因此，更准确的表述应该是：&lt;/p&gt;
&lt;p&gt;只要基类的函数被声明为虚函数，且派生类中有一个同名函数，该函数具有相同的参数列表和兼容的返回类型，则派生类的这个函数就能实现对基类函数的覆盖。为了确保这种覆盖是有意为之，可以在派生类的函数声明中使用&lt;code&gt;override&lt;/code&gt;关键字，这样如果没有正确覆盖基类的虚函数，编译器将报错。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 5 - 不定项选择题&lt;/p&gt;
&lt;p&gt;以下关于up/downcasting的说法正确的有：&lt;/p&gt;
&lt;p&gt;A.upcasting时必须显式转换；&lt;/p&gt;
&lt;p&gt;B.upcasting显隐式转换均合法；&lt;/p&gt;
&lt;p&gt;C.downcasting时必须显式转换；&lt;/p&gt;
&lt;p&gt;D.downcasting显隐式转换均合法；&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B, C&lt;/p&gt;
&lt;p&gt;解释：
B.将派生类指针(或引用)转换为基类指针(或引用)的过程称为upcasting&lt;/p&gt;
&lt;p&gt;C.将基类指针(或引用)转换为派生类指针(或引用)的过程称为downcasting&lt;/p&gt;
&lt;p&gt;往下是危险的，所以要用dynamic_cast保护&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不能声明为虚函数的函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1）普通函数。普通函数不属于成员函数，是不能被继承的。普通函数只能被重载，不能被重写，因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2）友元函数。友元函数不属于类的成员函数，不能被继承。对于没有继承特性的函数没有虚函数的说法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3）构造函数。假如子类可以继承基类构造函数，那么子类对象的构造将使用基类的构造函数，而基类构造函数并不知道子类的有什么成员，显然是不符合语义的。从另外一个角度来讲，多态是通过基类指针指向子类对象来实现多态的，在对象构造之前并没有对象产生，因此无法使用多态特性，这是矛盾的。因此构造函数不允许继承。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4）内联成员函数。我们需要知道内联函数就是为了在代码中直接展开，减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态，是在运行时绑定的。因此显然内联函数和多态的特性相违背。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5）静态成员函数。首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的，无法动态绑定，不支持多态，因此不能被重写，也就不能被声明为虚函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 3 - 单选题&lt;/p&gt;
&lt;p&gt;以下关于虚函数和多态的说法中错误的是：&lt;/p&gt;
&lt;p&gt;A.只要基类的函数被声明为虚函数，则派生类的同名函数就能自动实现对基类函数的覆盖（override）。&lt;/p&gt;
&lt;p&gt;B.一般不建议在派生类中把从基类继承来的非虚函数声明为虚函数。&lt;/p&gt;
&lt;p&gt;C.当基类的某个成员函数在派生类中被隐藏（overwrite）时，程序将根据引用或指针的类型选择函数方法。&lt;/p&gt;
&lt;p&gt;D.关键字virtual只用于类声明的函数原型中，而不会用于类外的函数方法定义中。&lt;/p&gt;
&lt;p&gt;答案：A&lt;/p&gt;
&lt;p&gt;选项分析：&lt;/p&gt;
&lt;p&gt;A. 只要基类的函数被声明为虚函数，则派生类的同名函数就能自动实现对基类函数的覆盖（override）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个说法有误。虽然基类函数被声明为虚函数允许派生类进行覆盖，但并不是派生类的同名函数就能“自动”实现对基类函数的覆盖。派生类中的函数需要有相同的函数签名（包括返回类型、函数名和参数列表），并且需要使用 &lt;code&gt;override&lt;/code&gt; 关键字（在C++11及以后的版本中）来显式地表明这是一个覆盖基类的虚函数。所以，A 选项是错误的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;B. 一般不建议在派生类中把从基类继承来的非虚函数声明为虚函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个说法通常被认为是正确的。如果基类中的函数不是虚函数，而在派生类中将其声明为虚函数，这可能会导致一些设计上的问题，比如切片问题等。通常，如果一个函数在基类中应当是虚函数，那么它应该在基类中就被声明为虚函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C. 当基类的某个成员函数在派生类中被隐藏（overwrite）时，程序将根据引用或指针的类型选择函数方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个说法是正确的。当派生类中的函数与基类中的函数同名但签名不同，或者基类中的函数没有被声明为虚函数时，基类的函数将被隐藏。在这种情况下，程序将根据对象的静态类型（即引用或指针的类型）来选择调用的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;D. 关键字virtual只用于类声明的函数原型中，而不会用于类外的函数方法定义中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个说法是正确的。&lt;code&gt;virtual&lt;/code&gt; 关键字只在类内部声明虚函数时使用，不需要在类外部的函数定义中重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 5 - 不定项选择题&lt;/p&gt;
&lt;p&gt;以下关于up/downcasting的说法正确的有：&lt;/p&gt;
&lt;p&gt;A.upcasting时必须显式转换；&lt;/p&gt;
&lt;p&gt;B.upcasting显隐式转换均合法；&lt;/p&gt;
&lt;p&gt;C.downcasting时必须显式转换；&lt;/p&gt;
&lt;p&gt;D.downcasting显隐式转换均合法；&lt;/p&gt;
&lt;p&gt;答案：BC&lt;/p&gt;
&lt;p&gt;收窄安全，隐式即可，无强制要求显式；拓宽危险，需要显式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

using namespace std;

class Cat {
public:
    // three overloaded functions
    virtual void func() const {
        cout &amp;#x3C;&amp;#x3C; &quot;func default&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    virtual void func(int a) const {
        cout &amp;#x3C;&amp;#x3C; &quot;func with int&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    virtual void func(double x) const {
        cout &amp;#x3C;&amp;#x3C; &quot;func with double&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    virtual void func(string str) const {
        cout &amp;#x3C;&amp;#x3C; &quot;func with string&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

class persianCat : public Cat {
public:
    // new redefined functions
    // 有同名函数，不管参数是否相同，都隐藏了基类的函数

    // 同名且参数相同，重写了基类的无参函数
    virtual void func() const {
        cout &amp;#x3C;&amp;#x3C; &quot;new func default&quot; &amp;#x3C;&amp;#x3C; endl;
    }

    // 同名且参数相同，重写了基类的int函数
    virtual void func(int a_) const {
        cout &amp;#x3C;&amp;#x3C; &quot;new func with int&quot; &amp;#x3C;&amp;#x3C; endl;
    }
};

int main() {
    Cat bai;
    persianCat hei;

    //--------------------------------------------------------

    Cat *c1 = &amp;#x26;hei;
    persianCat *c2 = &amp;#x26;hei;
    string s(&quot;hello&quot;);
    // hei.func(s);  // error:找不到这个函数，被隐藏了，强制类型转换又不行（string-&gt;int）
    // c2-&gt;func(s);  // error:
    c1-&gt;func(s);  // correct，输出func with string
    c1-&gt;func(2);  // correct, 输出func with int
    /*
    Q:为什么派生对象和派生类指针找不到这个函数？是因为隐藏吗
    A:是的，派生类同名函数隐藏了基类函数

    Q:为什么基类指针就可以调用
    A:基类指针调用的是基类的函数，基类的函数在基类指针的解释下都是可以找到的，再根据动态类型来调用派生类的函数
    */

    //-------------------------------------------------------

    // object of base class
    cout &amp;#x3C;&amp;#x3C; &quot;12&quot; &amp;#x3C;&amp;#x3C; endl;
    bai.func(2.1);  // 输出func with double，基类成员调用基类函数

    //-------------------------------------------------------

    cout &amp;#x3C;&amp;#x3C; &quot;13&quot; &amp;#x3C;&amp;#x3C; endl;
    hei.func(2.1);  // 输出func with new int，由于隐藏，无法调用基类的int函数，只能强转之后调用派生类的int函数
    // 因为基类的虚表已经被隐藏，只有在派生类中重定义了的函数才会出现在派生类的虚表里面。既然找不到函数，只好强制类型转换后调用int的函数了。

    //-------------------------------------------------------

    // hei.func(&quot;adsasd&quot;);
    cout &amp;#x3C;&amp;#x3C; &quot;14&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;error&quot; &amp;#x3C;&amp;#x3C; endl;

    //-------------------------------------------------------

    // pointer to base class
    cout &amp;#x3C;&amp;#x3C; &quot;15&quot; &amp;#x3C;&amp;#x3C; endl;
    Cat *p2Cat = &amp;#x26;bai;  // 基类指针指向基类对象
    p2Cat-&gt;func();      // 输出func default
    p2Cat-&gt;func(2);     // 输出func with int
    p2Cat-&gt;func(2.1);   // 输出func with double
    // 这仨都是基类指针指向基类对象，动态类型也是基类的，所以都是调用基类的函数

    //-------------------------------------------------------

    // pointer to base class
    cout &amp;#x3C;&amp;#x3C; &quot;16&quot; &amp;#x3C;&amp;#x3C; endl;
    // Cat *p2Cat
    p2Cat = &amp;#x26;hei;      // 基类指针指向派生类对象
    p2Cat-&gt;func();     // 输出new func default
    p2Cat-&gt;func(2);    // 输出new func with int
    p2Cat-&gt;func(2.1);  // 输出func with double
    // 基类指针指向派生类对象，从基类函数中找匹配参数的函数
    // 如果有，看看有无重写，有则调用派生类的函数，没有则调用基类的函数

    //----------------------------------------------------

    // pointer to derived class
    cout &amp;#x3C;&amp;#x3C; &quot;17&quot; &amp;#x3C;&amp;#x3C; endl;
    persianCat *p2persianCat = &amp;#x26;hei;  // 派生类指针指向派生类对象
    p2persianCat-&gt;func();             // 输出new func default
    p2persianCat-&gt;func(2);            // 输出new func with int
    p2persianCat-&gt;func(2.1);          // 输出nfunc with int
    // 派生类指针指向派生类对象，基类函数已经被隐藏了，直接调用派生类的函数
    // 如果没有完全匹配的，尝试类型转换

    //---------------------------------------------------

    // reference to base class
    cout &amp;#x3C;&amp;#x3C; &quot;18&quot; &amp;#x3C;&amp;#x3C; endl;
    Cat &amp;#x26;ref2Cat = bai;  // 基类引用指向基类对象
    ref2Cat.func();      // 输出func default
    ref2Cat.func(2);     // 输出func with int
    ref2Cat.func(2.1);   // 输出func with double
    // 基类引用指向基类对象，动态类型也是基类的，所以都是调用基类的函数

    //----------------------------------------------------

    // reference to base class
    cout &amp;#x3C;&amp;#x3C; &quot;19&quot; &amp;#x3C;&amp;#x3C; endl;
    Cat &amp;#x26;ref3Cat = hei;  // 基类引用指向派生类对象
    ref3Cat.func();      // 输出new func default
    ref3Cat.func(2);     // 输出new func with int
    ref3Cat.func(2.1);   // 输出func with double
    // 基类引用指向派生类对象，从基类函数中找匹配参数的函数
    // 找到了，看看有无重写，有则调用派生类的函数，没有则调用基类的函数

    //----------------------------------------------------

    // reference to derived class
    cout &amp;#x3C;&amp;#x3C; &quot;20&quot; &amp;#x3C;&amp;#x3C; endl;
    persianCat &amp;#x26;r2persianCat = hei;  // 派生类引用指向派生类对象
    r2persianCat.func();             // 输出new func default
    r2persianCat.func(2);            // 输出new func with int
    r2persianCat.func(2.1);          // 输出new func with int
    // 派生类引用指向派生类对象，基类函数已经被隐藏了，直接调用派生类的函数
    // 如果没有完全匹配的，尝试类型转换

    system(&quot;pause&quot;);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;13周课堂1，要输出类名，注意函数，只有基类或者次基类要用virtual，然后调用的函数，接受的形参必须是引用或者指针&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第十四周（模板）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Question 2 - 单选题&lt;/p&gt;
&lt;p&gt;模板函数的真正代码是在哪个时期产生的？&lt;/p&gt;
&lt;p&gt;A.源程序中声明函数时&lt;/p&gt;
&lt;p&gt;B.源程序中定义函数时&lt;/p&gt;
&lt;p&gt;C.源程序中调用函数时&lt;/p&gt;
&lt;p&gt;D.运行执行函数时&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：编译器的“调用”过程实际上是一个代码生成过程，而不是像程序运行时那样的函数执行过程。编译器通过分析源代码中的模板函数调用，确定需要生成的具体函数版本。这个过程包括确定模板参数的具体类型，并根据这些类型生成相应的函数实例代码。这一切都发生在编译时，而非运行时。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 3 - 单选题&lt;/p&gt;
&lt;p&gt;下列关于模板的描述中，错误的是？&lt;/p&gt;
&lt;p&gt;A.模板把数据类型作为一个设计参数，称为参数化程序设计&lt;/p&gt;
&lt;p&gt;B.使用时，模板参数与函数参数相同，是按位置而不是名称对应的&lt;/p&gt;
&lt;p&gt;C.模板参数表中可以有类型参数和非类型参数&lt;/p&gt;
&lt;p&gt;D.类模板与模板类是同一个概念&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;p&gt;解释：类模板是一个蓝图，不是一个具体的类，但是模板类是一个具体的类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 10 - 单选题&lt;/p&gt;
&lt;p&gt;关于类模板，描述错误的是?&lt;/p&gt;
&lt;p&gt;A.一个普通基类不能派生类模板&lt;/p&gt;
&lt;p&gt;B.类模板可以从普通类派生，也可以从类模板派生&lt;/p&gt;
&lt;p&gt;C.根据建立对象时的实际数据类型，编译器把类模板实例化为模板类&lt;/p&gt;
&lt;p&gt;D.函数的类模板参数需生成模板类并通过构造函数实例化&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 16 - 不定项选择题&lt;/p&gt;
&lt;p&gt;根据如下类模板定义，下列语句中可正常运行的有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;template&amp;#x3C;typename T&gt;
class Point{
    T x,y;
public:
    Point(T x_, T y_):x(x_),y(y_){}
};

template&amp;#x3C;typename T&gt;
class Circle : public Point&amp;#x3C;T&gt;{
    T r;
public:
    Circle(T x_, T y_, T r_):Point&amp;#x3C;T&gt;(x_,y_),r(r_){}
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A.&lt;code&gt;Circle&amp;#x3C;double&gt;* a = new Circle&amp;#x3C;int&gt;(4,5,6); &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.&lt;code&gt;Point&amp;#x3C;int&gt;* a = new Circle&amp;#x3C;int&gt;(4,5,6); &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;C.&lt;code&gt;Circle&amp;#x3C;int&gt; a(4,5,6);  Circle&amp;#x3C;double&gt; b(1,2,3);  a=b; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;D.&lt;code&gt;Circle&amp;#x3C;int&gt; a(1,2,2.5);  Point&amp;#x3C;int&gt; b(1,2);  b=a; &lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B, D&lt;/p&gt;
&lt;p&gt;解释：这个语句试图将 &lt;code&gt;Circle&amp;#x3C;int&gt;&lt;/code&gt; 类型的对象赋值给一个指向 &lt;code&gt;Circle&amp;#x3C;double&gt;&lt;/code&gt; 类型的指针。这是类型不匹配的，因为模板参数 &lt;code&gt;T&lt;/code&gt; 在两边不一致，导致类型不兼容。因此，这个语句不能正常运行。&lt;/p&gt;
&lt;p&gt;一旦实例化，就是不同类的了，如果将不同类非继承关系的指针之间进行直接赋值，是错的，而D选项里，基类和派生类都是int的，因而父类指针可以指向派生类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Question 17 - 单选题&lt;/p&gt;
&lt;p&gt;下述关于函数模板和类模板的说法中错误的是：&lt;/p&gt;
&lt;p&gt;A.调用函数模板时，编译器允许根据函数调用中所给出的实参类型来确定相应的模板实参&lt;/p&gt;
&lt;p&gt;B.调用函数模板时允许显示指定模板实参&lt;/p&gt;
&lt;p&gt;C.实例化类模板时，编译器允许不显式提供模板实参而根据构造函数的实参类型来推断模板实参&lt;/p&gt;
&lt;p&gt;D.实例化类模板时，必须显式地提供模板实参&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：对于类模板，当前的 C++ 标准（包括 C++17）要求在实例化类模板时必须显式提供模板实参。这意味着编译器不会尝试根据构造函数的实参类型来推断模板实参。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Question 15 - 单选题&lt;/p&gt;
&lt;p&gt;对模板进行实例化时，传递给非类型形参的实参可以是：&lt;/p&gt;
&lt;p&gt;A.动态对象&lt;/p&gt;
&lt;p&gt;B.局部变量&lt;/p&gt;
&lt;p&gt;C.非const的全局变量&lt;/p&gt;
&lt;p&gt;D.编译时常量表达式&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;ans: D
由于模板实例化发生在编译期，所以必须传递编译器常量才能识别，也就是说尖括号里面的内容必须是一个常量表达式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数模板调用的是同一个函数吗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;函数模板是一个蓝图，它本身并不是函数，是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;C:/Users/19912/AppData/Roaming/Typora/typora-user-images/image-20240529203345238.png&quot; alt=&quot;image-20240529203345238&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类函数分离声明与定义的写法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &quot;Stack.h&quot;
 
template&amp;#x3C;class T1&gt;
// 构造函数
Stack&amp;#x3C;T1&gt;::Stack(int capacity)
	:_a(new T1[capacity])
	,_capacity(capacity)
	,_size(0)
{}
template&amp;#x3C;class T1&gt;
// 插入函数
void Stack&amp;#x3C;T1&gt;::Push(T1 data){
	_a[_size] = data;
	_size++;
}
// 析构函数
template&amp;#x3C;class T1&gt;
Stack&amp;#x3C;T1&gt;::~Stack(){
	delete[]_a;
	_a = nullptr;
	_capacity = _size = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;补充&lt;/h2&gt;
&lt;p&gt;explicit表示不发生隐式类型转换，所以调用失败（注意：explicit只能在构造函数中使用）&lt;/p&gt;
&lt;h2&gt;程序题&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &amp;#x3C;iostream&gt;
#include &amp;#x3C;string&gt;

using namespace std;

// 函数模板demoPrint
template &amp;#x3C;typename T&gt;
void demoPrint(const T v1, const T v2) {
    cout &amp;#x3C;&amp;#x3C; &quot;the first version of demoPrint()&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;the arguments: &quot; &amp;#x3C;&amp;#x3C; v1 &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; v2 &amp;#x3C;&amp;#x3C; endl;
}

// 函数模板demoPrint的指定特殊
template &amp;#x3C;&gt;
void demoPrint(const char v1, const char v2) {
    cout &amp;#x3C;&amp;#x3C; &quot;the specify special of demoPrint()&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;the arguments: &quot; &amp;#x3C;&amp;#x3C; v1 &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; v2 &amp;#x3C;&amp;#x3C; endl;
}

// 函数模板demoPrint重载的函数模板
template &amp;#x3C;typename T&gt;
void demoPrint(const T v) {
    cout &amp;#x3C;&amp;#x3C; &quot;the second version of demoPrint()&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;the argument: &quot; &amp;#x3C;&amp;#x3C; v &amp;#x3C;&amp;#x3C; endl;
}

// 非函数模板demoPrint
void demoPrint(const double v1, const double v2) {
    cout &amp;#x3C;&amp;#x3C; &quot;the nonfunctional template version of demoPrint()&quot; &amp;#x3C;&amp;#x3C; endl;
    cout &amp;#x3C;&amp;#x3C; &quot;the arguments: &quot; &amp;#x3C;&amp;#x3C; v1 &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; v2 &amp;#x3C;&amp;#x3C; endl;
}

int main() {
    string s1(&quot;rabbit&quot;), s2(&quot;bear&quot;);
    char c1(&apos;k&apos;), c2(&apos;b&apos;);
    int iv1 = 3, iv2 = 5;
    double dv1 = 2.8, dv2 = 8.5;
    
    // 调用第一个函数模板
    demoPrint(iv1, iv2);
    
    // 调用第一个函数模板的指定特殊
    demoPrint(c1, c2);
    
    // 调用第二个函数模板
    demoPrint(iv1);
    
    // 调用非函数模板
    demoPrint(dv1, dv2);
    
    // 模板不会发生隐式类型转换，所以隐式转换后调用非函数模板
    demoPrint(iv1, dv2);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第十五、十六周（STL）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  //1
  int main() {
      vector&amp;#x3C;int&gt; vec;
      for (int i = 0; i &amp;#x3C; 5; i++) {
          vec.push_back(i + 1);
      }
      // 1 2 3 4 5
      auto it2 = vec.begin() + 2; // 3
      auto it = vec.begin() + 3;  // 4
      vec.insert(it2, 5); // 在3的前面插入5
      // 1 2 5 3 4 5
      cout &amp;#x3C;&amp;#x3C; *(it2) &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; *(it) &amp;#x3C;&amp;#x3C; endl; // 5 3（it没有移动）
  }
  
  //2
  int main() {
      vector&amp;#x3C;int&gt; vec;
      for (int i = 0; i &amp;#x3C; 5; i++) {
          vec.push_back(i + 1);
      }
      // 1 2 3 4 5
      auto it2 = vec.begin() + 2; // 3
      auto it = vec.begin() + 3;  // 4
      vec.insert(it2, 5);     // 1 2 5 3 4 5
      sort(vec.begin(), vec.end()); // 1 2 3 4 5 5
      cout &amp;#x3C;&amp;#x3C; *(it2) &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; *(it) &amp;#x3C;&amp;#x3C; endl; // 3 4（迭代器不会跟随数据移动而移动，只是指向一个位置）
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于stack，下列哪项描述是正确的？&lt;/p&gt;
&lt;p&gt;A.stack是一种先进先出（FIFO）的数据结构&lt;/p&gt;
&lt;p&gt;B.stack支持随机访问其中的元素&lt;/p&gt;
&lt;p&gt;C.stack提供的主要操作是push、pop和top等&lt;/p&gt;
&lt;p&gt;D.stack可以直接使用迭代器进行遍历&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A&lt;/strong&gt;：错误。栈是一种后进先出（LIFO）的数据结构，而不是先进先出（FIFO）。先进先出描述的是队列（&lt;code&gt;std::queue&lt;/code&gt;）的行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B&lt;/strong&gt;：错误。栈不支持随机访问其内部元素。栈的操作主要集中在栈顶，你不能直接访问栈中间的元素，这是由栈的设计和目的决定的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt;：错误。&lt;code&gt;std::stack&lt;/code&gt;不提供直接使用迭代器进行遍历的能力。这是因为栈是一种只能从一端（栈顶）访问元素的容器，不支持像数组或链表那样的随机访问模式。如果需要遍历栈中的元素，你需要将元素从栈中移除，这通常不是遍历操作所期望的。&lt;/li&gt;
&lt;li&gt;请注意：stack，queue等==是一个容器适配器，而不是一个顺序容器==。它提供了一组特定的接口（如 &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pop&lt;/code&gt;, &lt;code&gt;front&lt;/code&gt;, &lt;code&gt;back&lt;/code&gt; 等）来支持先进先出（FIFO）的数据结构模型。&lt;code&gt;std::queue&lt;/code&gt;通常使用&lt;code&gt;std::deque&lt;/code&gt;或&lt;code&gt;std::list&lt;/code&gt;作为其底层容器实现，但它限制了对这些底层容器的直接访问，仅提供了队列操作的接口。这种设计允许&lt;code&gt;std::queue&lt;/code&gt;专注于实现队列的行为，而不是容器的存储细节。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;C:/Users/19912/AppData/Roaming/Typora/typora-user-images/image-20240707115553040.png&quot; alt=&quot;image-20240707115553040&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二次输出的容量不会因为 &lt;code&gt;resize&lt;/code&gt; 调用而减少。即使 &lt;code&gt;vector&lt;/code&gt; 的大小被调整为4，其容量仍然保持不变，因为 &lt;code&gt;resize&lt;/code&gt; 减少大小不会自动减少容量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;C:/Users/19912/AppData/Roaming/Typora/typora-user-images/image-20240707120031314.png&quot; alt=&quot;image-20240707120031314&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个地方很好地说明了capacity和size的区别&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;  int main() {
      int n = 16;
      vector&amp;#x3C;int&gt; a;
      for (int i = 0; i &amp;#x3C; n; i++) {
          a.push_back(i);
      }
      // 0~15 capacity = 16 size = 16
      cout &amp;#x3C;&amp;#x3C; a.capacity() &amp;#x3C;&amp;#x3C; &quot; &quot;;  // 16
  
      int m = 50;
      a.reserve(m);
      cout &amp;#x3C;&amp;#x3C; a.capacity() &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; a.size() &amp;#x3C;&amp;#x3C; endl;
      // capacity = 50 size = 16
      for (int i = 0; i &amp;#x3C; m; i++) {
          a.push_back(i);
      }
      // capacity = 100 size = 66
      cout &amp;#x3C;&amp;#x3C; a.capacity() &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; a.size() &amp;#x3C;&amp;#x3C; endl;  // 100 66
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  int main() {
      int n = 17;
      vector&amp;#x3C;int&gt; a;
      for (int i = 0; i &amp;#x3C; n; i++) {
          a.push_back(i);
      }
      // 0~16, size = 17, capacity = 32
      cout &amp;#x3C;&amp;#x3C; a.capacity() &amp;#x3C;&amp;#x3C; &quot; &quot;;
      
      a.shrink_to_fit();
      // 0~16, size = 17, capacity = 17
      
      a.push_back(n + 1);
      // 0~17, size = 18, capacity = 34
      cout &amp;#x3C;&amp;#x3C; a.capacity();
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  int main() {
      int n = 3;
      vector&amp;#x3C;int&gt; a;
      for (int i = 0; i &amp;#x3C; n; i++) {
          a.push_back(i + 1);
      }
      // 1 2 3
      auto res = accumulate(a.begin(), a.end(), 1, [](int i, int j) { return pow(i, j); });
      // i = i^1 = 1^1 = 1
      // i = i^2 = 1^2 = 1
      // i = i^3 = 1^3 = 1
      cout &amp;#x3C;&amp;#x3C; res &amp;#x3C;&amp;#x3C; &quot; &quot;;
      
      auto res = accumulate(a.begin(),a.end(),1,[](int i,int j){return pow(j,i);});
      // i = 1^i = 1^1 = 1
      // i = 2^i = 2^1 = 2
      // i = 3^i = 3^2 = 9
  	cout &amp;#x3C;&amp;#x3C; res &amp;#x3C;&amp;#x3C; &quot; &quot;;
      
      sort(a.begin(), a.end(), [](int a, int b) { return a &gt; b; });
      // 3 2 1
      auto res2 = accumulate(a.begin(), a.end(), 1, [](int i, int j) { return pow(i, j); });
      // i = i^3 = 1^3 = 1
      // i = i^2 = 1^2 = 1
      // i = i^1 = 1^1 = 1
      cout &amp;#x3C;&amp;#x3C; res2;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这里，&lt;code&gt;i&lt;/code&gt; 是累积到目前为止的结果（初始值为 1），&lt;code&gt;j&lt;/code&gt; 是当前元素的值。lambda 表达式使用 &lt;code&gt;pow(i, j)&lt;/code&gt; 计算 &lt;code&gt;i&lt;/code&gt; 的 &lt;code&gt;j&lt;/code&gt; 次幂，这个结果将被用作下一步的累积值。&lt;/p&gt;
&lt;p&gt;因此，对于这个特定的 &lt;code&gt;accumulate&lt;/code&gt; 调用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个参数 &lt;code&gt;i&lt;/code&gt; 表示到目前为止的累积结果。&lt;/li&gt;
&lt;li&gt;第二个参数 &lt;code&gt;j&lt;/code&gt; 表示当前遍历到的元素值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;课堂2中有一个地方&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;map&amp;#x3C;string, set&amp;#x3C;int&gt;&gt; students;

// 在我们试图寻找到。对应的set的时候出现了问题
for(auto c : students.at(name)) {
    sum += c;
    cnt++;
    cout &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; &quot; &quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;错误示例 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(auto c : students[name]) {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误原因：&lt;code&gt;students&lt;/code&gt; 是一个对 &lt;code&gt;const map&amp;#x3C;string, set&amp;#x3C;int&gt;&gt;&lt;/code&gt; 的引用，这意味着你不能通过这个引用修改 &lt;code&gt;map&lt;/code&gt; 或其内部的元素。在 &lt;code&gt;for&lt;/code&gt; 循环中使用 &lt;code&gt;students[name]&lt;/code&gt; 实际上会尝试在 &lt;code&gt;map&lt;/code&gt; 中查找键 &lt;code&gt;name&lt;/code&gt;，如果它不存在，则会插入一个新的键值对（默认构造的 &lt;code&gt;set&amp;#x3C;int&gt;&lt;/code&gt;）。然而，由于 &lt;code&gt;students&lt;/code&gt; 是 &lt;code&gt;const&lt;/code&gt; 的，这种修改（即使它实际上是因为查找不存在的键而发生的隐式插入）也是不允许的。&lt;/p&gt;
&lt;h4&gt;错误示例 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(auto it = students[name].begin()...; // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误原因：和错误示例 1 类似，这里也是尝试通过 &lt;code&gt;students[name]&lt;/code&gt; 访问 &lt;code&gt;map&lt;/code&gt; 中的 &lt;code&gt;set&lt;/code&gt;，这同样会导致尝试修改 &lt;code&gt;const&lt;/code&gt; 对象，因为当 &lt;code&gt;name&lt;/code&gt; 不存在于 &lt;code&gt;map&lt;/code&gt; 中时，&lt;code&gt;students[name]&lt;/code&gt; 会隐式地插入一个新的 &lt;code&gt;set&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;正确示例 1&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(auto c : students.find(name)-&gt;second) {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确原因：&lt;code&gt;students.find(name)&lt;/code&gt; 返回一个迭代器，指向键为 &lt;code&gt;name&lt;/code&gt; 的元素（如果存在的话），或者指向 &lt;code&gt;map&lt;/code&gt; 的 &lt;code&gt;end()&lt;/code&gt;。由于它不会尝试修改 &lt;code&gt;map&lt;/code&gt;，所以这在 &lt;code&gt;const&lt;/code&gt; 上下文中是合法的。&lt;code&gt;-&gt;second&lt;/code&gt; 用于访问找到的 &lt;code&gt;map&lt;/code&gt; 元素的 &lt;code&gt;set&lt;/code&gt; 成员。&lt;/p&gt;
&lt;h4&gt;正确示例 2&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(auto c : students.at(name)) {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确原因：&lt;code&gt;students.at(name)&lt;/code&gt; 也会返回与键 &lt;code&gt;name&lt;/code&gt; 关联的 &lt;code&gt;set&lt;/code&gt;，但它会在找不到键时抛出一个异常（而不是像 &lt;code&gt;operator[]&lt;/code&gt; 那样隐式地插入一个元素）。由于它不修改 &lt;code&gt;map&lt;/code&gt;，所以在 &lt;code&gt;const&lt;/code&gt; 上下文中也是安全的。但是，使用 &lt;code&gt;at&lt;/code&gt; 时需要确保键确实存在，否则程序会因为未捕获的异常而终止。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;const&lt;/code&gt; 上下文中处理容器时，你必须确保不执行任何可能修改容器的操作。这包括避免使用 &lt;code&gt;operator[]&lt;/code&gt; 访问 &lt;code&gt;map&lt;/code&gt; 的元素，因为它可能会隐式地插入新元素。相反，应该使用 &lt;code&gt;find&lt;/code&gt; 或 &lt;code&gt;at&lt;/code&gt;（但要注意处理可能的异常）。&lt;/p&gt;
&lt;h2&gt;vector（序列式容器）常用函数&lt;/h2&gt;
&lt;p&gt;C++中的&lt;code&gt;std::vector&lt;/code&gt;是一个动态数组，提供了一系列用于管理其存储的元素的函数。以下是一些&lt;code&gt;std::vector&lt;/code&gt;的常用函数及其返回值：&lt;/p&gt;
&lt;h4&gt;容量相关&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;empty()&lt;/strong&gt;：检查容器是否为空。返回&lt;code&gt;bool&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;size()&lt;/strong&gt;：返回容器中的元素数。返回&lt;code&gt;size_type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;max_size()&lt;/strong&gt;：返回容器可能包含的最大元素数。返回&lt;code&gt;size_type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;capacity()&lt;/strong&gt;：返回在不重新分配的情况下容器可以容纳的元素数量。返回&lt;code&gt;size_type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;resize(n)&lt;/strong&gt;：调整容器的大小为&lt;code&gt;n&lt;/code&gt;个元素。无返回值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reserve(n)&lt;/strong&gt;：请求改变容器的容量至少为&lt;code&gt;n&lt;/code&gt;个元素。无返回值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shrink_to_fit()&lt;/strong&gt;：请求移除未使用的容量。无返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;修改器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;clear()&lt;/strong&gt;：移除所有元素。无返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;insert(position, value)&lt;/strong&gt;：在指定位置之前插入元素。==返回指向新插入的元素的迭代器。==&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;erase(position)&lt;/strong&gt;：移除指定位置的元素。==返回指向被移除元素之后元素的迭代器==。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意：erase会自动指向删除元素的下一个，所以这里注意代码规范&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  vector&amp;#x3C;int&gt; v = {1, 2, 3, 4, 5};
  // correct 
  for (auto i = v.begin(); i != v.end(); ) {
      if(*i == 5) {
          v.erase(i);
          cout &amp;#x3C;&amp;#x3C; *i &amp;#x3C;&amp;#x3C; endl;
      } else {
          i++;
      }
  }
  
  // error
  for (auto i = v.begin(); i != v.end(); i++) {
      if(*i == 5) {
          i = v.erase(i);
          cout &amp;#x3C;&amp;#x3C; *i &amp;#x3C;&amp;#x3C; endl;
      }
  }
  // erase 之后 i 指向 v.end()了，然后又++，导致指针丢失，所以一直循环
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;push_back(value)&lt;/strong&gt;：在容器末尾添加一个新元素。无返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;pop_back()&lt;/strong&gt;：移除容器末尾的元素。无返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;swap(vector)&lt;/strong&gt;：与另一个同类型的&lt;code&gt;vector&lt;/code&gt;交换内容。无返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;元素访问&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;operator[] (n)&lt;/strong&gt;：访问指定位置&lt;code&gt;n&lt;/code&gt;的元素。返回引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;at(n)&lt;/strong&gt;：访问指定位置&lt;code&gt;n&lt;/code&gt;的元素，带边界检查。返回引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;front()&lt;/strong&gt;：访问第一个元素。返回引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;back()&lt;/strong&gt;：访问最后一个元素。返回引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;data()&lt;/strong&gt;：返回指向容器中第一个元素的指针。返回&lt;code&gt;T*&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;map（关联式容器）常用函数&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;std::map&lt;/code&gt;是C++标准模板库（STL）中的一个关联容器，它存储键值对，并且基于键来自动排序。每个键在&lt;code&gt;std::map&lt;/code&gt;中是唯一的。以下是&lt;code&gt;std::map&lt;/code&gt;的一些常用成员函数及其返回值：&lt;/p&gt;
&lt;h4&gt;访问元素&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;at(const Key&amp;#x26; key)&lt;/code&gt;：返回给定键对应的值的引用，如果键不存在则抛出&lt;code&gt;std::out_of_range&lt;/code&gt;异常。返回类型为&lt;code&gt;mapped_type&amp;#x26;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator[](const Key&amp;#x26; key)&lt;/code&gt;：访问给定键对应的值，如果键不存在，则插入一个新的键值对，其中值进行默认初始化。返回类型为&lt;code&gt;mapped_type&amp;#x26;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;容量和大小&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;empty()&lt;/code&gt;：检查容器是否为空。返回&lt;code&gt;true&lt;/code&gt;如果容器为空，否则返回&lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回容器中键值对的数量。返回类型为&lt;code&gt;size_type&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_size()&lt;/code&gt;：返回容器可能包含的最大键值对数量。返回类型为&lt;code&gt;size_type&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;修改器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;：移除容器中的所有键值对。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert(const value_type&amp;#x26; value)&lt;/code&gt;：插入一个键值对，如果键已存在，则不进行任何操作。返回一个&lt;code&gt;pair&amp;#x3C;iterator,bool&gt;&lt;/code&gt;，其中&lt;code&gt;iterator&lt;/code&gt;指向插入的元素或已存在的元素，&lt;code&gt;bool&lt;/code&gt;表示是否插入成功。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;erase(const Key&amp;#x26; key)&lt;/code&gt;：移除指定键的键值对。返回移除的元素数量（0或1）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swap(map&amp;#x26; other)&lt;/code&gt;：与另一个&lt;code&gt;map&lt;/code&gt;交换内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;查找&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find(const Key&amp;#x26; key)&lt;/code&gt;：查找给定键的元素。==如果找到，则返回一个指向该元素的迭代器==；否则，返回&lt;code&gt;end()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count(const Key&amp;#x26; key)&lt;/code&gt;：返回具有给定键的元素数量（由于&lt;code&gt;map&lt;/code&gt;中的键是唯一的，因此返回值为0或1）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;迭代器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;begin()&lt;/code&gt; / &lt;code&gt;cbegin()&lt;/code&gt;：返回指向容器中第一个元素的迭代器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;end()&lt;/code&gt; / &lt;code&gt;cend()&lt;/code&gt;：返回指向容器末尾的迭代器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;观察者&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;key_comp()&lt;/code&gt;：返回用于键比较的函数对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value_comp()&lt;/code&gt;：返回用于值比较的函数对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第十七周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;以下哪个选项描述的是C++中异常处理的正确用法？&lt;/p&gt;
&lt;p&gt;A.在&lt;code&gt;try&lt;/code&gt;块中使用&lt;code&gt;throw&lt;/code&gt;语句抛出一个整数类型的异常&lt;/p&gt;
&lt;p&gt;B.在&lt;code&gt;catch&lt;/code&gt;块中捕获一个整数类型的异常，并将其赋值给一个字符串变量&lt;/p&gt;
&lt;p&gt;C.在&lt;code&gt;catch&lt;/code&gt;块中捕获一个整数类型的异常，并将其赋值给一个整数变量&lt;/p&gt;
&lt;p&gt;D.在&lt;code&gt;catch&lt;/code&gt;块中捕获一个整数类型的异常，并将其赋值给一个指针变量&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：这个选项是正确的行为，但它不是关于如何&lt;strong&gt;捕获&lt;/strong&gt;异常的描述，而是关于如何&lt;strong&gt;抛出&lt;/strong&gt;异常，属于异常检测。因此，它不是针对问题“描述C++中异常处理的正确用法”的最佳答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何抛出一个标准库中的异常对象？&lt;/p&gt;
&lt;p&gt;A.throw new std::exception(&quot;error&quot;);&lt;/p&gt;
&lt;p&gt;B.throw std::exception(&quot;error&quot;);&lt;/p&gt;
&lt;p&gt;C.throw &quot;error&quot;;&lt;/p&gt;
&lt;p&gt;D.throw std::runtime_error(&quot;error&quot;);&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D
注意：exception没有含参数的构造函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一个try-catch块中，try抛出异常，如果catch块没有匹配到任何异常，程序将会怎样？&lt;/p&gt;
&lt;p&gt;A.程序正常继续执行&lt;/p&gt;
&lt;p&gt;B.程序崩溃&lt;/p&gt;
&lt;p&gt;C.抛出未捕获的异常&lt;/p&gt;
&lt;p&gt;D.返回默认值&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;p&gt;解释：会输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;terminate called after throwing an instance of &apos;std::exception&apos;
  what():  std::exception
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在异常处理中，&lt;code&gt;finally&lt;/code&gt; 块用于：&lt;/p&gt;
&lt;p&gt;A.C++ 没有 &lt;code&gt;finally&lt;/code&gt; 块&lt;/p&gt;
&lt;p&gt;B.抛出异常&lt;/p&gt;
&lt;p&gt;C.捕获异常&lt;/p&gt;
&lt;p&gt;D.定义异常&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;finally是Js里的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;throw&lt;/code&gt; 关键字后面可以跟随：&lt;/p&gt;
&lt;p&gt;A.任何类型的值&lt;/p&gt;
&lt;p&gt;B.只有整数&lt;/p&gt;
&lt;p&gt;C.只有字符串&lt;/p&gt;
&lt;p&gt;D.只有自定义异常类型&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第十八周&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在C++中，异常机制被用来解决哪种类型的程序错误？&lt;/p&gt;
&lt;p&gt;A.语法错误&lt;/p&gt;
&lt;p&gt;B.逻辑错误&lt;/p&gt;
&lt;p&gt;C.运行时错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下列关于异常，叙述错误的是（）&lt;/p&gt;
&lt;p&gt;A.编译错误属于异常，可以抛出&lt;/p&gt;
&lt;p&gt;B.运行错误属于异常&lt;/p&gt;
&lt;p&gt;C.硬件故障也可当异常抛出&lt;/p&gt;
&lt;p&gt;D.只要是人认为的异常都可被抛出&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A. 编译错误属于异常，可以抛出&lt;/strong&gt;：这个叙述是错误的。编译错误是在编译时发生的错误，它们是由编译器检测到的，通常是因为代码不符合语言的语法规则、类型不匹配、缺少必要的定义等原因。编译错误必须在编译阶段被修正，程序才能成功编译成可执行文件。编译错误不能被抛出或捕获，因为它们在程序运行之前就必须被解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B. 运行错误属于异常&lt;/strong&gt;：这个叙述是正确的。运行时错误是程序执行过程中发生的错误，如除以零、访问无效的内存地址、文件不存在等。这些错误可以通过异常处理机制被抛出和捕获，以便程序可以优雅地处理错误情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C. 硬件故障也可当异常抛出&lt;/strong&gt;：在某种程度上，这个叙述可以被认为是正确的。虽然硬件故障本身不是通过C++标准异常机制直接抛出的，但是硬件故障（如磁盘读写错误、网络连接中断等）可以通过操作系统或硬件抽象层检测到，并最终通过软件异常或错误码的形式暴露给应用程序。应用程序可以捕获这些软件异常或检查错误码来响应硬件故障。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D. 只要是人认为的异常都可被抛出&lt;/strong&gt;：这个叙述是正确的，但需要一定的上下文理解。在C++中，几乎任何类型的对象都可以作为异常被抛出，这意味着程序员可以根据需要定义和抛出自定义的异常类型。因此，从这个角度来看，只要是开发者认为需要特殊处理的情况，都可以通过抛出异常来处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当程序遇到一个==没有== &lt;code&gt;noexcept&lt;/code&gt; 修饰符的函数，会假设这个函数可能抛出异常，导致额外的代码执行，使用 &lt;code&gt;noexcept&lt;/code&gt; 可以优化程序性能。
该说法是否正确？&lt;/p&gt;
&lt;p&gt;A.正确&lt;/p&gt;
&lt;p&gt;B.错误&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A&lt;/p&gt;
&lt;p&gt;注意是没有&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【多选】下列关于异常传播的说法正确的是（）&lt;/p&gt;
&lt;p&gt;A.当在try块内部发生异常时，程序会立即退出当前的try块，并开始搜索匹配的catch块来处理该异常。&lt;/p&gt;
&lt;p&gt;B.如果在try块中没有找到匹配的catch块，异常会传播到调用栈的上一层，即调用该try块的函数或方法中&lt;/p&gt;
&lt;p&gt;C.过程会继续进行，直到找到匹配的catch块或者异常传播到程序的顶层（通常是main函数外部），此时程序会调用std::terminate并终止执行。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: A, B, C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下列哪项最能描述C++中的异常？&lt;/p&gt;
&lt;p&gt;A.一种错误的检测机制&lt;/p&gt;
&lt;p&gt;B.一种错误的处理方法&lt;/p&gt;
&lt;p&gt;C.一种处理程序中意外情况的方法&lt;/p&gt;
&lt;p&gt;D.一种调试工具&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在以下代码中，哪一行是重新抛出异常的正确方法？
try {
throw std::runtime_error(&quot;error&quot;);
} catch (std::runtime_error&amp;#x26; e) {
// 哪行是重新抛出异常？
}&lt;/p&gt;
&lt;p&gt;A.throw e;&lt;/p&gt;
&lt;p&gt;B.throw std::runtime_error(e);&lt;/p&gt;
&lt;p&gt;C.throw;&lt;/p&gt;
&lt;p&gt;D.rethrow;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下列关于自定义异常的描述哪个是正确的？&lt;/p&gt;
&lt;p&gt;A.自定义异常必须继承自 &lt;code&gt;std::exception&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;B.自定义异常必须包含一个 &lt;code&gt;what()&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;C.自定义异常不能包含成员变量&lt;/p&gt;
&lt;p&gt;D.自定义异常类可以不继承任何标准异常类&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: D&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在对象的构造函数中抛出异常时，以下哪项描述是正确的？&lt;/p&gt;
&lt;p&gt;A.对象构造完成后抛出异常&lt;/p&gt;
&lt;p&gt;B.对象不会被创建，析构函数不会被调用&lt;/p&gt;
&lt;p&gt;C.对象会被部分创建，析构函数会被调用以清理已分配的资源&lt;/p&gt;
&lt;p&gt;D.构造函数不能抛出异常&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Standard Answer: B&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;文件读写&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;流&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流&lt;/strong&gt;是一种对&lt;strong&gt;连接&lt;/strong&gt;的抽象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流入的量&lt;/strong&gt;等于&lt;strong&gt;流出的量&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存区&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓冲区&lt;/strong&gt;的作用？&lt;/li&gt;
&lt;li&gt;从命令行向程序输入数据时，实际上是输入到缓冲区里&lt;/li&gt;
&lt;li&gt;一旦按Enter回车键，缓冲区里的数据才流入程序&lt;/li&gt;
&lt;li&gt;在按回车键之前，我们可以修改缓冲区（即当前行）的数据&lt;/li&gt;
&lt;li&gt;但一旦按回车键后，我们就不能修改当前行数据了&lt;/li&gt;
&lt;li&gt;程序输出时，同样是输出到缓冲区里&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++的文件读写方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ofstream&lt;/strong&gt;: 写操作（输出）的文件类 (由&lt;strong&gt;ostream&lt;/strong&gt;继承而来)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ifstream&lt;/strong&gt;: 读操作（输入）的文件类(由&lt;strong&gt;istream&lt;/strong&gt;继承而来)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fstream&lt;/strong&gt;: 可同时读写操作的文件类 (由&lt;strong&gt;iostream&lt;/strong&gt;继承而来)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用法和cin、cout完全一样！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;  /*样例*/
  #include &amp;#x3C;fstream&gt;
  #include &amp;#x3C;iostream&gt;
  
  using namespace std;
  
  // 实现功能：将source.txt中的内容去掉空格后写入result.txt
  
  int main() {
      ifstream infile(&quot;source.txt&quot;);
      // jkadshf ah fa dfhas dfahsdf ashdf a sdf ads
      ofstream outfile(&quot;result.txt&quot;);
      while (!infile.eof()) {  // eof()函数用于判断是否到达文件末尾
          char tmp;
          infile &gt;&gt; tmp;  // 从文件中读取一个字符
          if (tmp != &apos; &apos;) {
              outfile &amp;#x3C;&amp;#x3C; tmp;  // 将读取的字符写入文件
          }
      }
      infile.close();
      outfile.close();
      // jkadshfahfadfhasdfahsdfashdfasdfsdfads
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::out&lt;/strong&gt;  文件以输出(写)方式打开&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::in&lt;/strong&gt;   文件以输入(读)方式打开&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::ate&lt;/strong&gt;  初始位置：文件尾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::app&lt;/strong&gt;  所有输出附加在文件末尾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::trunc&lt;/strong&gt;  如果文件已存在则先删除该文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ios::binary&lt;/strong&gt;  二进制方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以用 | 来放入多个参数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>随机过程</title><link>https://iaohr9.github.io/blog/%E9%9A%8F%E6%9C%BA%E8%BF%87%E7%A8%8B</link><guid isPermaLink="true">https://iaohr9.github.io/blog/%E9%9A%8F%E6%9C%BA%E8%BF%87%E7%A8%8B</guid><description>中山大学龚杰老师随机过程课堂笔记</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数学常识&lt;/h1&gt;
&lt;h2&gt;概率论&lt;/h2&gt;
&lt;h3&gt;条件概率&lt;/h3&gt;
&lt;p&gt;条件概率是指在已知某一事件发生的条件下，另一个事件发生的概率。形式化定义为：&lt;/p&gt;
&lt;p&gt;$$
P(A | B) = \frac{P(A \cap B)}{P(B)}, \quad P(B) &gt; 0
$$&lt;/p&gt;
&lt;h3&gt;全概率公式&lt;/h3&gt;
&lt;p&gt;全概率公式用于计算一个事件的概率，该事件可以通过多个互斥事件的并集来表示。形式化定义为：&lt;/p&gt;
&lt;p&gt;$$
P(A) = \sum_{i} P(A | B_{i}) P(B_{i})
$$&lt;/p&gt;
&lt;p&gt;其中，$B_i$ 是一组互斥且完备的事件。&lt;/p&gt;
&lt;h3&gt;贝叶斯公式&lt;/h3&gt;
&lt;p&gt;贝叶斯公式用于更新事件的概率，基于新的信息。形式化定义为：&lt;/p&gt;
&lt;p&gt;$$
P(A | B) = \frac{P(B | A) P(A)}{P(B)}, \quad P(B) &gt; 0
$$&lt;/p&gt;
&lt;h3&gt;概率分布和概率密度&lt;/h3&gt;
&lt;h4&gt;离散随机变量&lt;/h4&gt;
&lt;p&gt;离散随机变量的概率分布函数（Probability Mass Function, PMF）定义为：&lt;/p&gt;
&lt;p&gt;$$
P(X = x) = p_X(x)
$$&lt;/p&gt;
&lt;h4&gt;连续随机变量&lt;/h4&gt;
&lt;p&gt;连续随机变量的概率密度函数（Probability Density Function, PDF）定义为：&lt;/p&gt;
&lt;p&gt;$$
f_X(x) = \frac{d}{dx} P(X \leq x)
$$&lt;/p&gt;
&lt;p&gt;这个定义是需要熟悉的，有些时候往往会通过算一个极限来求出 PDF。&lt;/p&gt;
&lt;p&gt;$$
f_X(x) = \lim_{\Delta x \to 0} \frac{P(x \leq X &amp;#x3C; x + \Delta x)}{\Delta x}
$$&lt;/p&gt;
&lt;p&gt;这在实际计算中经常用到。&lt;/p&gt;
&lt;h4&gt;累积分布函数&lt;/h4&gt;
&lt;p&gt;累积分布函数（Cumulative Distribution Function, CDF）定义为：&lt;/p&gt;
&lt;p&gt;$$
F_X(x) = P(X \leq x)
$$&lt;/p&gt;
&lt;h4&gt;联合分布&lt;/h4&gt;
&lt;p&gt;对于多个随机变量的联合分布，定义为：&lt;/p&gt;
&lt;p&gt;$$
P(X_1 = x_1, X_2 = x_2, \ldots, X_n = x_n) = p_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n)
$$&lt;/p&gt;
&lt;p&gt;对于连续随机变量的联合分布，定义为：&lt;/p&gt;
&lt;p&gt;$$
P(X_1 \leq x_1, X_2 \leq x_2, \ldots, X_n \leq x_n) = F_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n)
$$&lt;/p&gt;
&lt;p&gt;概率密度函数为：&lt;/p&gt;
&lt;p&gt;$$
f_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n) = \frac{\partial^n}{\partial x_1 \partial x_2 \cdots \partial x_n} F_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n)
$$&lt;/p&gt;
&lt;p&gt;如果 $X_1, X_2, \ldots, X_n$ 是独立的，则有：&lt;/p&gt;
&lt;p&gt;$$
P(X_1 = x_1, X_2 = x_2, \ldots, X_n = x_n) = P(X_1 = x_1) P(X_2 = x_2) \cdots P(X_n = x_n)
$$&lt;/p&gt;
&lt;p&gt;概率密度上也有&lt;/p&gt;
&lt;p&gt;$$
f_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n) = f_{X_1}(x_1) f_{X_2}(x_2) \cdots f_{X_n}(x_n)
$$&lt;/p&gt;
&lt;p&gt;当然联合分布也有&lt;/p&gt;
&lt;p&gt;$$
F_{X_1, X_2, \ldots, X_n}(x_1, x_2, \ldots, x_n) = F_{X_1}(x_1) F_{X_2}(x_2) \cdots F_{X_n}(x_n)
$$&lt;/p&gt;
&lt;h4&gt;随机变量函数的分布&lt;/h4&gt;
&lt;p&gt;对于随机变量 $X$ 和函数 $Y=g(X)$，其分布可以通过以下方式计算：&lt;/p&gt;
&lt;p&gt;比如 $Z=X+Y$，我们用 $Z$ 和 $X$ 来表示 $Y$，则有：
$$
Y = Z - X
$$
然后我们可以通过联合分布来计算：&lt;/p&gt;
&lt;p&gt;$$
f_{X+Y}(x, y) = \int_{-\infty}^{+\infty} f(x, z-x) =\int_{-\infty}^{+\infty} f_X(x) f_Y(z - x) , dx
$$&lt;/p&gt;
&lt;h3&gt;数字特征&lt;/h3&gt;
&lt;h4&gt;期望&lt;/h4&gt;
&lt;p&gt;期望（Expectation）是随机变量的加权平均值，定义为：&lt;/p&gt;
&lt;p&gt;对于离散随机变量：
$$
\mathbb{E}[X] = \sum_{x} x P(X = x)
$$&lt;/p&gt;
&lt;p&gt;对于连续随机变量：
$$
\mathbb{E}[X] = \int_{-\infty}^{+\infty} x f_X(x) , dx
$$&lt;/p&gt;
&lt;p&gt;对于高纬度随机变量，可以使用多重积分：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}[X] = \int_{-\infty}^{+\infty} \cdots\int_{-\infty}^{+\infty}  x_1 x_2 \cdots f_X(x_1, x_2, \ldots) , dx_1 , dx_2 \cdots
$$&lt;/p&gt;
&lt;h5&gt;性质&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;线性性质：对于常数 $a$ 和 $b$，有 $\mathbb{E}[aX + b] = a\mathbb{E}[X] + b$。（对任意随机变量 $X$ 和常数 $a, b$ 成立）&lt;/li&gt;
&lt;li&gt;独立性：如果 $X$ 和 $Y$ 独立，则 $\mathbb{E}[XY] = \mathbb{E}[X]\mathbb{E}[Y]$。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;全期望公式&lt;/h5&gt;
&lt;p&gt;对于随机变量 $X$ 和条件 $Y$，全期望公式为：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}[X] = \mathbb{E}[\mathbb{E}[X | Y]]
$$&lt;/p&gt;
&lt;p&gt;内层的 $\mathbb{E}[X | Y]$ 是在条件 $Y$ 下对 $X$ 的期望，而外层的 $\mathbb{E}$ 是对 $Y$ 的期望。相当于对 $Y$ 的所有可能值进行枚举，然后加权平均。&lt;/p&gt;
&lt;h4&gt;方差&lt;/h4&gt;
&lt;p&gt;方差（Variance）是随机变量与其期望之间的偏离程度，定义为：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Var}(X) = \mathbb{E}[(X - \mathbb{E}[X])^2] = \mathbb{E}[X^2] - (\mathbb{E}[X])^2
$$&lt;/p&gt;
&lt;h4&gt;协方差&lt;/h4&gt;
&lt;p&gt;协方差（Covariance）是两个随机变量之间的线性关系，定义为：&lt;/p&gt;
&lt;p&gt;$$
COV(X, Y) = \mathbb{E}[(X - \mathbb{E}[X])(Y - \mathbb{E}[Y])] = \mathbb{E}[XY] - \mathbb{E}[X]\mathbb{E}[Y]
$$
其实方差是协方差的特例，即 $COV(X, X) = \mathrm{Var}(X)$。&lt;/p&gt;
&lt;p&gt;他们与期望有着密切的关系：&lt;/p&gt;
&lt;p&gt;记忆：协方差=乘积的期望 - 期望的乘积&lt;/p&gt;
&lt;h5&gt;协方差矩阵&lt;/h5&gt;
&lt;p&gt;对于多维随机变量 $\mathbf{X} = (X_1, X_2, \ldots, X_n)$，协方差矩阵定义为：&lt;/p&gt;
&lt;p&gt;$$
\Sigma = \begin{pmatrix}
\mathrm{Var}(X_1) &amp;#x26; COV(X_1, X_2) &amp;#x26; \cdots &amp;#x26; COV(X_1, X_n) \
COV(X_2, X_1) &amp;#x26; \mathrm{Var}(X_2) &amp;#x26; \cdots &amp;#x26; COV(X_2, X_n) \
\vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \vdots \
COV(X_n, X_1) &amp;#x26; COV(X_n, X_2) &amp;#x26; \cdots &amp;#x26; \mathrm{Var}(X_n)
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;这在高维高斯过程中非常重要。&lt;/p&gt;
&lt;h3&gt;自相关函数&lt;/h3&gt;
&lt;p&gt;随机过程的自相关函数定义为
$$
R_X(t_1, t_2) = \mathbb{E}[X(t_1) X(t_2)]
$$&lt;/p&gt;
&lt;h3&gt;协方差函数&lt;/h3&gt;
&lt;p&gt;随机过程的协方差函数定义为
$$
C_X(t_1, t_2) = \mathbb{E}[(X(t_1) - \mathbb{E}[X(t_1)])(X(t_2) - \mathbb{E}[X(t_2)])]
$$&lt;/p&gt;
&lt;h3&gt;平稳性&lt;/h3&gt;
&lt;h4&gt;宽平稳&lt;/h4&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;p&gt;$X (t)$ 对于任意的 $t_1, t_2$，都有
$$
\mathbb{E}[X(t_1)] = \mathbb{E}[X(t_2)]
$$
且
$$
R_X(t_1, t_2) = R_X(t_1 + \tau, t_2 + \tau)
$$
即自相关函数只与时间差 $\tau = t_2 - t_1$ 有关。即
$$
R_X(t_1, t_2) = R_X(\tau) = R_X(t_2 - t_1))
$$
也就是说，要证明是宽平稳的，只需要证明自相关函数只与时间差 $\tau$ 有关。&lt;/p&gt;
&lt;h4&gt;严平稳&lt;/h4&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;p&gt;$X(t)$ 对于任意的 $t_1, t_2$，都有
$$
F_{X(t_1), X(t_2)}(x_1, x_2) = F_{X(t_1 + \tau), X(t_2 + \tau)}(x_1, x_2)
$$
即联合分布函数不随时间平移而改变。&lt;/p&gt;
&lt;h3&gt;母函数&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;定义：&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;令 $X$ 是取值为非负整数的随机变量，已知 $P(X = k) = p_k$ 是对应的概率，则称一个 $X$ 型如&lt;/p&gt;
&lt;p&gt;$$
G_X(s) = \mathbb{E}[s^X] = \sum_{k=0}^{\infty} p_k s^k
$$&lt;/p&gt;
&lt;p&gt;的函数为&lt;strong&gt;概率母函数（Probability Generating Function）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也有别名加生成函数和形式级数。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;性质&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;已知随机变量 $X$ 的概率母函数为：&lt;/p&gt;
&lt;p&gt;$$
G_X(s) = \mathbb{E}[s^X] = \sum_{k=0}^{\infty} p_k s^k
$$&lt;/p&gt;
&lt;p&gt;则有以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;归一性&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
G_X(1) = 1&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;期望公式&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\mathbb{E}[X] = G_X&apos;(1)&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;方差公式&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\mathrm{Var}(X) = G_X&apos;&apos;(1) + G_X&apos;(1)\left(1 - G_X&apos;(1)\right)&lt;/p&gt;
&lt;p&gt;$$
&lt;strong&gt;概率母函数的性质：计算两个离散随机变量和的分布&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;概率母函数除了描述单个随机变量的分布外，还有一个非常有用的性质 —— &lt;strong&gt;可用于计算两个独立离散随机变量之和的分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定理：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $X_1$ 和 $X_2$ 是两个&lt;strong&gt;相互独立&lt;/strong&gt;的、取非负整数值的随机变量，分别具有概率母函数：&lt;/p&gt;
&lt;p&gt;$$
G_{X_1}(t) = \mathbb{E}[t^{X_1}], \quad G_{X_2}(t) = \mathbb{E}[t^{X_2}]
$$&lt;/p&gt;
&lt;p&gt;则它们之和 $X = X_1 + X_2$ 的概率母函数为：&lt;/p&gt;
&lt;p&gt;$$
G_X(t) = G_{X_1 + X_2}(t) = G_{X_1}(t) \cdot G_{X_2}(t)
$$&lt;/p&gt;
&lt;p&gt;也就是说：&lt;strong&gt;独立随机变量的和的概率母函数，等于各自概率母函数的乘积。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;特征函数&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;特征函数（Characteristic Function）是随机变量分布的一个重要工具，定义为：&lt;/p&gt;
&lt;p&gt;$$
\phi_X(\omega) = \mathbb{E}[e^{j\omega X}] = \int_{-\infty}^{\infty} e^{j\omega x} f_X(x) , dx
$$
其中，$j$ 是虚数单位，$\omega$ 是频率变量。&lt;/p&gt;
&lt;p&gt;其实就是对随机变量 $X$ 的概率密度函数 $f_X(x)$ 进行傅里叶逆变换。&lt;/p&gt;
&lt;h4&gt;性质&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;特征函数与概率分布一一对应&lt;/li&gt;
&lt;li&gt;若 $X_1, X_2 ... X_n$ 相互独立，则它们的特征函数满足
$$
\phi_{X_1 + X_2 + ... + X_n}(\omega) = \phi_{X_1}(\omega) \cdot \phi_{X_2}(\omega) \cdots \phi_{X_n}(\omega)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h2&gt;高数&lt;/h2&gt;
&lt;h3&gt;Gamma 积分公式&lt;/h3&gt;
&lt;h4&gt;📌 定义（欧拉积分形式）&lt;/h4&gt;
&lt;p&gt;对于复数 ( z )，实部大于 0：&lt;/p&gt;
&lt;p&gt;$$
\Gamma(z) = \int_0^{\infty} t^{z-1} e^{-t} , dt \quad \text{Re}(z) &gt; 0
$$&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;注意，很多时候去凑形式的时候，往往忘记了 $t^{z-1}$ 中的 $t$ 也要凑。&lt;/p&gt;
&lt;h4&gt;🎯 特殊值&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当 $z = n \in \mathbb{N}^+$ 时：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Gamma(n) = (n - 1)!
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Gamma(1) = 0! = 1, \quad \Gamma(2) = 1! = 1, \quad \Gamma(3) = 2! = 2
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特别地：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Gamma\left(\frac{1}{2}\right) = \sqrt{\pi}
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;🔁 变形与代换形式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;代换  $t = x^2$，可以得到：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Gamma\left(\frac{1}{2}\right) = \int_0^{\infty} x^{-1/2} e^{-x} , dx = \sqrt{\pi}
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h5&gt;🔗 与 Beta 函数关系&lt;/h5&gt;
&lt;p&gt;Beta 函数定义为：&lt;/p&gt;
&lt;p&gt;$$
B(x, y) = \int_0^1 t^{x-1}(1 - t)^{y - 1} , dt
$$&lt;/p&gt;
&lt;p&gt;与 Gamma 函数的关系：&lt;/p&gt;
&lt;p&gt;$$
B(x, y) = \frac{\Gamma(x)\Gamma(y)}{\Gamma(x + y)}
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h6&gt;📉 斯特林近似（当 ( z \to \infty ) 时）&lt;/h6&gt;
&lt;p&gt;$$
\Gamma(z) \approx \sqrt{2\pi} , z^{z - \frac{1}{2}} e^{-z}
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;高斯积分&lt;/h3&gt;
&lt;h4&gt;高斯积分（Gaussian Integral）&lt;/h4&gt;
&lt;h5&gt;1. 基本高斯积分&lt;/h5&gt;
&lt;p&gt;最基础的高斯积分形式为：&lt;/p&gt;
&lt;p&gt;$$
\int_{-\infty}^{+\infty} e^{-x^2} , dx = \sqrt{\pi}
$$&lt;/p&gt;
&lt;h5&gt;2. 带参数的高斯积分&lt;/h5&gt;
&lt;p&gt;对于带参数的高斯积分：&lt;/p&gt;
&lt;p&gt;$$
\int_{-\infty}^{+\infty} e^{-a x^2} , dx = \sqrt{\frac{\pi}{a}}, \quad a &gt; 0
$$&lt;/p&gt;
&lt;h5&gt;3. 奇函数高斯积分&lt;/h5&gt;
&lt;p&gt;对于奇函数乘以高斯函数的积分：&lt;/p&gt;
&lt;p&gt;当 $k$ 为奇数时：
$$
\int_{-\infty}^{+\infty} x^k e^{-x^2} , dx = 0
$$&lt;/p&gt;
&lt;h5&gt;4. 偶函数高斯积分&lt;/h5&gt;
&lt;p&gt;对于偶函数乘以高斯函数的积分：
$$
\int_{-\infty}^{+\infty} x^{2n} e^{-x^2} , dx = \frac{(2n-1)!!}{2^n} \sqrt{\pi}, \quad n \in \mathbb{N}
$$&lt;/p&gt;
&lt;h3&gt;平方展开式&lt;/h3&gt;
&lt;p&gt;对于任意 $a_1, a_2, \dots, a_n$，平方和的展开公式为：&lt;/p&gt;
&lt;p&gt;$$
\left(\sum_{k=1}^n a_k\right)^2 = \sum_{k=1}^n a_k^2 + 2 \sum_{1 \leq k &amp;#x3C; l \leq n} a_k a_l.
$$&lt;/p&gt;
&lt;h2&gt;线性代数&lt;/h2&gt;
&lt;h3&gt;特征值和特征向量&lt;/h3&gt;
&lt;p&gt;设 $A$ 是一个 $n \times n$ 的矩阵，$\lambda$ 是一个标量，$v$ 是一个非零向量。如果满足以下方程：&lt;/p&gt;
&lt;p&gt;$$
A v = \lambda v
$$
则称 $\lambda$ 是矩阵 $A$ 的特征值，$v$ 是对应的特征向量。&lt;/p&gt;
&lt;h4&gt;具体如何计算呢&lt;/h4&gt;
&lt;p&gt;特征值和特征向量的计算可以通过求解以下特征方程来完成：&lt;/p&gt;
&lt;p&gt;$$
\text{det}(\lambda I - A) = 0
$$
其中 $I$ 是单位矩阵，$\text{det}$ 表示行列式。特征值 $\lambda$ 的求解就是求解这个方程的根。&lt;/p&gt;
&lt;p&gt;解关于 $\lambda$ 的多项式方程后，可以得到特征值 $\lambda_1, \lambda_2, \ldots, \lambda_n$。&lt;/p&gt;
&lt;p&gt;对应的特征向量可以通过将特征值代入以下方程求解：&lt;/p&gt;
&lt;p&gt;$$
(A - \lambda_i I)v = 0
$$
这个方程一定是线性相关的，求到最后用某一个 $x$ 来表示其他的 $x$，然后就可以得到特征向量。&lt;/p&gt;
&lt;h3&gt;对角化&lt;/h3&gt;
&lt;p&gt;一个矩阵 $A$ 可以被对角化，如果存在一个可逆矩阵 $P$ 和一个对角矩阵 $D$，使得：&lt;/p&gt;
&lt;p&gt;$$
A = P D P^{-1}
$$
其中 $D$ 的对角线元素是 $A$ 的特征值。&lt;/p&gt;
&lt;p&gt;对角化的过程通常包括以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算特征值&lt;/strong&gt;：求解特征方程 $\text{det}(\lambda I - A) = 0$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算特征向量&lt;/strong&gt;：对于每个特征值 $\lambda_i$，求解方程 $(A - \lambda_i I)v = 0$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造矩阵 $P$ 和 $D$&lt;/strong&gt;：将特征向量按列排列形成矩阵 $P$，将特征值按对角线排列形成对角矩阵 $D$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对角化的好处在于可以简化矩阵的运算，特别是在计算矩阵的幂或指数时。&lt;/p&gt;
&lt;h3&gt;对称&lt;/h3&gt;
&lt;p&gt;一个矩阵 $A$ 是对称的，如果它等于它的转置，即：&lt;/p&gt;
&lt;p&gt;$$
A = A^T
$$
对称矩阵具有以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;实特征值&lt;/strong&gt;：所有特征值都是实数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正交特征向量&lt;/strong&gt;：对应不同特征值的特征向量是正交的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可对角化&lt;/strong&gt;：对称矩阵总是可以被对角化，且可以通过正交矩阵对角化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;正交对角化&lt;/h3&gt;
&lt;p&gt;对于对称矩阵 $A$，一定可以找到一个正交矩阵 $Q$ 和一个对角矩阵 $D$，使得：&lt;/p&gt;
&lt;p&gt;$$
A = Q D Q^T
$$
其中 $D$ 的对角线元素是 $A$ 的特征值，$Q$ 的列向量是 $A$ 的正交特征向量。&lt;/p&gt;
&lt;p&gt;与一般的对角化不同，我们这里多一步，对特征向量做&lt;strong&gt;正交归一化&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;随机过程&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;你现在知道 &lt;strong&gt;随机变量（random variable）&lt;/strong&gt; 是啥了，它表示&lt;strong&gt;一个&lt;/strong&gt;“值不确定”的量，比如投一次骰子，结果是个离散随机变量；温度计测一次温度，是个连续随机变量。&lt;/p&gt;
&lt;p&gt;但这些都是&lt;strong&gt;一次性的不确定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;随机过程（stochastic process）&lt;/strong&gt; = 多次 + 时变 + 不确定&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;随机过程就是一组随时间变化的随机变量序列&lt;/strong&gt;。你把时间引入随机变量，就是随机过程。&lt;/p&gt;
&lt;h1&gt;离散时间马尔科夫链&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;设有可数样本空间的 $\mathcal{S}$，如果随机过程 ${X_n}_{n=0}^{\infty}$ 满足：&lt;/p&gt;
&lt;p&gt;$$
P(X_{n+1} = i_{n+1} | X_n = i_n, X_{n-1} = i_{n-1}, \ldots, X_0 = i_0) = P(X_{n+1} = i_{n+1} | X_n = i_n)
$$&lt;/p&gt;
&lt;p&gt;对任意的 $n \geq 0$，则称 ${X_n}_{n=0}^{\infty}$ 是一个&lt;strong&gt;离散时间马尔科夫链&lt;/strong&gt;（DTMC）。&lt;/p&gt;
&lt;p&gt;定义告诉我们，马尔科夫链的未来状态只与当前状态有关，而与过去的状态无关。这种性质称为&lt;strong&gt;无后效性&lt;/strong&gt;（Markov property）。&lt;/p&gt;
&lt;h2&gt;状态转移概率&lt;/h2&gt;
&lt;p&gt;对于马尔科夫链 $X_n$ 的 $n$ 步转移概率定义为：&lt;/p&gt;
&lt;p&gt;$$
P(X_{k+n} = j | X_k = i) = P_{ij}^{(n)}
$$&lt;/p&gt;
&lt;p&gt;得到一个矩阵 $P^{(n)}$&lt;/p&gt;
&lt;p&gt;在我们的课程中，认为转移概率不依赖于 $k$ ，因此是&lt;strong&gt;齐次的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;特别地当 $n=1$ 时，称为&lt;strong&gt;一步转移概率&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
P_{ij} = P(X_{k+1} = j | X_k = i)
$$&lt;/p&gt;
&lt;h2&gt;C-K 方程&lt;/h2&gt;
&lt;p&gt;C-K 方程（Chapman-Kolmogorov 方程）是描述马尔科夫链状态转移的基本方程。它表明从状态 $i$ 到状态 $j$ 的 $n$ 步转移概率可以通过中间状态的转移概率来计算：
$$
P_{ij}^{(n + m)} = \sum_{k} P_{ik}^{(n)} P_{kj}^{(m)}
$$
矩阵形式为
$$
P^{(n + m)} = P^{(n)} P^{(m)}
$$
这意味着，$n$ 步转移矩阵可以通过一步转移矩阵的乘积来计算。&lt;/p&gt;
&lt;p&gt;$$
P^{(n)} = P^n
$$
有括号表示 $n$ 步转移概率矩阵，没有括号表示 $n$ 次矩阵乘积。&lt;/p&gt;
&lt;p&gt;这里通常会用到矩阵的&lt;strong&gt;幂运算&lt;/strong&gt;，即将转移矩阵 $P$ 自身乘以 $n$ 次。为了简化计算，我们可以使用矩阵的特征分解或对角化方法。&lt;/p&gt;
&lt;h2&gt;表示方法&lt;/h2&gt;
&lt;h3&gt;转移矩阵法&lt;/h3&gt;
&lt;p&gt;对于马尔科夫链 ${X_n}_{n=0}^{\infty}$，可以用转移矩阵 $P$ 来表示状态转移概率：&lt;/p&gt;
&lt;p&gt;$$
P = \begin{pmatrix}
p_{00} &amp;#x26; p_{01} &amp;#x26; \cdots &amp;#x26; p_{0n} \
p_{10} &amp;#x26; p_{11} &amp;#x26; \cdots &amp;#x26; p_{1n} \
\vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \vdots \
p_{n0} &amp;#x26; p_{n1} &amp;#x26; \cdots &amp;#x26; p_{nn}
\end{pmatrix}
$$
其中 $P_{ij}$ 表示从状态 $i$ 到状态 $j$ 的一步转移概率。&lt;/p&gt;
&lt;h3&gt;状态转移图&lt;/h3&gt;
&lt;p&gt;状态转移图是马尔科夫链的图形表示方法，其中每个状态用节点表示，转移概率用有向边表示。边上的权重表示从一个状态到另一个状态的转移概率。&lt;/p&gt;
&lt;h3&gt;递推函数&lt;/h3&gt;
&lt;p&gt;$$
X_{n+1} = f(X_n, Z_{n+1})
$$
其中 $Z_k$ 是一个随机变量序列，只需要满足下列条件之一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$Z_k$ 独立同分布，且与 $X_0$ 独立&lt;/li&gt;
&lt;li&gt;$Z_k$ 同分布，且给定 $X_n$ 的条件下，$Z_k$ 独立于 $X_0$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是我们==最常用到的证明方法==，特别是当状态空间有限时。&lt;/p&gt;
&lt;h2&gt;状态分类&lt;/h2&gt;
&lt;h3&gt;可达性&lt;/h3&gt;
&lt;p&gt;状态 $j$ 从状态 $i$ 可达，当且仅当存在一个正整数 $n$，使得 $P_{ij}^{(n)} &gt; 0$。这意味着从状态 $i$ 可以通过若干步转移到达状态 $j$。&lt;/p&gt;
&lt;h3&gt;传递性&lt;/h3&gt;
&lt;p&gt;状态 $i$ 是传递的，若 $i \rightarrow j, j\rightarrow k$，则 $i \rightarrow k$。即如果从状态 $i$ 可以到达状态 $j$，并且从状态 $j$ 可以到达状态 $k$，则从状态 $i$ 也可以到达状态 $k$。&lt;/p&gt;
&lt;h3&gt;相通性&lt;/h3&gt;
&lt;p&gt;状态 $i$ 和状态 $j$ 是相通的，当且仅当 $i \rightarrow j$ 且 $j \rightarrow i$。这意味着从状态 $i$ 可以到达状态 $j$，并且从状态 $j$ 也可以到达状态 $i$。&lt;/p&gt;
&lt;p&gt;相同是一种等价关系！因此，==&lt;strong&gt;等价类具有相同的类性质&lt;/strong&gt;==。这是很有用的，意味着当我们==确定了一个状态的性质后，可以直接推广到整个等价类==。&lt;/p&gt;
&lt;h3&gt;闭集&lt;/h3&gt;
&lt;p&gt;状态集合 $C$ 是闭集，当且仅当对于任意的状态 $i \in C$ 和 $j \notin C$，都有 $P_{ij} = 0$。这意味着从闭集中的状态无法转移到闭集外的状态，是一种&quot;进得去，出不来&quot;的吸收态。&lt;/p&gt;
&lt;h3&gt;不可约性&lt;/h3&gt;
&lt;p&gt;如果闭集 $C$ 没有闭的真子集，则称 $C$ 是不可约的。（$C$ 中任意两状态都相通）&lt;/p&gt;
&lt;p&gt;一个马尔科夫链是 &lt;strong&gt;不可约的&lt;/strong&gt;，如果从任意状态 i 出发，经过有限步转移后能以 &lt;strong&gt;正概率&lt;/strong&gt; 到达任意其他状态 j
如果链是 &lt;strong&gt;可约的&lt;/strong&gt;（Reducible），则存在某些状态子集，一旦进入就无法离开&lt;/p&gt;
&lt;p&gt;可以想象成韦恩图&lt;/p&gt;
&lt;h3&gt;周期性&lt;/h3&gt;
&lt;p&gt;状态 $i$ 的周期 $d_i$ 定义为：
$$
d_i = \gcd{n : P_{ii}^{(n)} &gt; 0}
$$
讲人话，即状态 $i$ 回到自身的步数的最大公约数。
如果 $d_i = 1$，则称状态 $i$ 是&lt;strong&gt;非周期的&lt;/strong&gt;（aperiodic）；如果 $d_i &gt; 1$，则称状态 $i$ 是&lt;strong&gt;周期的&lt;/strong&gt;（periodic）。&lt;/p&gt;
&lt;p&gt;判断周期性，画图即可&lt;/p&gt;
&lt;p&gt;如果两个状态 $i$ 和 $j$ 相通，则它们的周期相同，即 $d_i = d_j$。&lt;/p&gt;
&lt;p&gt;其实先前提到过，如果两个状态是相通的，那么他们属于同一个类，共享类性质，周期当然相同。&lt;/p&gt;
&lt;h3&gt;长时间特性&lt;/h3&gt;
&lt;p&gt;人们往往关注马尔科夫链长时间后的统计特性，比如哪些状态会被频繁访问，哪些状态会被忽略。为了描述这种长期行为，我们引入了常返性（recurrence）和瞬时性（transience）的概念。&lt;/p&gt;
&lt;h4&gt;首达概率&lt;/h4&gt;
&lt;p&gt;状态 $j$ 从状态 $i$ 经过 $n$ 步的首达概率定义为：
$$
f_{ij}^{(n)} = P(X_n = j, X_{n-1} \neq j, \ldots, X_1 \neq j | X_0 = i)
$$
通常这个概率可以通过状态转移图，然后一个个去计算。然后得到规律，即得到 $n$ 的一般表达式。&lt;/p&gt;
&lt;h4&gt;常返性与瞬时性&lt;/h4&gt;
&lt;p&gt;如果
$$
f_{ii}=\sum_{n=1}^{\infty} f_{ij}^{(n)} = 1
$$
那么状态 $i$ 是&lt;strong&gt;常返的&lt;/strong&gt;（recurrent），否则是&lt;strong&gt;瞬时的&lt;/strong&gt;（transient）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;另外的判断定理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\sum_{n=0}^{+\infty} p_{ii}^{(n)}=\frac{1}{1-f_{ii}}
$$&lt;/p&gt;
&lt;p&gt;如果
$$
\sum_{n=1}^{\infty} p_{ii}^{(n)} = \infty
$$
则状态 $i$ 是常返的；如果
$$
\sum_{n=1}^{\infty} p_{ii}^{(n)} &amp;#x3C; \infty
$$
则状态 $i$ 是瞬时的。&lt;/p&gt;
&lt;p&gt;直观理解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;常返状态&lt;/strong&gt;：像一个“黑洞”，一旦进入，就会无限次返回（如随机游走在一维整数格点上的原点）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非常返状态&lt;/strong&gt;：像一个“临时站点”，最终可能会被抛弃（如随机游走在三维空间中的点，几乎不会返回原点）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;首达时间&lt;/h4&gt;
&lt;p&gt;对于常返状态，我们可以定义他的首达时间。&lt;/p&gt;
&lt;p&gt;常返态 $i$ 的首达时间定义为：&lt;/p&gt;
&lt;p&gt;$$
T_i = \min{n \geq 1 : X_n = i | X_0 = i}
$$
根据首达时间，可以定义平均首达时间：&lt;/p&gt;
&lt;p&gt;$$
\mu_i = E[T_i] = \sum_{n=1}^{\infty} n f_{ii}^{(n)}
$$
如果 $\mu_i &amp;#x3C; \infty$，则称状态 $i$ 是&lt;strong&gt;正常返的&lt;/strong&gt;（positive recurrent），否则是&lt;strong&gt;零返的&lt;/strong&gt;（null recurrent）。&lt;/p&gt;
&lt;p&gt;如何理解呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正常返状态&lt;/strong&gt;：平均返回时间是有限的，意味着状态会被频繁访问 (&lt;strong&gt;会回来，而且回来得比较勤快&lt;/strong&gt;)
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;典型例子&lt;/strong&gt;：有限状态不可约马尔科夫链的所有状态（如天气预报模型中的“晴天”“雨天”）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零返状态&lt;/strong&gt;：平均返回时间是无限的，意味着状态会被稀疏访问 (&lt;strong&gt;会回来，但可能要等很久很久&lt;/strong&gt;)
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;典型例子&lt;/strong&gt;：一维对称随机游走（醉汉左右晃悠，最终会回家，但平均等待时间无限）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h5&gt;推论：常返性对转移概率极限的影响&lt;/h5&gt;
&lt;p&gt;对于瞬时态和零常返态，有&lt;/p&gt;
&lt;p&gt;$$
\lim_{n \to \infty} p_{ii}^{(n)} = 0
$$
对于非周期正返态，有
$$
\lim_{n \to \infty} p_{ii}^{(n)} = \frac{f_{ii}}{\mu_i}
$$
其中 $\mu_i$ 是状态 $i$ 的平均返回时间。&lt;/p&gt;
&lt;h4&gt;常返态存在定理&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;对于有限状态马尔科夫链，一定存在正常返态。&lt;/li&gt;
&lt;li&gt;对于不可约的马尔科夫链，所有状态都是正常返的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个定理告诉我们，各状态的常返性可以一眼看出！
只需要判断是否有限状态或不可约即可。对找到的不可约状态，直接认为是正常返的。
别 tm 再去死算计算首达概率了！&lt;/p&gt;
&lt;h2&gt;极限行为与平稳分布&lt;/h2&gt;
&lt;p&gt;这里考虑的问题是，当马尔科夫链运行足够长时间后，状态分布是否会趋向于某个稳定的分布，极限是否存在，如果存在，如何求解。&lt;/p&gt;
&lt;h3&gt;极限分布&lt;/h3&gt;
&lt;p&gt;如果存在一个概率分布 $\pi$，使得对于任意状态 $i$，都有：
$$
\lim_{n \to \infty} P(X_n = i) = p_i
$$
则称 $\pi$ 是马尔科夫链的&lt;strong&gt;极限分布&lt;/strong&gt;（stationary distribution）。&lt;/p&gt;
&lt;p&gt;又
$$
P(X_n=j)) = \sum_{i} p_{ij}^{(n)} P(X_0=i)
$$
可以知道：&lt;/p&gt;
&lt;p&gt;极限分布依赖于初始分布 $P(X_0=i)$ 和转移概率 $p_{ij}^{(n)}$。&lt;/p&gt;
&lt;p&gt;由[[&lt;/p&gt;
&lt;p&gt;##推论：常返性对转移概率极限的影响]]可知，当是非常返态或者零常返态的，极限分布概率为 0，所以马尔科夫链最终形态由正常返态决定。&lt;/p&gt;
&lt;p&gt;可约-&gt;有正常返的-&gt;极限随初值
周期-&gt;极限可能不存在&lt;/p&gt;
&lt;h3&gt;平稳分布&lt;/h3&gt;
&lt;p&gt;如果存在一个概率分布 $\pi=(p_1, p_2, \ldots, p_n)$，满足平衡方程
$$
\pi = \pi P
$$
则称 $\pi$ 是马尔科夫链的&lt;strong&gt;平稳分布&lt;/strong&gt;（stationary distribution）。&lt;/p&gt;
&lt;p&gt;平稳分布是马尔科夫链长期行为的一个重要特征，它描述了在长时间运行后，状态分布的稳定性。&lt;/p&gt;
&lt;p&gt;通常列出平衡方程之后，还需要加上 $\sum \pi_i=1$ 来确保 $\pi$ 是一个概率分布。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有时候极限尽管不存在，但仍然可以找到平稳分布。&lt;/li&gt;
&lt;li&gt;平稳分布和极限存在没有必然联系&lt;/li&gt;
&lt;li&gt;平稳分布不一定是唯一的，可能存在多个平稳分布。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;平稳分布存在定理&lt;/h4&gt;
&lt;p&gt;存在的充要条件是链中存在正常返态&lt;/p&gt;
&lt;p&gt;存在且唯一的充要条件是链中存在唯一的不可约正常返子集&lt;/p&gt;
&lt;h1&gt;泊松过程&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;随机过程 $N(t)$ 表示时间段 $[0, t]$ 内发生事件数目的总和，则称其为计数过程。&lt;/p&gt;
&lt;p&gt;把满足下面的条件的计数过程称为标准泊松过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$N(0) = 0$，即在时间 $0$ 时刻没有发生事件。&lt;/li&gt;
&lt;li&gt;平稳增量性，对于任意的 $0 \leq s &amp;#x3C; t$，事件发生的次数 $N(t) - N(s)$ 只与时间间隔 $t - s$ 有关&lt;/li&gt;
&lt;li&gt;独立增量性，对于任意的 $0 \leq s &amp;#x3C; t$，事件发生的次数 $N(t) - N(s)$ 与 $N(s)$ 独立。&lt;/li&gt;
&lt;li&gt;微元时间内发生多于一个事件的概率是刚好发生一次的概率的无穷小量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于某类题，要你证明是泊松过程，严格按照定义，证明每一个条件&lt;/p&gt;
&lt;h3&gt;概率分布&lt;/h3&gt;
&lt;p&gt;$$
P(N(t)=k) = \frac{(\lambda t)^k e^{-\lambda t}}{k!}, \quad k = 0, 1, 2, \dots
$$&lt;/p&gt;
&lt;h3&gt;$\lambda$ 的含义&lt;/h3&gt;
&lt;p&gt;$\lambda$ 是单位时间内事件发生的平均次数，称为&lt;strong&gt;到达率&lt;/strong&gt;（arrival rate）。它描述了事件发生的频率。
$$
\lambda = \lim_{t \to 0} \frac{N(t)}{t}
$$&lt;/p&gt;
&lt;h3&gt;期望和方差&lt;/h3&gt;
&lt;p&gt;其期望和方差分别为：
$$
E[N(t)] =  Var[N(t)] = \lambda t
$$&lt;/p&gt;
&lt;h3&gt;母函数&lt;/h3&gt;
&lt;p&gt;泊松过程的母函数（生成函数）为：
$$
M(t) = E[e^{tN(t)}] = e^{\lambda t(e^t - 1)}
$$&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;h3&gt;联合分布&lt;/h3&gt;
&lt;p&gt;为了求解泊松过程的联合分布，我们可以利用泊松过程的平稳增量性和独立增量性&lt;/p&gt;
&lt;p&gt;设 $N(t)$ 是强度为 $\lambda$ 的泊松过程，则对于任意的 $0 \leq t_1 &amp;#x3C; t_2 &amp;#x3C; \cdots &amp;#x3C; t_n$，$0 \leq k_1, k_2, \ldots, k_n$，有：
$$
P(N(t_1) = k_1, N(t_2) = k_2, \ldots, N(t_n) = k_n) = \prod_{i=1}^n P(N(t_i) - N(t_{i-1}) = k_i - k_{i-1})
$$&lt;/p&gt;
&lt;h3&gt;条件分布&lt;/h3&gt;
&lt;p&gt;设 $N(t)$ 是强度为 $\lambda$ 的泊松过程，$0 \leq s &amp;#x3C; t$，则有：
$$
P(N(t) = m | N(s) = k) = \frac{\lambda^{m-k} (t-s)^{m-k} e^{-\lambda(t-s)}}{(m-k)!}
$$
这说明在已知 $N(s) = m$ 的条件下，$N(t) - N(s)$ 仍然服从泊松分布，实际上就是 $N(t-s)$&lt;/p&gt;
&lt;h3&gt;到达时刻&lt;/h3&gt;
&lt;h4&gt;概率密度&lt;/h4&gt;
&lt;p&gt;对于强度为 $\lambda$ 的泊松过程，设 $S_n$ 是第 $n$ 次到达的时刻服从于 Gamma 分布，概率密度为：
$$
f_{S_n}(t) = \lambda e^{-\lambda t} \frac{(\lambda t)^{n-1}}{(n-1)!}, \quad t \geq 0
$$
证明：用微元法，第 $n$ 次到达时刻 $S_n$ 可以看作是前 $n-1$ 次到达时刻 $S_{n-1}$ 加上一个独立的指数分布随机变量 $X_n$，即：&lt;/p&gt;
&lt;p&gt;$$
f_{S_n}=\lim_{\Delta t \to 0}  \frac{P(t \le S_n \le t + \Delta t)}{\Delta t}= \lim_{\Delta t \to 0}\frac{P(N(t)=n - 1)P(N(t+\Delta t) - N(t) = 1)}{\Delta t} =  e^{-\lambda t} \frac{(\lambda t)^{n-1}}{(n-1)!}\lambda
$$&lt;/p&gt;
&lt;h4&gt;联合密度&lt;/h4&gt;
&lt;p&gt;任意 $n$ 次事件的到达时刻的联合概率密度为
$$
f_{S_1 \ldots S_n}(t_1, t_2, \ldots, t_n) = \lambda^ne^{-\lambda t_n}
$$
证明思路同上面微元法，这里不给了&lt;/p&gt;
&lt;h4&gt;条件密度&lt;/h4&gt;
&lt;p&gt;已知 $[0, t]$ 内发生了 $n$ 次事件，设 $S_1, S_2, \ldots, S_n$ 是这些事件的到达时刻，则它们的条件密度为
$$
f_{S_1, S_2, \ldots, S_n | N(t) = n}(t_1, t_2, \ldots, t_n) = \frac{n!}{t^n}, \quad 0 \leq t_1 &amp;#x3C; t_2 &amp;#x3C; \cdots &amp;#x3C; t_n \leq t
$$&lt;/p&gt;
&lt;h3&gt;事件间隔&lt;/h3&gt;
&lt;p&gt;对于强度为 $\lambda$ 的泊松过程，事件间隔 $S_n$ 服从指数分布，概率密度为：
$$
f_{S_n}(t) = \lambda e^{-\lambda t}, \quad t \geq 0
$$
指数分布具有无记忆性。这意味着泊松过程的事件间隔（注意不是到达时刻）是独立的，并且每个间隔的分布不受前一个间隔的影响。从任意一个时间点开始重新观察，得到的新过程在概率意义上与原过程相同。&lt;/p&gt;
&lt;h4&gt;可加性&lt;/h4&gt;
&lt;p&gt;两个泊松过程 $N_1(t)$ 和 $N_2(t)$ 的事件间隔之和仍然服从泊松分布，且参数为 $\lambda_1 + \lambda_2$。即：
$$
P(N_1(t) + N_2(t) = k) = \frac{(\lambda_1 t + \lambda_2 t)^k e^{-(\lambda_1 + \lambda_2)t}}{k!}, \quad k = 0, 1, 2, \ldots
$$
但是！做差不成立！&lt;/p&gt;
&lt;h3&gt;顺序统计量&lt;/h3&gt;
&lt;p&gt;注意，以下定义是独立于泊松过程的概率论的知识&lt;/p&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;设 $X_1, X_2, \ldots, X_n$ 是从同一分布中独立抽取的随机变量，则它们的顺序统计量定义为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第 $i$ 个顺序统计量 $X_{(i)}$ 是这 $n$ 个随机变量中第 $i$ 小的值。记作 $X_{(i)}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;概率密度&lt;/h4&gt;
&lt;p&gt;对于第 $k$ 个顺序统计量 $X_{(k)}$，其期望为：
$$
E[X_{(k)}] = \frac{k}{n+1}
$$&lt;/p&gt;
&lt;h4&gt;联合分布&lt;/h4&gt;
&lt;p&gt;对于 $n$ 个独立同分布的随机变量 $X_1, X_2, \ldots, X_n$，它们的顺序统计量 $X_{(1)}, X_{(2)}, \ldots, X_{(n)}$ 的联合分布为：&lt;/p&gt;
&lt;p&gt;$$
f_{X_{(1)}, X_{(2)}, \ldots, X_{(n)}}(x_1, x_2, \ldots, x_n) = n! f_X(x_1) f_X(x_2) \cdots f_X(x_n)&lt;br&gt;
$$
其中 $f_X(x)$ 是单个随机变量的概率密度函数。&lt;/p&gt;
&lt;p&gt;$n!$ 是因为每个顺序统计量的排列方式有 $n!$ 种可能。&lt;/p&gt;
&lt;h4&gt;与泊松过程的关系&lt;/h4&gt;
&lt;p&gt;对于泊松过程的事件到达时刻 $S_1, S_2, \ldots, S_n$，它们与 $n$ 个独立的 $(0,t)$ 上的均匀分布随机变量 $U_1, U_2, \ldots, U_n$ 的顺序统计量有相同的分布。
$$
(S_1, S_2, \ldots, S_n) \overset{d}{=} (U_{(1)}, U_{(2)}, \ldots, U_{(n)})
$$
因此有&lt;/p&gt;
&lt;h5&gt;推论&lt;/h5&gt;
&lt;p&gt;如果 $S_1, S_2, \ldots, S_n$ 是泊松过程的事件到达时刻，对任意函数 $g(x)$&lt;/p&gt;
&lt;p&gt;$$
E[\sum_{k=1}^n g(S_k)|N(t)=n] = nEg(U)
$$
其中 $U$ 是均匀分布在 $(0,t)$ 上的随机变量。&lt;/p&gt;
&lt;p&gt;这一推论是很有用的，用一道例题来说明：&lt;/p&gt;
&lt;h6&gt;等待时间总和&lt;/h6&gt;
&lt;p&gt;设乘客按照参数为 $\lambda$ 的泊松过程到达机场，飞机起飞时间为 $t$。求在飞机起飞前到达的乘客的等待时间总和的期望。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设在飞机起飞前到达的乘客数为 $N(t)$，则他们的到达时刻为 $S_1, S_2, \ldots, S_{N(t)}$。每个乘客的等待时间为 $t - S_i$，因此到达乘客的等待时间总和为：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{N(t)} (t - S_i)
$$&lt;/p&gt;
&lt;p&gt;注意这里 $N(t)$ 是随机变量，我们需要将他固定下来，因此要使用条件期望：
$$
E(\sum_{i=1}^{N(t)} (t - S_i))= E_{N(t)}(E(\sum_{i=1}^{N(t)} (t - S_i)|N(t)))
$$
利用[[&lt;/p&gt;
&lt;p&gt;##推论]]，有
$$
E(\sum_{i=1}^{N(t)} (t - S_i)|N(t)=n) = n E_U(t - U)= n(t - \frac{t}{2}) = \frac{nt}{2}
$$&lt;/p&gt;
&lt;p&gt;因此带回有
$$
E(\sum_{i=1}^{N(t)} (t - S_i)) = E_{N(t)}(\frac{N(t)t}{2}) = \frac{t}{2} E(N(t)) = \frac{t}{2} \lambda \lambda t = \frac{\lambda t^2}{2}
$$&lt;/p&gt;
&lt;h2&gt;泊松过程的拓广&lt;/h2&gt;
&lt;p&gt;泊松过程的托管本质上就是在放松它的各种限制条件&lt;/p&gt;
&lt;h3&gt;非齐次泊松过程&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;非齐次泊松过程是指到达率 $\lambda(t)$ 随时间变化的泊松过程。其定义与齐次泊松过程类似，但增量的分布依赖于时间。&lt;/p&gt;
&lt;p&gt;$$
\lambda(t) = \lim_{\Delta t \to 0} \frac{N(t + \Delta t) - N(t)}{\Delta t}
$$&lt;/p&gt;
&lt;p&gt;需要格外注意的是，这里的 $\lambda(t)$ 是一个确定性的函数，而不是随机变量。这一点将与后面的条件泊松过程区分开来。&lt;/p&gt;
&lt;h4&gt;概率分布&lt;/h4&gt;
&lt;p&gt;对于非齐次泊松过程，事件在时间 $[0, t]$ 内发生的次数 $N(t)$ 的概率分布为：&lt;/p&gt;
&lt;p&gt;$$
P(N(t) = k) = \frac{[\int_0^t \lambda(s) ds]^k }{k!}e^{-\int_0^t \lambda(s) ds}, \quad k = 0, 1, 2, \ldots
$$&lt;/p&gt;
&lt;p&gt;可以看到，只是原来的 $\lambda t$ 换成了 $\int_0^t \lambda (s) ds$，即在时间 $[0, t]$ 内的总到达率。若 $\lambda(t)$ 是常数 $\lambda$，则退化为齐次泊松过程。&lt;/p&gt;
&lt;p&gt;例如，在 $[t_1, t_2]$ 上出现 $k$ 个事件的概率为：&lt;/p&gt;
&lt;p&gt;$$
P(N(t_2)-N(t_1)=k)=\frac{[\int_{t_1}^{t_2} \lambda(s) ds]^k }{k!}e^{-\int_{t_1}^{t_2} \lambda(s) ds}
$$&lt;/p&gt;
&lt;p&gt;期望和方差都为&lt;/p&gt;
&lt;p&gt;$$
\int_{t_1}^{t_2}\lambda(t)dt
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其实&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只需要把标准泊松过程的 $\lambda t$ 换成 $\int_0^t \lambda(s) ds$ 即可。剩余的一样，仅仅失去平稳增量性。&lt;/p&gt;
&lt;h3&gt;复合泊松过程&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;复合泊松过程是指在泊松过程的基础上，放宽了每次到达事件的数量限制。即在每个时间间隔内，可能发生多个事件，每个事件的数量服从某种分布。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所以复合的意思就是，标准泊松，每次事件数不是 1，而是另一个分布&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设 $N(t)$ 是泊松过程，$Y_i$ 是每次到达事件的数量，则复合泊松过程 $Y{(t)}$ 定义为：&lt;/p&gt;
&lt;p&gt;$$
Y(t) = \sum_{i=1}^{N(t)} Y_i
$$&lt;/p&gt;
&lt;h4&gt;特征函数定理&lt;/h4&gt;
&lt;p&gt;$$
\phi_Y(t) = exp\left(\lambda t [\phi_{Y_1}(u) - 1] \right)
$$
这意味着，复合泊松过程的分布由泊松过程的参数 $\lambda$ 和每次到达事件数量的分布决定。&lt;/p&gt;
&lt;h3&gt;条件泊松（随机参数泊松）&lt;/h3&gt;
&lt;p&gt;放宽独立增量性，允许到达率 $\lambda(t)$ 是一个随机变量，这样的泊松过程称为条件泊松过程（或随机参数泊松过程）。&lt;/p&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;条件泊松过程是指在给定某些条件下，泊松过程的到达率 $\lambda(t)$ 是一个随机变量。即在某些条件下，泊松过程的到达率是随机的。&lt;/p&gt;
&lt;h4&gt;概率分布&lt;/h4&gt;
&lt;p&gt;条件泊松过程的参数 $\Lambda$ 为连续随机变量时（概率密度函数为 $f_\Lambda(\lambda)$），则条件泊松过程的概率分布为：&lt;br&gt;
$$
P(N(t) = k)=\int_0^\infty P(N(t) = k | \Lambda = \lambda) f_\Lambda(\lambda) d\lambda =\int_0^\infty \frac{[\lambda t]^k e^{-\lambda t}}{k!} f_\Lambda(\lambda) d\lambda&lt;br&gt;
$$
这其实就是个全概率公式，利用了条件泊松过程的定义。&lt;/p&gt;
&lt;h3&gt;更新过程&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;与标准泊松对比，更新过程的事件间隔是独立同分布的随机变量。&lt;/p&gt;
&lt;h4&gt;概率分布&lt;/h4&gt;
&lt;p&gt;设更新过程的事件间隔的分布为 $F_T(t)$，概率密度为 $f_T(t)$，$S_n=\sum_{i=1}^n T_i$ 是第 $n$ 次事件发生的时间，则更新过程的概率分布为：
$$
P(N(t) = n) = F_{S_n}(t)-F_{S_{n+1}}(t)
$$
其中 $F_{S_n}(t)$ 是第 $n$ 次事件发生的时间的分布函数。&lt;/p&gt;
&lt;p&gt;这里 $F_{S_n}(t)$ 可以通过卷积得到，设 $F_{k}(t) = F_{S_k}(t)$，则有&lt;/p&gt;
&lt;p&gt;$$
F_n(t) = \int_0^t f_T(s) \cdot F_{n-1}(t-s) , ds
$$&lt;/p&gt;
&lt;p&gt;也就是 $f_T(s)$ 的 $n$ 重卷积。&lt;/p&gt;
&lt;p&gt;为了理解这个公式，我们需要==将次数转化到时间轴==上，$t$ 时刻发生了 $n$ 个事件，那么等价于 $S_n$ 这个事件发生在 $t$ 时刻前，而 $S_{n+1}$ 这个事件发生在 $t$ 时刻后，因此 $F_{S_n}(t)-F_{S_{n+1}}(t)$ 就是在 $t$ 时刻发生了 $n$ 个事件的概率。&lt;/p&gt;
&lt;p&gt;$n$ 重卷积难做？那就用拉普拉斯变换转为频域上的乘法运算吧&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}{F_{S_n}(t)} = \mathcal{L}{f_T(t)}^n
$$&lt;/p&gt;
&lt;h1&gt;连续时间马尔科夫链&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;设可数状态空间的连续时间随机过程 $X(t)$, 对所有$k\in \mathbb{N}$, $s, s_1, \ldots, s_k, s$ 且 $s \ge s_k \ge \ldots \ge s_1 \ge 0$ , 有
$$
P(X(t+s) = j | X(t) = i, X(t+s_1) = i_1, \ldots, X(t+s_k) = i_k) = P(X(t+s) = j | X(t) = i)
$$
则称 $X(t)$ 为连续时间马尔科夫链。&lt;/p&gt;
&lt;p&gt;我们所讨论的马尔科夫链都为齐次的，满足
$$
P(X(t+s) = j | X(s) = i) = P(X(t) = j | X(0) = i)
$$&lt;/p&gt;
&lt;h3&gt;与离散时间马尔科夫链的区别&lt;/h3&gt;
&lt;h4&gt;区别&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;离马的状态转移发生在离散的时间点上，而连马的状态转移可以发生在任意时刻。&lt;/li&gt;
&lt;li&gt;离马的转移概率矩阵是一个固定的矩阵，而连马的转移概率矩阵是一个函数，依赖于时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;状态转移概率&lt;/h2&gt;
&lt;p&gt;状态转移概率矩阵 $P(t)$ 定义为：
$$
P_{ij}(t) = P(X(t) = j | X(0) = i)
$$&lt;/p&gt;
&lt;p&gt;矩阵形式为：
$$
P(t) = \begin{pmatrix}
P_{00}(t) &amp;#x26; P_{01}(t) &amp;#x26; P_{02}(t) &amp;#x26; \cdots \
P_{10}(t) &amp;#x26; P_{11}(t) &amp;#x26; P_{12}(t) &amp;#x26; \cdots \
P_{20}(t) &amp;#x26; P_{21}(t) &amp;#x26; P_{22}(t) &amp;#x26; \cdots \
\vdots &amp;#x26; \vdots &amp;#x26; \vdots &amp;#x26; \ddots
\end{pmatrix}
$$
一定满足行和为一：
$$
\sum_{j} P_{ij}(t) = 1, \quad \forall i
$$&lt;/p&gt;
&lt;p&gt;和离散的一样，也满足C-K方程：
$$
P(t+s) = P(t)P(s)
$$&lt;/p&gt;
&lt;h2&gt;Q矩阵&lt;/h2&gt;
&lt;h3&gt;标准连续时间马尔科夫链&lt;/h3&gt;
&lt;p&gt;标准连续时间马尔科夫链的转移概率矩阵 $P(t)$ 满足
$$\lim_{t \to 0} P(t) = I
$$
其中 $I$ 是单位矩阵。&lt;/p&gt;
&lt;p&gt;这意味着当时间趋近于零时，转移概率矩阵趋近于单位矩阵，即在极短的时间内，状态不会发生变化。&lt;/p&gt;
&lt;h3&gt;Q矩阵&lt;/h3&gt;
&lt;p&gt;如果满足标准条件，那么可以定义极限
$$
Q = \lim_{t \to 0} \frac{P(t) - I}{t}
$$
其中 $Q$ 称为&lt;strong&gt;转移率矩阵&lt;/strong&gt;或&lt;strong&gt;速率矩阵&lt;/strong&gt;。
$Q$ 的元素 $q_{ij}$ 表示从状态 $i$ 到状态 $j$ 的瞬时转移率，即在极短时间内从状态 $i$ 转移到状态 $j$ 的速率。&lt;/p&gt;
&lt;h4&gt;补充&lt;/h4&gt;
&lt;p&gt;后面我们经常会计算$q_{ij}$。需要注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 $i = j$，则 $q_{ii} = \lim_{t \to 0} \frac{P_{ii}(t) - 1}{t}$&lt;/li&gt;
&lt;li&gt;如果 $i \neq j$，则 $q_{ij} = \lim_{t \to 0} \frac{P_{ij}(t)}{t}$&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;保守性&lt;/h4&gt;
&lt;p&gt;对于标准有限状态连续时间马尔科夫链，$Q$ 矩阵的每一行的和为零，即
$$
\sum_{j} q_{ij} = 0, \quad \forall i
$$
这表示在任意时刻，系统必须处于某个状态。&lt;/p&gt;
&lt;h3&gt;停留时间定理&lt;/h3&gt;
&lt;p&gt;设连续时间马尔科夫链满足标准性和保守性条件，$\tau = \inf{t \ge 0: X(t) \neq X(0)}$ 表示从状态 $X(0)$ 开始到达其他状态的时间。则有&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\tau$ 的分布是参数为 $q_{ii}$ 的指数分布, 即 $P(\tau &gt; t) = e^{-q_{ii} t}$。&lt;/li&gt;
&lt;li&gt;条件分布 $P(X(\tau) = j | X(0) = i)$  = $\frac{q_{ij}}{q_{ii}}$，即在状态 $i$ 停留时间结束后，转移到状态 $j$ 的概率与速率矩阵的元素有关。&lt;/li&gt;
&lt;li&gt;$\tau$ 与 $X(t)$ 在给定 $X(0)$ 的条件下是独立的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;嵌入链&lt;/h2&gt;
&lt;p&gt;从连马中只抽出&lt;strong&gt;跳转了&lt;/strong&gt;和&lt;strong&gt;跳去哪了&lt;/strong&gt;的信息，形成一个离散时间马尔科夫链，称为嵌入链
它的转移概率为
$$
p_{ij} = \begin{cases}
\frac{q_{ij}}{-q_{ii}}, &amp;#x26; i \neq j \
0, &amp;#x26; i = j
\end{cases}
$$&lt;/p&gt;
&lt;h2&gt;Kolmogorov方程&lt;/h2&gt;
&lt;p&gt;Kolmogorov方程描述了连续时间马尔科夫链的状态转移概率矩阵 $P(t)$ 的演化。它有两种形式：前向和后向。&lt;/p&gt;
&lt;h3&gt;前向Kolmogorov方程&lt;/h3&gt;
&lt;p&gt;$$
\frac{dP(t)}{dt} = P(t)Q
$$&lt;/p&gt;
&lt;h3&gt;后向Kolmogorov方程&lt;/h3&gt;
&lt;p&gt;$$
\frac{dP(t)}{dt} = QP(t)
$$&lt;/p&gt;
&lt;h3&gt;转移概率的收敛级数形式&lt;/h3&gt;
&lt;p&gt;如果 $Q$ 是一个有限矩阵，则可以将转移概率矩阵 $P(t)$ 表示为
$$
P(t) = I + tQ + \frac{t^2}{2!}Q^2 + \frac{t^3}{3!}Q^3 + \cdots
$$
这个级数形式可以用于计算转移概率矩阵，尤其在 $t$ 较小的情况下。&lt;/p&gt;
&lt;h2&gt;极限行为&lt;/h2&gt;
&lt;h3&gt;平稳分布&lt;/h3&gt;
&lt;p&gt;平稳分布是指当时间趋近于无穷大时，马尔科夫链的状态分布趋于一个固定的分布。设 $\pi$ 是平稳分布，则满足
$$
\pi = \pi P(t)
$$
这表示在平稳分布下，状态的分布不随时间变化。&lt;/p&gt;
&lt;p&gt;同样也可以用速率矩阵 $Q$ 来描述平稳分布：&lt;/p&gt;
&lt;p&gt;$$
\pi Q = 0
$$
这表示在平稳分布下，状态的转移率矩阵 $Q$ 的每一行和为零。&lt;/p&gt;
&lt;h3&gt;定理&lt;/h3&gt;
&lt;p&gt;对于不可约的连续时间马尔科夫链&lt;/p&gt;
&lt;p&gt;如果$\pi Q = 0$有解，则存在唯一的平稳分布 $\pi$，且满足
$$
\lim_{t \to \infty} P_{ij}(t) = \pi_j&lt;br&gt;
$$&lt;/p&gt;
&lt;p&gt;如果无解，则不存在平稳分布。
$$
\lim_{t \to \infty} P_{ij}(t) = 0
$$&lt;/p&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h3&gt;纯生过程&lt;/h3&gt;
&lt;p&gt;纯生过程是一种特殊的连续时间马尔科夫链，其中状态空间为非负整数，且状态只能增加或保持不变。其在$[t, \Delta t]$内的转移概率为
$$
P_{ij}(\Delta t) =
\begin{cases}
\lambda_i \Delta t + o(\Delta t), &amp;#x26; j = i+1 \
o(\Delta t), &amp;#x26; j = i \
0, &amp;#x26; \text{others}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;根据定义可以算出$Q$矩阵为
$$
Q_{ij} =
\begin{cases}
\lambda_i, &amp;#x26; j = i+1 \
-\lambda_i, &amp;#x26; j = i \
0, &amp;#x26; \text{otherwise}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;$$
Q = \begin{pmatrix}
-\lambda_0 &amp;#x26; \lambda_0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; \cdots
\
0 &amp;#x26; -\lambda_1 &amp;#x26; \lambda_1 &amp;#x26; 0 &amp;#x26; \cdots
\
0 &amp;#x26; 0 &amp;#x26; -\lambda_2 &amp;#x26; \lambda_2 &amp;#x26; \cdots
\
\vdots &amp;#x26; \vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \ddots&lt;br&gt;
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;可验证
$$
\pi Q = 0
$$&lt;/p&gt;
&lt;p&gt;只有平凡解，即 $\pi = 0$。
因此纯生过程没有平稳分布。&lt;/p&gt;
&lt;h3&gt;生灭过程&lt;/h3&gt;
&lt;p&gt;生灭过程是一种连续时间马尔科夫链，其状态空间为非负整数 ${0, 1, 2, \ldots}$，且从状态 $i$ 只能转移到相邻状态 $i-1$（死亡）或 $i+1$（出生）。&lt;/p&gt;
&lt;p&gt;设生灭过程的出生率为 $\lambda_i$（从状态 $i$ 到状态 $i+1$ 的速率），死亡率为 $\mu_i$（从状态 $i$ 到状态 $i-1$ 的速率）。&lt;/p&gt;
&lt;p&gt;其在 $[t, t+\Delta t]$ 内的转移概率为：
$$
P_{ij}(\Delta t) =
\begin{cases}
\lambda_i \Delta t + o(\Delta t), &amp;#x26; j = i+1 \
\mu_i \Delta t + o(\Delta t), &amp;#x26; j = i-1 \
1 - (\lambda_i + \mu_i) \Delta t + o(\Delta t), &amp;#x26; j = i \
o(\Delta t), &amp;#x26; \text{others}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;其中 $\mu_0 = 0$（状态 0 不能再减少）。&lt;/p&gt;
&lt;p&gt;根据定义，$Q$ 矩阵为：
$$
Q_{ij} =
\begin{cases}
\lambda_i, &amp;#x26; j = i+1 \
\mu_i, &amp;#x26; j = i-1 \
-(\lambda_i + \mu_i), &amp;#x26; j = i \
0, &amp;#x26; \text{others}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;矩阵形式为：
$$
Q = \begin{pmatrix}
-\lambda_0 &amp;#x26; \lambda_0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; \cdots \
\mu_1 &amp;#x26; -(\lambda_1 + \mu_1) &amp;#x26; \lambda_1 &amp;#x26; 0 &amp;#x26; \cdots \
0 &amp;#x26; \mu_2 &amp;#x26; -(\lambda_2 + \mu_2) &amp;#x26; \lambda_2 &amp;#x26; \cdots \
\vdots &amp;#x26; \vdots &amp;#x26; \vdots &amp;#x26; \ddots &amp;#x26; \ddots
\end{pmatrix}
$$&lt;/p&gt;
&lt;h4&gt;平稳分布&lt;/h4&gt;
&lt;p&gt;设平稳分布为 $\pi = (\pi_0, \pi_1, \pi_2, \ldots)$，则需满足 $\pi Q = 0$，即：&lt;/p&gt;
&lt;p&gt;对于 $i = 0$：$-\lambda_0 \pi_0 + \mu_1 \pi_1 = 0$&lt;/p&gt;
&lt;p&gt;对于 $i \geq 1$：$\lambda_{i-1} \pi_{i-1} - (\lambda_i + \mu_i) \pi_i + \mu_{i+1} \pi_{i+1} = 0$&lt;/p&gt;
&lt;p&gt;这可以简化为&lt;strong&gt;细致平衡条件&lt;/strong&gt;：
$$
\lambda_i \pi_i = \mu_{i+1} \pi_{i+1}, \quad i \geq 0
$$&lt;/p&gt;
&lt;p&gt;递推可得：
$$
\pi_n = \pi_0 \prod_{k=0}^{n-1} \frac{\lambda_k}{\mu_{k+1}}, \quad n \geq 1
$$&lt;/p&gt;
&lt;p&gt;由归一化条件 $\sum_{n=0}^{\infty} \pi_n = 1$，得到：
$$
\pi_0 = \frac{1}{1 + \sum_{n=1}^{\infty} \prod_{k=0}^{n-1} \frac{\lambda_k}{\mu_{k+1}}}
$$&lt;/p&gt;
&lt;p&gt;当且仅当级数 $\sum_{n=1}^{\infty} \prod_{k=0}^{n-1} \frac{\lambda_k}{\mu_{k+1}}$ 收敛时，存在平稳分布。&lt;/p&gt;
&lt;h1&gt;高斯过程&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;设随机过程$X(t)$，如果$\forall n, \forall t_1, t_2, \ldots, t_n \in [0, T]$，都有
$$
(X(t_1), X(t_2), \ldots, X(t_n))
$$
服从$n$元高斯分布，则称$X(t)$为&lt;strong&gt;高斯过程&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;单元高斯分布&lt;/h3&gt;
&lt;p&gt;设随机变量 $X$ 服从单元高斯分布，记作
$$
X \sim \mathcal{N}(\mu, \sigma)
$$
其中 $\mu$ 是均值，$\sigma$ 是标准差。则其概率密度函数为：
$$
f_X(x) = \frac{1}{\sqrt{2\pi}\sigma} \exp\left(-\frac{(x - \mu)^2}{2\sigma^2}\right)
$$&lt;/p&gt;
&lt;h3&gt;多元高斯分布&lt;/h3&gt;
&lt;p&gt;设随机变量$X = (X_1, X_2, \ldots, X_n)$服从多元高斯分布，记作
$$
X \sim \mathcal{N}(\mu, \Sigma)
$$
其中 $\mu$ 是均值向量，$\Sigma$ 是协方差矩阵。则其概率密度函数为：
$$
f_X(x) = \frac{1}{(2\pi)^{n/2}|\Sigma|^{1/2}} \exp\left(-\frac{1}{2}(x - \mu)^T \Sigma^{-1} (x - \mu)\right)
$$&lt;/p&gt;
&lt;h4&gt;特征函数&lt;/h4&gt;
&lt;p&gt;多元高斯过程的特征函数为：
$$
\phi_X(\omega) = \exp\left(j \omega^T \mu - \frac{1}{2} \omega^T \Sigma \omega\right)
$$
当结论记下来就好&lt;/p&gt;
&lt;h4&gt;性质&lt;/h4&gt;
&lt;h5&gt;线性性&lt;/h5&gt;
&lt;p&gt;设 $X \sim \mathcal{N}(\mu, \Sigma)$，$A$ 是常数矩阵，则 $AX + b \sim \mathcal{N}(A\mu + b, A\Sigma A^T)$。&lt;/p&gt;
&lt;p&gt;这意味着对一个多元高斯分布它的&lt;strong&gt;线性组合&lt;/strong&gt;以及它的任何一个&lt;strong&gt;子向量&lt;/strong&gt;均服从高斯分布&lt;/p&gt;
&lt;h5&gt;独立性&lt;/h5&gt;
&lt;p&gt;设 $X \sim \mathcal{N}(\mu, \Sigma)$，$Y \sim \mathcal{N}(\nu, \Lambda)$&lt;/p&gt;
&lt;p&gt;有
$$
\Sigma_{11} = \text{Cov}(X_1, X_1) = E((X_1-\mu_1)(X_1-\mu_1)^T)
$$
$$
\Sigma_{22} = \text{Cov}(X_2, X_2) = E((X_2-\mu_2)(X_2-\mu_2)^T)
$$
$$
\Sigma_{12} = \text{Cov}(X_1, X_2) = E((X_1-\mu_1)(X_2-\mu_2)^T) = \Sigma_{21}^T
$$&lt;/p&gt;
&lt;p&gt;如果 $\Sigma_{12} = 0$，则 $X_1$ 和 $X_2$ 独立。&lt;/p&gt;
&lt;h6&gt;例子&lt;/h6&gt;
&lt;p&gt;设 $X = (X_1, X_2)^T$，求一个常系数矩阵 $A$ 使得 $Y=AX$ 的各组分.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解&lt;/strong&gt;
设
$$
A = \begin{pmatrix}
I &amp;#x26; A&apos; \
0 &amp;#x26; I
\end{pmatrix}
$$
其中 $I$ 是单位矩阵，$A&apos;$ 是任意矩阵。
则
$$
Y = AX = \begin{pmatrix}
X_1 + A&apos;X_2 \
X_2
\end{pmatrix}
$$
要使得$Y$ 的各组分独立，则需要满足
$$
\text{Cov}(X_1 + A&apos;X_2, X_2) = Cov(X_1, X_2) + A&apos; \text{Cov}(X_2, X_2) = 0
$$
这意味着
$$
A&apos; \Sigma_{22} + \Sigma_{12} = 0
$$
因此
$$
A&apos; = -\Sigma_{12} \Sigma_{22}^{-1}
$$&lt;/p&gt;
&lt;h2&gt;布朗运动&lt;/h2&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;标准布朗运动（Brownian motion）是一个特殊的高斯过程，满足以下条件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$B(0) = 0$，即初始位置为零。&lt;/li&gt;
&lt;li&gt;平稳增量，即 $B(t) - B(s) \sim B(t-s)$&lt;/li&gt;
&lt;li&gt;对任意$t$，$B(t)$ 服从正态分布&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;性质&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;期望
$$
E[B(t)] = 0
$$&lt;/li&gt;
&lt;li&gt;方差
$$
\text{Var}(B(t)) = t
$$&lt;/li&gt;
&lt;li&gt;协方差
$$
\text{Cov}(B(t), B(s)) = \min(t, s)
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里尤其需要注意协方差的计算，有一个常用的技巧
$$
\text{Cov}(B(t), B(s)) = E[B(t)B(s)] - E[B(t)]E[B(s)] = E[B(t)B(s)]
$$
因为 $E[B(t)] = E[B(s)] = 0$。&lt;/p&gt;
&lt;p&gt;然后我们可以利用技巧得到
$$
E[B(t)B(s)] = E(B(t)[B(s)-B(t)+B(t)]) = E[B(t)[B(s)-B(t)]] + E[B(t)^2]
$$&lt;/p&gt;
&lt;p&gt;这就让我们可以用独立增量的性质来计算协方差。&lt;/p&gt;
&lt;p&gt;$$
\text{Cov} = E(B(t))E(B(s-t)) + E(B(t)^2) = 0 + t = t
$$&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;布朗运动几乎处处连续，但是几乎处处不可微。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>