并发编程Concurrent Programming**是全书的最后一章也是将之前所有知识硬件、操作系统、网络、I/O综合运用的一章。这一章的核心目标是如何利用并发技术构建高效、健壮的网络服务器和应用程序。1. 并发的三种基本构造方法CSAPP 介绍了在系统中实现并发的三种主要机制它们各有优劣A. 基于进程Process-based Concurrency原理利用fork()系统调用创建子进程。每个进程有独立的地址空间。优点隔离性好一个进程崩溃不会影响其他进程。共享简单通过文件描述符继承共享状态如监听套接字。缺点开销大:fork和上下文切换成本高。通信困难: 进程间通信IPC需要显式机制管道、信号、共享内存比线程复杂。典型应用早期的 Web 服务器如 Apache prefork 模式。B. 基于 I/O 多路复用I/O Multiplexing / Event-driven原理单线程中使用select、poll或epoll监控多个文件描述符的状态。当某个描述符就绪如可读/可写时事件循环处理它。优点粒度细比进程更精细的控制。无同步问题单线程执行无需锁。缺点编码复杂需要将程序逻辑分解为状态机代码难以阅读和维护。无法利用多核单线程只能在一个 CPU 核心上运行。阻塞风险: 如果某个事件处理函数中有耗时操作如磁盘 I/O整个服务器都会停滞。典型应用Nginx, Node.js, Redis。C. 基于线程Thread-based Concurrency原理使用pthread库创建轻量级线程。线程共享同一进程的地址空间代码、数据、堆、打开的文件但拥有独立的栈和寄存器。优点逻辑简单代码结构类似串行程序易于编写和理解。共享方便全局变量天然共享。性能适中创建和切换开销小于进程。缺点同步复杂需要小心处理竞态条件Race Conditions使用互斥锁、信号量等同步原语。调试困难并发 Bug 难以复现。典型应用现代大多数高性能服务器通常结合线程池使用。2. 线程基础POSIX Threads创建线程pthread_create(tid, NULL, thread_func, arg)终止线程:隐式线程函数返回。显式调用pthread_exit(retval)。被取消其他线程调用pthread_cancel(tid)。回收线程pthread_join(tid, retval)。类似于进程的waitpid防止产生“僵尸线程”。分离线程pthread_detach(tid)。线程结束后自动释放资源无需 join。3. 基于信号的并发问题在多线程环境中信号处理变得极其复杂信号传递信号通常传递给进程中的任意一个线程而不是特定线程。屏蔽信号每个线程有独立的信号掩码。最佳实践主线程负责接收信号如 SIGINT, SIGCHLD。工作线程屏蔽所有信号。或者使用sigwait在专用线程中同步等待信号。4. 共享变量与竞态条件这是并发编程中最危险的部分。线程内存模型寄存器私有。栈私有。堆共享。数据段 (.data, .bss)共享。代码段 (.text)共享只读。竞态条件Race Condition当多个线程访问共享变量且至少有一个线程进行写操作且没有适当的同步机制时结果取决于线程执行的相对速度调度顺序导致不可预测的行为。经典例子两个线程同时对全局计数器cnt进行操作。// 汇编层面// 1. Load cnt to register// 2. Increment register// 3. Store register back to cnt如果线程 A 执行完步骤 2 后被切换线程 B 也执行完步骤 2然后两者分别执行步骤 3最终cnt只增加了 1而不是 2。5. 用信号量同步线程SemaphoresCSAPP 重点介绍了信号量作为通用的同步工具。定义信号量s是一个非负整数全局变量支持两个原子操作P(s)(Wait/Down): 如果s 0则s--并立即返回如果s 0则挂起线程直到s变为正数。V(s)(Signal/Up):s。如果有线程在 P(s) 上等待唤醒其中一个。用途互斥Mutual Exclusion保护临界区。sem_tmutex;sem_init(mutex,0,1);// 初始值为1的二值信号量// 临界区前P(mutex);// ... 访问共享资源 ...V(mutex);调度Scheduling协调线程执行顺序例如生产者-消费者问题。empty信号量记录空闲缓冲区数量。full信号量记录已填充缓冲区数量。生产者-消费者问题Producer-Consumer Problem这是并发编程的 Hello World。缓冲区环形缓冲区。互斥锁保护缓冲区的索引指针。信号量slots: 初始化为缓冲区大小 N表示可用空间。items: 初始化为 0表示可用数据。逻辑生产者P(slots)-P(mutex)- 放入数据 -V(mutex)-V(items)消费者P(items)-P(mutex)- 取出数据 -V(mutex)-V(slots)6. 使用线程提高并行性基于线程的服务器设计动态创建线程每来一个连接accept就pthread_create一个新线程处理。缺点频繁创建销毁线程开销大容易遭受 DoS 攻击。线程池Thread Pool预先创建一组工作线程。主线程接受连接后将连接描述符放入任务队列工作线程从队列获取任务处理。优点复用线程控制并发度稳定性高。并行性 vs 并发性并发Concurrency逻辑上的同时单核交替执行。并行Parallelism物理上的同时多核真正同时执行。线程使得程序可以利用多核 CPU 实现真正的并行计算。7. 其他同步机制互斥锁Mutex Lockspthread_mutex_t。比信号量更轻量专门用于互斥。读写锁Reader-Writer Locks允许多个读者同时访问但写者独占。适合读多写少的场景。条件变量Condition Variablespthread_cond_t。配合互斥锁使用用于线程间的等待/通知机制比信号量更灵活避免虚假唤醒问题。8. 并发 Bug 的类型安全失败Safety Failure程序产生了错误的结果如竞态条件导致数据损坏。活性失败Liveness Failure程序无法继续执行。死锁Deadlock一组线程互相等待对方持有的资源。四个必要条件互斥、持有并等待、不可抢占、循环等待。预防按固定顺序获取锁。活锁Livelock线程不断重试但无法进展。饥饿Starvation某些线程长期得不到调度。总结第 12 章强调了并发是复杂的必须仔细设计同步机制。线程是主流在现代多核系统中基于线程的并发是性能和编程复杂度的最佳平衡点。信号量是核心原语理解 P/V 操作是解决同步问题的关键。线程池是最佳实践在生产环境中永远不要为每个请求动态创建线程务必使用线程池。结合前面的章节你现在可以构建一个完整的、高并发的 Web 服务器或代理服务器Proxy Lab 的终极目标使用Socket I/O(Ch 11) 接收请求。使用线程池(Ch 12) 处理并发。使用互斥锁/信号量(Ch 12) 保护共享缓存或日志。使用虚拟内存(Ch 9) 管理大规模数据映射。底层依靠异常控制流(Ch 8) 和硬件中断(Ch 7) 驱动。