解读《深入理解计算机系统(CSAPP)》第12章并发编程

前言

📫作者简介小明Java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫 

🏆CSDN专家博主/Java领域优质创作者、阿里云专家/签约博主、InfoQ签约博主、华为云专家、51CTO专家🏆

🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~

本文解读

如果逻辑控制流在时间上重叠,那么就称它们是并发(concurrent)的。并发可以看做是一种操作系统内核用来运行多个应用程序的机制,并发不局限于内核。

应用级并发的一些应用场合:(1)访问慢速 I/O 设备。当一个用户等待来自慢速 I/O 设备(比如磁盘)的数据到达时,内核会运行其他进程;(2)与人交互。每次用户请求某种操作时(比如通过点击鼠标),一个独立的并发逻辑流被创建来执行这个操作;(3)通过推迟工作以降低延迟;(4)服务多个网络客户端。一个并发服务器为每个客户端创建一个单独的逻辑流;(5)在多核机器上进行并行计算。被划分称并发流的应用程序通常在多个机器上比单处理器机器上快很多,因为这些流会并行执行,而不是交错执行。

现代操作系统提供了三种基本的构造并发程序的方法:(1)进程。在这种形式下,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用显式的进程间通信(IPC)机制。(2)I/O 多路复用。在这种形式下,应用程序在一个进程的上下文中显式地调用它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。(3)线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。线程像进程流一样由内核进行调度,像 I/O 多路复用一样共享同一个虚拟地址空间。

重点解读:

一、基于进程的并发编程

构造并发程序最简单的方法就是用进程,使用 fork, exec, waitpid 等函数。

优点:父子进程间共享状态信息:共享文件表,但是不共享用户地址空间。这样避免了一个进程覆盖另外一个进程的虚拟内存。
缺点:独立的地址空间使得进程共享状态信息变得更加困难,需要使用进程间通信(IPC)机制。

二、基于I/O多路复用的并发编程

I/O多路复用技术,基本思想就是使用 select 函数,要求内核挂起进程,只有在一个或多个 I/O 事件发生后,才将控制返回给应用程序。

IO多路复用可以用到并发事件驱动程序的基础,在事件驱动程序中某些事件会导致流向前推进,一般的思路是将逻辑流模型转化为状态机,不严格的说一个状态机就是一组状态、输入时间和转移。服务器使用IO多路复用借助select函数检测输入事件的发生,当每个已连接描述符准备好可读时,服务器就为相应的状态机执行转移,在这里就是从描述符读和写回一个文本行。

事件驱动设计的优点:(1)它比基于进程的设计给了程序员更多对程序行为的控制。例如,我们可以编写一个事件驱动的并发服务器,为某些客户端提供它们需要的服务,而对于基于进程的并发服务器来说是很困难的;(2)一个基于IO多路复用的事件驱动服务器试运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间,这使得流之间共享数据变得很容易;(3)一个与作为单个进程运行相关的优点是,你可以利用熟悉的调试工具例如GDB,来调试你的并发服务器,就像对顺序程序那样(4)比基于进程的设计要高效的多,因为他们不需要进程上下文切换来调度新的流。

事件驱动设计的缺点:(1)编码复杂,上面的事件驱动并发echo服务器需要的代码比基于进程的服务器多三倍,随着并发粒度的减小,复杂性还会上升,这里的粒度是指每个逻辑流每个时间片执行的指令数量;(2)不能充分利用多核处理器。

三、基于线程的并发编程

到目前为止,前面看到了两种创建并发逻辑流的方法,第一种方法中,每个流使用了单独的进程,内核会自动调度每个进程,而每个进程有自己的私有地址空间,使流之间共享数据很困难。第二种方法中,创建自己的逻辑流,并利用IO多路复用来显式地调度流。因为只有一个进程,所有的流共享整个地址空间,本节介绍第三种方法,基于线程,它是之前地两种方法的混合。

线程是运行在进程上下文中的逻辑流,在之前程序都是由每个进程中一个线程组成的,现代操作系统允许我们编写一个进程里运行多个线程的程序,线程由内核自动调度。每个线程都有自己的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码,所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。

基于线程的逻辑流结合了基于进程和基于IO多路复用的流的特征,同进程一样,线程由内核自动调度,并且内核通过一个整数ID来识别线程同基于IO多路复用的流一样,多个线程运行在单一进程的上下文中,共享这个进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。

每个进程开始生命周期时都是单一线程,这个线程被称为主线程,在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发的运行,最后主线程执行一个慢速系统调用,例如read或者sleep,或者因为被系统的间隔计时器中断,控制就会通过上下文切换传递到对等线程,对等线程会执行一段时间,然后控制传回主线程,依次类推。

多线程的执行模型在某些方面和多进程的执行模型是相似的。

线程执行在一些方面和进程是不同的,因为一个线程的上下文比一个进程的上下文小得多,线程的上下文切换要比进程快得多。另一个不同的是线程不像进程那样,不是按照严格的父子层次来组织的,和一个进程相关的线程组成一个对等(线程)池,独立于其他线程创建的线程。主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。对等(线程)池的概念主要影响的是一个线程可以杀死它的任何对等线程或者等待它的任意对等线程终止。另外每个对等线程都能读写相同的共享数据。

四、多线程程序中的共享变量

线程存储器模型:一组并发线程运行在一个进程的上下文中。每个线程都有自己独立的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文剩余部分,其中包括整个用户虚拟地址空间,它是由只读文本(代码)、读写数据、堆以及所有的共享库代码和数据区域组成的,线程也共享相同的打开文件集合。

从实际操作角度来说,让一个线程去读写另一个线程的寄存器值是不可能的,另一方面,任何线程都可以访问共享虚拟内存的任意位置,如果某个线程修改了一个内存位置,那么其他每个线程最终都能在它读这个位置时发现这个变化,因此寄存器是不共享的,而虚拟内存总是共享的。

各自独立的线程栈的内存模型不是那么整齐清楚,这些栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立的访问的,我们说的通常不是总是,因为不同的线程栈是不对其他线程设防的,所以,如果一个线程以某一种方式得到一个指向其他线程栈的指针,那么它就可以读写这个栈的任何部分。

多线程的C程序中变量根据它们的存储类型被映射到虚拟内存。(1)全局变量。全局变量是定义在函数之外的变量,在运行时,虚拟内存的读写区域只包含每个全局变量的一个实例,任何线程都可以引用。(2)本地自动变量。本地自动变量就是定义在函数内部但是没有static属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例,即使多个线程执行同一线程例程也是如此。(3)本地静态变量。本地静态变量,是定义在函数内部并有static属性的变量。和全局变量一样,虚拟内存的读写区域只包含在程序中声明的每个本地静态变量的一个实例。例如,即使每个对等线程都声明了cnt,在运行时虚拟内存的读写区域也只有一个cnt的实例,每个对等线程都读写这个实例。

五、用信号量同步线程

共享变量虽然很方便但是会引入同步错误的问题,对于我们而言是没有办法预测操作系统是否将为你的线程选择一个正确的顺序,我们需要借助一种叫作进度图的方法来阐明这些正确或者不正确的指令顺序。

信号量提供了一种很方便的方法来确保对共享变量的互斥访问,基本思想是,将每个共享变量与一个信号量s联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。

除了提供互斥之外,信号量的另一个作用是调度对共享资源的访问,在这种场景中,一个线程用信号量操作来通知另一线程,程序中某个条件已经为真了,两个经典而有用的例子就是生产者-消费者和读者-写者问题。

生产者-消费者问题:

读者-写者问题:读者-写者问题是互斥问题的一个概括,一组并发的线程要访问一个共享对象,例如,一个主存中的数据结构或者一个磁盘上的数据库,有的线程只读对象,而其他的线程只修改对象。修改对象的叫作写者,只读对象的线程叫作读者。写者必须拥有对对象的独占的访问,而读者可以和无线个其他读者共享对象。一般来说,有无线多个并发的读者和写者。

读写者问题有几个变种,分别基于读者和写者的优先级。第一类读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者,换言之,读者不会因为有一个写者在等待而等待。第二类写者优先,要求一旦一个写者准备好可以写,它就会尽可能尽快完成它的写操作。同第一类问题不同,在一个写者后到达的读者必须等待,即使这个写者也在等待。

六、使用线程提高并行性

并发(concurrency)和并行(parallellism)是:解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。

下图是顺序、并发和并行程序之间的几何关系,所有程序的集合能够被划分成不相交的顺序集合和并发程序的集合。并行程序是一个运行在多个处理器上的并发程序。

七、并发问题

线程安全:一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的。
四个(不相交)线程不安全函数(类):(1)不保护共享变量的函数;(2)保持跨越多个调用状态的函数;(3)返回指向静态变量的指针的函数;(4)调用线程不安全函数的函数。

可重入性:有一类重要的线程安全函数,叫作可重入线程安全函数,其特点是,当它们被多个线程调用时,不会引入任何共享数据。尽管线程安全和可重入有时会被当做同义词但是还是有清晰地技术差别。可重入函数集合是线程安全函数的一个真子集。

竞争:当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争。通常发生竞争是因为程序员假定线程将按照某种特殊的轨迹线穿过执行状态空间,而忘记另一条准则规定:多线程的程序必须对任何可行的轨迹线都正确工作。

死锁:信号量引入了一个潜在的运行错误,叫作死锁。它指的是一组线程被阻塞了,等待一个永远不会为真的条件。进度图对于理解死锁是一个很好的工具。下图展示了一对用两个信号量实现互斥的线程的进度图(它是指一组线程被阻塞了,等待一个永远也不会为真的条件

程序员使用P和V操作顺序不当,以至于两个信号量的禁止区域重叠,如果某个执行轨迹线碰巧到达了死锁状态d,那么就不可能有进一步的进展了,因为重叠的禁止区域阻塞了每个合法向上的进展,换句话说,程序死锁是因为每个线程都在等待其他线程执行一个根本不可能发生的V操作。
重叠的禁止区域引起了一组称为死锁区域的状态,如果一个轨迹线碰巧到达了一个死锁区域的状态,那么死锁就不能避免了,轨迹线可以进入死锁区域,但不可能离开。
避免死锁:明确互斥锁加锁的顺序规则,给所有的互斥操作的一个全序,如果每个线程都是以一种顺序获得互斥锁并以相反的顺序释放,那么这个程序就是无锁。

总结

一个并发程序是由在时间上重叠的一组逻辑流组成的。在这一章中,我们学习了三种不同的构建并发程序的机制:进程、IO多路复用和线程。
1、进程是由内核自动调度的,而且因为它们有各自独立的虚拟地址空间,所以要实现共享数据,必须要有显式的IPC机制。
2、事件驱动程序创建它们自己的并发逻辑流,这些逻辑流被模型化状态机,用IO多路复用来显式的调度这些流,因为程序运行在单一进程中,所以在流之间共享数据速度很快而且很容易。线程是这些方法的混合,同基于进程的流一样,线程也是由内核自动调度的,同基于IO多路复用的流一样,线程是运行在单一进程的上下文中的,因此可以快速方便的共享数据。
3、无论哪种并发机制,同步对共享数据的并发访问都是比较困难的,提出信号量的P和V操作就是为了帮助解决这个问题。
4、信号量操作可以用来提供对共享数据的互斥访问,也对比如生产者-消费者程序中有限缓冲区和读写者系统中的共享对象这样的资源访问进行调度。
5、并发也引入了其他一些困难的问题,被线程调用的函数必须具有一种称为线程安全的属性。
6、我们定义了四种不同种类的线程不安全函数,以及一些将它们变为线程安全的建议。可重入函数是线程安全函数的一个真子集,它不访问任何共享数据。可重入函数比不可重入函数更高效,因为它们不涉及任何同步操作。竞争和死锁是并发程序中出现的另一些困难问题,当程序员错误的假设逻辑流如何调度时,就会发生竞争,当一个流等待一个永远不会发生的事件时,就会产生死锁。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 深入理解计算机系统(CSAPP)是由Randal E. Bryant和David R. O'Hallaron编写的经典计算机科学教材。该教材通过涵盖计算机体系结构、机器级别表示和程序执行的概念,帮助学生深入理解计算机系统的底层工作原理和运行机制。 深入理解计算机系统的练习题对于学生巩固并应用所学知识非常有帮助。这些练习题涵盖了计算机硬件、操作系统和编译器等多个领域,旨在培养学生解决实际问题和设计高性能软件的能力。 对于深入理解计算机系统的练习题,关键是通过实践进行学习。在解答练习题时,应根据课本提供的相关知识和工具,仔细阅读问题描述,并根据实际需求设计相应的解决方案。 在解答练习题时,需要多角度思考问题。首先,应准确理解题目要求,并设计合适的算法或代码来解决问题。其次,应考虑代码的正确性和效率,以及对系统性能的影响。此外,还要注意处理一些特殊情况和异常情况,避免出现潜在的错误或安全漏洞。 解答练习题的过程中,应注重查阅相关资料和参考优秀的解答。这可以帮助我们扩展对问题的理解,并学习他人的思路和解决方法。同时,还可以通过与同学和老师的讨论,共同探讨问题和学习经验。 总之,通过解答深入理解计算机系统的练习题,可以帮助学生巩固所学知识,同时培养解决实际问题和设计高性能软件的能力。这是一个学以致用的过程,可以加深对计算机系统运行机制和底层工作原理的理解。 ### 回答2: 理解计算机系统(CSAPP)是一本经典的计算机科学教材,通过深入研究计算机系统的各个方面,包括硬件、操作系统和编程环境,对于提高计算机科学专业知识与能力具有很大帮助。 练习题是CSAPP中的重要部分,通过练习题的完成,可以加深对计算机系统的理解,并将理论知识转化为实践能力。练习题的数量、难度逐渐递增,从简单的概念与基础问题到复杂的系统设计与实现。 在解答练习题时,首先需要对题目进行仔细阅读和理解,明确题目的要求和限制条件。然后,可以利用课堂讲解、教材内容、网络资源等进行查阅和学习相应的知识。同时,还可以参考课后习题解答等资料,了解一些常见的解题方法和思路。 在解答练习题时,可以利用计算机系统的工具和环境进行实际测试和验证。例如,可以使用调试器、编译器和模拟器等工具对程序或系统进行分析和测试。这样可以更加深入地理解问题的本质,并找到恰当的解决方法。 另外,解答练习题时还可以与同学、教师和网上社区进行交流和讨论。这样可以互相学习和交流解题思路,共同解决问题。还可以了解不同的解题方法和技巧,提高解题效率和质量。 练习题的解答过程可能会遇到一些困难和挑战,例如理论知识的不足、复杂问题的分析与解决。但是通过不断地思考和实践,相信可以逐渐提高解题能力,更好地理解计算机系统。 总之,深入理解计算机系统(CSAPP)练习题是提高计算机科学专业知识和能力的重要途径。通过仔细阅读和理解题目,查阅相关知识,利用计算机系统工具和环境进行实践,与他人进行交流和讨论,相信可以更好地理解计算机系统的各个方面,并将知识转化为实际能力。 ### 回答3: 《深入理解计算机系统(CSAPP)》是计算机科学领域的经典教材之一,对于深入理解计算机系统的原理、设计和实现起到了极大的帮助。在阅读这本书的过程中,书中的习题也是非常重要的一部分,通过做习题,我们可以更好地理解书中所讲的概念和思想。 CSAPP的习题涵盖了课本中各个节的内容,从基础的数据表示和处理、程序的机器级表示、优化技术、程序的并发与并行等方面进行了深入探讨。通过解答习题,我们可以对这些知识进行实践应用,巩固自己的理解,并培养自己的解决问题的思维方式。 在解答习题时,我们需要充分理解题目要求和条件,并从知识的角度进行分析。有些习题可能需要进行一些编程实践,我们可以通过编程实现来验证和测试我们的思路和解决方案。在解答问题时,我们还可以查阅一些参考资料和网上资源,充分利用互联网的学习资源。 在解答习题时,我们需要保持积极的思维和态度。可能会遇到一些困难和挑战,但是通过坚持和努力,我们可以克服这些困难,提高我们的解决问题的能力。同时,我们还可以通过与同学或者其他人进行讨论,相互分享解题经验和思路,从而更好地理解问题。 综上所述,通过深入理解计算机系统(CSAPP)的习题,我们可以进一步巩固和深化对计算机系统的理解。掌握这些知识,不仅可以提高我们在计算机领域的能力,还可以为我们未来的学习和职业发展奠定重要的基础。因此,认真对待CSAPP的习题,是我们在学习计算机系统知识中不可或缺的一部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小 明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值