
文章目录一、进程与线程关系和区别进程和线程有什么区别什么时候用多进程什么时候用多线程多线程与多进程对比二、进程的状态、切换与创建fork() 创建子进程时发生了什么三、进程间通信数据怎么穿过隔离边界1. 管道匿名管道与命名管道 FIFO2. 消息队列3. 共享内存数据少搬一次但同步不能省4. 套接字本机和跨机器都能统一处理5. 信号适合通知不适合传大块数据四、线程同步共享数据怎样安全访问1. 互斥锁保护共享状态互斥锁和自旋锁怎么选2. 读写锁读可以并发写必须独占3. 条件变量条件不满足时先睡眠条件变量为什么要配合互斥锁还要用 while 判断4. 原子变量适合简单、独立的共享状态5. 内存屏障约束顺序和可见性6. 信号量管理可用资源数量7. 生产者—消费者模型互斥锁与信号量协作五、并发问题竞态、死锁与优先级反转1. 什么是竞态条件2. 死锁的四个必要条件哪些代码容易产生死锁3. 优先级反转六、几个容易答错的追问1. 一个线程崩溃会不会影响整个进程2. 共享变量加了 volatile是否就线程安全3. fork() 之后为什么只有一个线程4. 线程越多并发能力越强吗七、一分钟面试回答八、高频面试题精选在 C 面试中进程与线程几乎是绕不开的高频基础题看似只问两者的区别面试官往往还会继续追问进程通信、线程同步、死锁和内存模型。想把这类问题答完整关键不是背定义而是真正理解资源归谁管理、CPU 调度谁运行以及不同执行流之间如何协作。一、进程与线程关系和区别进程是什么正在运行的程序实例也是资源管理和地址空间隔离的容器。不同进程通常拥有彼此独立的虚拟地址空间。线程是什么进程内部的一条执行流属于进程也是操作系统实际调度的重要实体。同一进程的线程共享什么代码段、全局变量和静态变量、堆内存、打开的文件和 Socket 等进程资源。每个线程各自有什么线程 ID、程序计数器、寄存器、线程栈、调度状态和线程局部存储TLS。核心关系只有一句进程之间默认隔离同一进程的线程共享进程资源但各自保留执行上下文。进程和线程有什么区别对比项进程线程基本定位资源管理和地址空间隔离的容器进程内部的执行流地址空间不同进程通常彼此独立同一进程内共享资源归属拥有内存、文件等进程级资源使用所属进程的资源独有状态地址空间、权限和进程管理信息等线程 ID、程序计数器、寄存器、线程栈和 TLS 等数据交换需要管道、共享内存、套接字等 IPC可以直接访问共享数据但必须正确同步创建与切换通常开销更大还可能涉及地址空间切换通常更轻量故障影响隔离性较强一个进程退出通常不直接破坏其他进程内存一个线程破坏共享内存可能影响整个进程什么时候用多进程什么时候用多线程多线程的核心优势开销小共享数据方便同一进程中的线程共享地址空间可以直接访问堆中的对象创建和切换通常也比进程更轻。它适合任务联系紧密、需要频繁交换数据的场景例如线程池处理并发请求、后台线程执行耗时任务、多个工作线程共同处理一批内存数据。代价是共享数据必须正确同步。锁竞争、数据竞争或某个线程破坏内存都可能影响整个进程。因此多线程的重点不只是“并发”还要保证共享状态和对象生命周期足够清晰。多进程的核心优势隔离性强故障边界清楚不同进程通常拥有独立的地址空间。一个进程崩溃、越界访问或出现资源泄漏时通常不会直接破坏另一个进程的内存因此更适合强调稳定性、安全性、权限隔离和独立部署的场景。例如浏览器会用不同进程隔离部分页面和功能模块服务端也常用多个工作进程承载请求。需要跨机器运行时任务更要拆成独立进程或服务通过套接字、RPC 或消息系统通信。代价是创建、切换和进程间通信通常更重。多线程与多进程对比特性多线程多进程核心优势创建和切换通常更轻共享数据方便隔离性强故障边界和权限边界清楚通信方式直接读写共享内存但必须做好同步使用管道、消息队列、共享内存或套接字等 IPC资源开销通常较小共享所属进程的大部分资源通常较大每个进程拥有独立地址空间和进程资源故障影响一个线程破坏共享状态可能影响整个进程一个进程退出通常不会直接破坏其他进程的内存典型场景高并发 I/O、GUI 后台任务、需要频繁共享数据的并行任务浏览器隔离、独立服务、不同权限模块、多进程并行计算核心总结频繁共享数据、任务联系紧密优先考虑多线程更看重隔离、稳定性和独立部署优先考虑多进程。实际工程常用多进程划分故障边界再在每个进程内部使用多线程处理并发。二、进程的状态、切换与创建进程有五个基本状态创建进程正在建立操作系统为它分配必要的管理信息和资源就绪已经具备运行条件正在等待 CPU运行正在 CPU 上执行阻塞或等待正在等待 I/O、锁、条件或其他事件终止执行已经结束系统正在回收相关资源。图中两条返回“就绪”的路径原因不同运行中的进程因时间片耗尽或被抢占而回到就绪队列阻塞中的进程要等事件完成后才重新就绪。简单说就绪缺的是 CPU阻塞缺的是某个事件阻塞不能直接变成运行还要再次等待调度。现代操作系统通常以线程作为 CPU 调度单位只有一个线程的进程也常被简化为“进程在运行”。发生下列情况时当前线程可能让出 CPU并触发调度或上下文切换时间片用完当前线程等待 I/O当前线程等待锁或条件变量更高优先级任务变为可运行当前线程主动让出 CPU。上下文切换本身不完成业务工作。线程数量远大于可用 CPU 核心数时过多切换可能反而降低吞吐量。fork()创建子进程时发生了什么在 Linux 和其他 POSIX 系统中fork()会从当前进程创建一个子进程。调用成功后父子进程都会从fork()返回的位置继续执行但拿到的返回值不同在父进程中fork()返回子进程的 PID也就是一个大于 0 的整数在子进程中fork()返回 0调用失败时返回 -1并且不会创建子进程。因此同一段代码可以通过返回值判断自己当前运行在父进程还是子进程中#includesys/types.h#includeunistd.hpid_t pidfork();if(pid-1){// 创建失败}elseif(pid0){// 子进程执行这里}else{// 父进程执行这里pid 是子进程 ID}从程序视角看子进程获得了父进程地址空间的一份副本包括代码、数据、堆和栈等内容。但现代操作系统通常不会在fork()返回前立刻复制所有物理内存而是采用写时复制Copy-on-WriteCOW。结合图看写时复制分三步fork()完成后父子进程拥有各自的页表但页表项暂时指向同一组物理页相关页被标记为只读和 COW只读访问不会复制。子进程第一次写页 B 时因为页面当前不可写CPU 触发写保护异常并进入内核内核为子进程分配新物理页 B’复制原页 B 的内容再把子进程的页表项改为指向 B’。父进程仍指向原页 B未修改的页 A、页 C 继续共享。因此fork()创建的是独立的虚拟地址空间视图并不等于立即复制全部物理内存。COW 按内存页复制即使只修改一个变量也会复制这个变量所在的整页。子进程还会继承父进程已经打开的文件描述符等运行状态。父子进程中的描述符通常引用同一个内核打开文件对象因此文件偏移和状态处理需要格外注意。后面的匿名管道示例正是利用fork()前创建的管道描述符让父子进程分别保留读端和写端。fork()只负责创建子进程如果子进程要运行另一个程序通常会继续调用exec系列函数用新程序替换当前进程映像。常见流程可以概括为父进程fork()子进程exec()父进程通过wait()或waitpid()等待并回收子进程。三、进程间通信数据怎么穿过隔离边界进程地址空间默认隔离因此进程间通信需要操作系统提供通道或者显式建立一块可共同访问的区域。不同 IPC 解决的问题并不相同可以先用下面这张表建立整体认识。表中的信号量属于进程间同步机制通常不传递业务数据但经常与共享内存配合使用。通信方式工作方式主要优势局限与注意点适合场景匿名管道由内核维护字节流缓冲区通常在父子进程间传递数据接口简单创建后即可读写默认单向、没有消息边界通常要求进程具有亲缘关系父子进程传递少量连续数据、连接命令行程序命名管道 FIFO在文件系统中提供一个可打开的管道入口无亲缘关系的本机进程也能使用接入方式直观仍以字节流为主吞吐量和协议能力有限双方要约定读写规则本机简单服务、脚本与后台进程通信共享内存将同一组物理内存页映射到多个进程的地址空间数据不必反复经过内核缓冲区搬运适合大块数据只解决数据共享不负责互斥、顺序和一致性设计与清理更复杂图像、音视频、模型数据等高吞吐交换消息队列发送方按消息写入队列接收方按协议取出保留消息边界便于异步处理和解耦队列容量、单条消息大小和传输开销受实现限制任务分发、事件通知、多生产者与多消费者信号量通过计数变化表示许可数量或事件状态能限制并发数量也能建立进程间的执行顺序通常不携带业务数据使用顺序错误可能造成死锁或永久等待共享内存同步、资源池和并发数量控制套接字通过 Unix 域或 TCP、UDP 等协议双向收发数据既能用于本机也能扩展到跨机器通信接口通用需要设计应用层协议网络通信还要处理超时、断线和重试客户端—服务端、跨语言服务、分布式系统信号操作系统向目标进程发送异步事件通知开销较小适合打断或通知进程状态变化可携带信息很少处理函数中能安全执行的操作受到严格限制终止、重载配置、子进程状态通知文件映射将文件内容映射到虚拟地址空间多个进程访问同一份映射适合大文件随机访问也能借助文件实现共享必须考虑刷新时机、同步协议、文件生命周期和异常恢复大文件处理、持久化共享数据、只读资源共享不存在“任何情况下最快、最好”的 IPC。选择前先问四个问题通信双方是否在同一台机器需要传输的是字节流、结构化消息还是大块数据是否需要双向通信、多对多通信或跨语言是否需要持久化、访问控制和明确的消息边界1. 管道匿名管道与命名管道 FIFO匿名管道和命名管道都由内核维护字节流缓冲区数据按写入顺序流动但没有天然的消息边界。主要区别在于通信双方怎样找到同一条管道。对比项匿名管道Pipe命名管道FIFO通信对象通常用于有亲缘关系的进程例如父子进程同一台机器上无亲缘关系的进程也能使用标识方式通过fd[0]和fd[1]两个文件描述符访问通过文件系统路径访问例如/tmp/myfifo创建方式调用pipe()创建调用mkfifo()或使用mkfifo命令创建生命周期所有相关文件描述符关闭后内核管道对象被释放路径会保留到执行unlink()或手动删除缓冲数据不会持久化到磁盘通信方向一条管道通常负责一个方向双向通信一般使用两条一条 FIFO 通常负责一个方向双向通信一般使用两个 FIFO数据缓冲使用内核缓冲区容量有限且取决于系统实现使用内核缓冲区容量有限且取决于系统实现典型场景父子进程传输数据、Shell 管道本机客户端与服务端、脚本与后台进程通信主要特点创建简单不需要管理路径但通常依赖文件描述符继承无亲缘关系的进程也能通过路径会合但要管理路径、权限和清理匿名管道没有文件系统路径。在 Linux 和其他 POSIX 系统中pipe()返回读端fd[0]和写端fd[1]父进程通常在fork()前创建管道让子进程继承这两个描述符。FIFO 的路径只负责让通信双方找到同一个管道并不保存业务数据。进程通过路径打开 FIFO 后数据仍然在内核缓冲区中传递。下面演示父进程向子进程发送一段文本#includecstring#includeiostream#includesys/wait.h#includeunistd.hintmain(){intfd[2];if(pipe(fd)-1){return1;}pid_t pidfork();if(pid-1){return1;}if(pid0){close(fd[1]);// 子进程只读charbuffer[128]{};ssize_t nread(fd[0],buffer,sizeof(buffer)-1);if(n0){buffer[n]\0;std::coutchild received: bufferstd::endl;}close(fd[0]);_exit(0);}close(fd[0]);// 父进程只写constchar*messagehello from parent;write(fd[1],message,std::strlen(message));close(fd[1]);waitpid(pid,nullptr,0);}示例中父进程关闭读端子进程关闭写端。未使用的端点必须及时关闭否则接收方可能一直等不到 EOF。2. 消息队列消息队列强调的是“消息”而不只是连续字节流。发送方把一条条消息放入队列接收方按约定取出。与管道相比它通常更适合需要消息边界、优先级或异步解耦的场景。需要注意的是“消息队列”既可能指操作系统提供的 POSIX/System V 消息队列也可能指 RabbitMQ、Kafka 等独立中间件。两者的部署方式、持久化能力和适用范围完全不同面试时应先确认语境。3. 共享内存数据少搬一次但同步不能省共享内存把同一组物理内存页映射到多个进程的地址空间。进程可以直接读写这块区域因此很适合交换大块数据。进程 A 和进程 B 仍然拥有独立的虚拟地址空间共享区域在两个进程中的虚拟地址也可以不同。操作系统通过页表把两段虚拟地址映射到同一组物理内存页。A 写入后B 访问自己的映射区就能读到同一份数据不需要像管道那样反复中转和复制。共享内存减少了管道、消息队列反复搬运大块数据的开销因此吞吐量通常很高。但它只解决“双方都能看到数据”不负责读写顺序、并发覆盖和内存可见性仍要配合进程共享互斥锁、信号量、原子操作或无锁数据结构。相关进程可以使用匿名共享映射无亲缘关系的进程通常通过shm_open()等方式建立具名共享内存并处理好初始化、权限和清理。4. 套接字本机和跨机器都能统一处理套接字适合双向通信也便于扩展成客户端—服务端模型。Unix 域套接字用于同一台机器不经过完整的 IP 网络路径TCP 提供可靠、有序的字节流UDP 提供数据报不保证送达、顺序或去重。TCP 是字节流也没有业务消息边界。一次send()不一定对应接收端的一次recv()。应用层必须设计长度字段、分隔符或固定长度协议处理所谓的粘包、拆包问题。RPC 不是另一种物理传输介质。它是一种让远程调用看起来更像本地函数调用的通信模型底层通常仍建立在 TCP、HTTP、HTTP/2 或其他传输协议上。5. 信号适合通知不适合传大块数据信号更像一个异步事件通知机制例如通知进程终止、重新加载配置或子进程状态变化。普通信号能携带的信息很少而且信号处理函数的执行环境受限。信号处理函数里只能调用异步信号安全的函数不能随意加锁、分配内存或使用大部分 C 标准库组件。因此不要把信号当成普通消息队列使用。四、线程同步共享数据怎样安全访问同一进程的线程天然共享地址空间所以它们交换数据通常不需要传统 IPC。一个线程写入堆上的对象另一个线程只要拿到地址就可以访问。真正的问题不是“能不能看到”而是访问期间对象是否还活着多个线程会不会同时修改一个线程写完后另一个线程何时能看见条件不满足时线程应该忙等还是睡眠。因此线程通信往往和线程同步同时出现。常用线程同步工具的定位如下同步方式C 标准库支持解决的问题主要优势局限与注意点互斥锁std::mutex、std::recursive_mutex、std::timed_mutexC11保证临界区在同一时刻只被一个线程执行语义直接适合保护容器和多个变量组成的不变量持锁范围过大影响并发加锁顺序不一致可能死锁递归互斥锁应谨慎使用条件变量std::condition_variable、std::condition_variable_anyC11条件不满足时阻塞等待状态变化后再唤醒避免忙等适合任务队列和生产者—消费者模型必须与互斥锁和条件谓词配合需要防止虚假唤醒和丢失通知信号量std::counting_semaphore、std::binary_semaphoreC20用许可数量限制同时访问资源的线程数比单纯互斥更适合资源池、并发限流和事件计数不直接保护复杂数据结构许可数量管理错误会导致永久等待原子操作std::atomicTC11对单个共享状态执行不可分割的读、写或读改写开销通常较小适合计数器、状态位和指针发布多个变量之间的联合约束仍可能需要锁错误的内存序很难排查读写锁std::shared_mutexC17std::shared_timed_mutexC14允许多个读线程并发写线程独占访问读多写少时可以提高并发度写操作可能长期等待读写比例不合适时开销可能高于普通互斥锁内存屏障std::memory_order、std::atomic_thread_fenceC11约束内存访问顺序并建立可见性适合无锁发布和跨核心状态同步不能代替互斥锁内存序错误很难排查线程屏障std::barrierC20让一组线程在阶段边界等待全部到达后再继续适合分阶段并行算法能清楚表达批次同步点参与线程数和退出协议必须设计清楚任一线程未到达都会阻塞其他线程1. 互斥锁保护共享状态#includemutexstd::mutex mutex;intcounter0;voidincrease(){std::lock_guardstd::mutexlock(mutex);counter;}互斥锁保证同一时刻只有一个线程进入临界区。它保护的是一组不变量而不只是某一行代码。判断锁粒度时要看哪些操作必须作为一个整体完成。互斥锁和自旋锁怎么选两者都用于互斥核心区别是没拿到锁时怎么等互斥锁让线程阻塞自旋锁让线程忙等。特性互斥锁自旋锁核心机制阻塞与唤醒循环检查锁状态等待状态线程通常进入阻塞状态线程保持可运行状态CPU 占用阻塞期间基本不占用 CPU等待期间持续占用 CPU上下文切换可能发生阻塞、唤醒和上下文切换通常避免因等锁而睡眠使用场景持锁时间较长、不确定或临界区可能阻塞持锁时间极短且临界区不会阻塞适用环境用户态程序和内核都常见主要用于多核环境下的内核、运行时和底层并发代码典型风险调度开销和死锁长时间空转浪费 CPU实际互斥锁可能先短暂自旋再阻塞C 标准库目前没有通用的std::spinlock需要时通常使用平台原语或成熟并发库。2. 读写锁读可以并发写必须独占读写锁允许多个线程同时读取共享资源但写入时必须独占。它的规则可以概括为读—读不互斥读—写互斥写—写互斥。C17 通常使用std::shared_mutex实现读写锁读取操作使用std::shared_lock获取共享锁写入操作使用std::unique_lock获取独占锁。#includemutex#includeshared_mutexclassConfig{public:intread()const{std::shared_lockstd::shared_mutexlock(mutex_);returnvalue_;}voidwrite(intvalue){std::unique_lockstd::shared_mutexlock(mutex_);value_value;}private:mutablestd::shared_mutex mutex_;intvalue_0;};读写锁适合读操作明显多于写操作、并且临界区有一定工作量的场景。如果写入频繁或临界区很短读写锁自身的管理开销可能高于普通互斥锁具体实现还可能让写线程长期等待。3. 条件变量条件不满足时先睡眠条件变量为什么要配合互斥锁还要用while判断先记住一句话条件变量负责等待和通知互斥锁负责保护真正的共享条件。以消费者等待队列非空为例真正的条件是“队列不为空”而不是“是否收到过一次通知”。通知只是在提醒等待线程共享状态可能发生了变化请醒来重新检查。队列是否为空仍然要在互斥锁保护下读取。不带谓词的写法通常是std::unique_lockstd::mutexlock(mutex_);while(queue_.empty()){notEmpty_.wait(lock);}intvaluequeue_.front();queue_.pop();wait(lock)内部有一个非常关键的过程它会原子地释放互斥锁并进入等待状态。如果先手动解锁再单独调用等待函数生产者可能恰好在这两个操作之间放入数据并发出通知消费者随后才开始睡眠于是就可能错过这次通知。线程被唤醒后wait()会先重新获得互斥锁然后才返回并再次检查条件。为什么必须再次检查而不能用if主要有两个原因虚假唤醒线程可能在没有对应通知的情况下从wait()返回这是条件变量允许的行为条件可能再次失效消费者收到通知后不一定立即拿到互斥锁。另一个消费者可能先一步取走数据等当前线程获得锁时队列又空了。C 提供的带谓词重载已经封装了这段循环notEmpty_.wait(lock,[this]{return!queue_.empty();});它在语义上等价于前面的while循环。工程代码优先使用这种写法不容易漏掉重新检查。生产者则要先在同一把互斥锁保护下修改共享状态再发送通知通知不是状态本身真正可靠的是被锁保护的条件。4. 原子变量适合简单、独立的共享状态如果共享状态只是一个计数器或标志位可以考虑std::atomic#includeatomicstd::atomicboolstopped{false};voidrequestStop(){stopped.store(true);}boolshouldStop(){returnstopped.load();}原子操作不是“给任意代码自动加锁”。当多个变量之间存在联合约束时一组原子变量未必能维护整体不变量互斥锁可能更清晰、更可靠。内存序也不能只靠名称猜。除非已经证明更弱的内存序正确否则优先使用默认的顺序一致性或者由熟悉并发内存模型的人完成设计和评审。5. 内存屏障约束顺序和可见性多核处理器中每个核心都有自己的寄存器和缓存。编译器或 CPU 可能调整指令顺序一个核心的写入也可能暂时停留在本地缓存中导致其他核心不能立即看到最新数据。如图所示内存屏障用于限制特定的指令重排它与原子同步操作配合可以建立跨线程的内存可见性。它并不意味着执行屏障后所有写入都会立即对所有核心可见。典型场景是线程 A 先写入数据再发布“数据已就绪”的标志线程 B 读到该标志后才读取数据。C 通常使用原子的release/acquire内存序建立这种关系#includeatomicintdata0;std::atomicboolready{false};voidproduce(){data42;ready.store(true,std::memory_order_release);}voidconsume(){if(ready.load(std::memory_order_acquire)){intobserveddata;// observed 42}}当线程 B 的acquire读取到了线程 A 通过release发布的值时A 在发布之前的写入对 B 可见。内存屏障只保证有序性和可见性不提供互斥也不能代替锁。6. 信号量管理可用资源数量互斥锁回答“谁能进入临界区”计数信号量回答“还有多少个许可”。例如连接池中有 10 个连接可以把信号量初始值设为 10每借出一个连接消耗一个许可归还时再释放一个许可。信号量可以限制并发数量或完成事件通知但不会自动保护复杂数据结构。数据本身是否安全仍取决于互斥锁或其他访问协议。7. 生产者—消费者模型互斥锁与信号量协作生产者向有界队列写入数据消费者从队列取出数据。这里既要防止多个线程同时破坏队列结构也要在队列满或空时让对应线程等待。图中的三个同步对象分工如下mutex保护队列结构防止多个线程同时修改队列empty记录空槽数量初始值为队列容量Nfull记录已有数据数量初始值为0。生产者先获取一个empty许可再加锁写入队列最后释放一个full许可消费者执行相反的过程。缓冲区满时empty为0生产者等待缓冲区空时full为0消费者等待。下面的 C20 代码与图中的mutex empty full完全对应#includecstddef#includemutex#includequeue#includesemaphoreclassBoundedQueue{public:staticconstexprstd::ptrdiff_t Capacity5;voidpush(intvalue){empty_.acquire();// 等待空槽{std::lock_guardstd::mutexlock(mutex_);queue_.push(value);}full_.release();// 已有数据数加一唤醒消费者}intpop(){full_.acquire();// 等待数据intvalue;{std::lock_guardstd::mutexlock(mutex_);valuequeue_.front();queue_.pop();}empty_.release();// 空槽数加一唤醒生产者returnvalue;}private:std::mutex mutex_;std::queueintqueue_;std::counting_semaphoreCapacityempty_{Capacity};std::counting_semaphoreCapacityfull_{0};};信号量负责等待和计数互斥锁负责保护队列本身。两者不能互相替代。五、并发问题竞态、死锁与优先级反转1. 什么是竞态条件如果程序结果依赖多个线程难以控制的执行先后顺序就可能出现竞态条件。例如counter通常包含读取、计算和写回不是一个不可分割的操作。两个线程同时执行可能都读到旧值最终丢失一次更新。数据竞争是 C 内存模型中的更具体概念多个线程并发访问同一内存位置至少一个是写操作并且缺少必要同步。数据竞争会导致未定义行为。2. 死锁的四个必要条件经典死锁需要同时满足互斥资源一次只能被一个执行单元使用占有并等待已经持有资源时继续等待其他资源不可抢占资源不能被外部强行收回循环等待多个执行单元形成首尾相接的等待环。哪些代码容易产生死锁多把锁的获取顺序不一致线程 A 先锁m1再锁m2线程 B 却先锁m2再锁m1两个线程可能各持有一把锁并等待另一把持锁执行阻塞操作拿着锁进行磁盘或网络 I/O、调用join()或者等待另一个任务完成持锁时间不可控嵌套调用中再次加锁持锁调用其他函数、回调或析构逻辑而这些代码又尝试获取当前锁或另一把锁异常路径没有释放锁手动调用lock()后因为异常、提前return或错误分支漏掉unlock()持锁等待依赖自己的事件线程持有某把锁等待通知而负责产生通知的线程也必须先获得这把锁。这些写法的共同点是线程持有资源的同时继续等待其他资源或事件最终可能形成循环等待。死锁的处理可以分成四种思路预防死锁通过资源分配规则提前破坏四个必要条件中的至少一个避免死锁分配资源前判断系统是否仍处于安全状态银行家算法是经典例子检测死锁允许死锁发生再通过等待图或检测算法识别等待环解除死锁检测到死锁后终止或回滚部分任务必要时回收资源使等待环被打破。应用开发中最常用的是预防具体做法包括对多把锁规定统一的获取顺序使用std::scoped_lock一次锁住多把互斥锁减少持锁范围不在持锁期间执行不可控回调或阻塞 I/O必要时使用超时和失败回退用线程分析工具和压力测试发现低概率问题。#includemutexstd::mutex first;std::mutex second;voidupdateBoth(){std::scoped_locklock(first,second);// 同时安全访问两把锁保护的数据}3. 优先级反转优先级反转是指高优先级线程等待低优先级线程持有的锁而低优先级线程又被中优先级线程抢占结果高优先级线程反而迟迟无法运行。图中低优先级线程 L 先获得锁高优先级线程 H 随后需要这把锁只能阻塞。此时中优先级线程 M 抢占 L导致 L 没有机会继续运行并释放锁H 也只能继续等待。最终H 实际上被 M 间接延迟这就是优先级反转。常见解决办法是优先级继承当 H 等待 L 持有的锁时系统临时把 L 的优先级提升到 H 的级别让 L 尽快执行并释放锁之后再恢复原优先级。实时系统中还可能使用优先级上限协议并尽量缩短临界区。六、几个容易答错的追问1. 一个线程崩溃会不会影响整个进程线程共享进程地址空间和资源。某个线程发生未处理异常、非法内存访问或破坏共享数据时通常可能导致整个进程终止或进入错误状态。这也是多进程隔离的重要价值一个工作进程退出不一定要让主进程和其他工作进程一起退出。2. 共享变量加了volatile是否就线程安全不是。在标准 C 中volatile主要用于告诉编译器该对象的访问具有特殊可观察性常见于内存映射 I/O。它不提供普通多线程同步所需的原子性和 happens-before 关系。线程间同步应使用互斥锁、原子变量、条件变量等并发工具。3.fork()之后为什么只有一个线程在 POSIX 系统中多线程进程调用fork()后子进程只复制调用fork()的那个线程。其他线程不会出现在子进程里但它们原来持有的锁状态可能被复制过去。如果某把锁在fork()时由消失的线程持有子进程再去获取它就可能永久阻塞。因此多线程程序在fork()后、exec()前能安全调用的函数受到严格限制。工程中通常尽量避免在复杂多线程状态下随意fork()。4. 线程越多并发能力越强吗不是。CPU 密集任务的有效并行度通常受 CPU 核心数量限制。线程太多会增加调度和缓存开销。I/O 密集任务允许更多线程在部分线程阻塞时继续工作但也受到内存、连接数、下游容量和锁竞争限制。线程池大小应该根据任务特征和实际指标调整而不是套固定公式。七、一分钟面试回答如果面试官让你简单介绍进程、线程和通信机制可以先这样回答**第一进程和线程的定位不同。**进程是操作系统进行资源分配和地址空间隔离的基本单位线程是进程内部的执行单元也是 CPU 调度的基本单位。一个进程中至少有一个线程真正执行代码的是线程。**第二它们的内存关系不同。**不同进程拥有各自独立的虚拟地址空间默认不能直接访问对方的内存同一进程内的线程共享代码段、全局变量、堆内存以及打开的文件和 Socket但每个线程都有自己的线程 ID、程序计数器、寄存器、线程栈和线程局部存储。**第三它们的通信方式不同。**进程之间需要通过管道、消息队列、共享内存或 Socket 等 IPC 通信线程可以直接读写进程内的共享数据但必须通过互斥锁、条件变量或原子操作等机制避免数据竞争。**最后选择时要看场景。**多进程的隔离性和稳定性更好但创建、切换和通信成本通常更高多线程更轻量共享数据更方便但一个线程出错或同步不当可能影响整个进程。这段回答先建立主线后续再根据面试官的问题展开 IPC、上下文切换、条件变量或死锁。八、高频面试题精选进程和线程有什么区别同一进程中的线程共享哪些资源又各自拥有哪些状态常见的进程通信方式和线程同步方式分别有哪些它们各自适合什么场景互斥锁和自旋锁有什么区别什么时候适合使用互斥锁什么时候适合使用自旋锁读写锁的工作机制是什么为什么适合读多写少的场景条件变量为什么必须配合互斥锁使用等待条件时为什么通常要使用while判断什么是死锁死锁产生的四个必要条件是什么工程中如何预防或解决死锁