南京大学-操作系统-多处理器编程(线程库、理解并发程序执行、宽松内存模型)
1. 多处理器编程的入门
- 多处理器编程的基本内容包括线程的创建、理解并发程序的执行机制,以及应对宽松内存模型带来的挑战。在并发编程中,我们必须放弃常见的编程假设,如程序的顺序执行和状态的原子性,这使得并发程序的行为变得复杂和难以预测。
- 编译器和处理器通常会对代码进行优化,这进一步增加了并发编程的复杂性,特别是在涉及多处理器系统时。为了掌握并发编程,除了要具备数学建模的能力,还需借助工具的支持,理解并发程序的真实行为可能与直觉大相径庭。
2. 操作系统与多线程编程模型
- 操作系统是一个状态机的管理者,允许多个程序并发运行。为了正确处理这种并发性,操作系统通过系统调用实现线程调度,确保程序的正确执行。
- 在多线程编程模型中,每个线程拥有独立的堆栈并共享内存。程序员可以通过选择不同线程的执行步骤来定义多线程程序的行为,从而实现高效的并行计算。
3. 多线程编程的简化实现
- 入门多线程编程可以通过简化的线程API来实现。这种方法通过创建状态机并运行相关函数,充分利用多处理器的性能,达到超过100%的CPU利用率。
- 共享内存的多线程编程方式简单易懂,通过递归和指针操作,可以探讨堆栈的大小和位置,同时揭示程序内存分配的特性。
4. 并发编程的挑战与应对
- 并发编程是一个极具挑战的领域,它打破了程序顺序执行的假设,使得程序行为变得复杂和难以预测。程序员必须适应这种新范式,放弃以往顺序编程的经验。
- 并发编程中的细微变化可能引发严重后果,例如简化排序导致的数据破坏、无符号整数运算溢出等问题。对共享变量操作时必须考虑原子性,以避免并发bug。
5. 并发bug的影响与预防
- 并发bug可能导致严重的安全漏洞,例如释放后指针仍指向数据,可能会泄露敏感信息。支付宝案例展示了并发行为的复杂性,突显了并发bug的难以应对。
- 共享内存编程带来了许多挑战,甚至连简单的1+1操作在并发环境下都可能出错。历史上,计算机科学家为实现正确的1+1付出了巨大的努力,显示了并发编程的复杂性。
6. 数学视角在并发编程中的重要性
- 在并发程序设计中,直觉往往不可靠。使用数学视角来解决并发程序问题至关重要,程序员需要证明程序的正确性,以确保其准确性。
- 编译器的优化假设程序的原子性和顺序性,但在并发编程中,这些假设可能导致程序行为难以理解,甚至隐藏微妙的bug。因此,了解编译器和处理器的优化行为对并发程序设计至关重要。
7. 超标量处理器与共享内存系统
- 现代处理器采用超标量流水线技术,通过动态数据流分析,在一个时钟周期内执行多条指令。然而,指令冲突的判断和共享内存系统中的相对论效应,使得处理器看到的内存镜像存在差异,增加了理解全局行为的难度。
- 共享内存系统中的相对论效应不仅挑战了传统的顺序执行模型,还使得model checker等验证工具难以应对,凸显了并发编程的复杂性。
8. 并发程序设计的实际应用
- 在并发程序设计中,通过使用旗子(flag)和线程操作,可以在共享内存环境下实现正确的并发程序。这些技术在UNIX时代也得到了广泛应用,如通过管道和小工具实现统计和排序功能,体现了Unix哲学中的工具组合思想。
- 多处理器编程中,选择合适的内存模型对性能和人类理解之间取得平衡是CPU设计者的挑战。Apple Silicon通过硬件配置模拟X86内存模型,成功解决了这一难题,展示了多处理器编程中的创新与突破。
总结
多处理器编程和并发编程虽然充满挑战,但通过理解其核心概念、掌握适当的工具和方法,并采用数学视角进行验证,程序员可以有效应对这些复杂性,设计出更可靠、高效的并发程序。在实际应用中,不断优化并发编程的实践,将为现代计算机系统带来更强大的性能和更高的安全性。
本博客采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MM's Journal of Technology!