《深入理解计算机系统》并发编程——读书笔记

现在操作系统提供了三种基本的构造并发程序的方法:

1、进程。每个逻辑控制流都是一个进程,由内核来调度和维护;

2、I/O多路复用。

3、线程。

一、基于进程的并发编程

                

      在接受连接请求之后,服务器派生出一个子进程,这个子进程获得服务器描述表完整的拷贝。子进程关闭它的拷贝中监听描述符3,父进程关闭它的已连接描述符4的拷贝,因为不需要这些描述符了。

      程序实例:

    

    因为通常服务器会运行很长时间,所以需要一个SIGCHLD处理程序,来回收僵死进程。因为当SIGCHLD执行时,信号是阻塞的,而UNIX信号是不排队的,所以SIGCHLD必须准备好回收多个僵死进程。另外注意,循环中的父进程和子进程关闭各自需要关闭的描述符。

    进程能够共享文件表,但不共享用户地址空间。

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

    1、面对困境——服务器必须响应两个互相独立的I/O事件:1)网络客户端发起的连接请求  2)用户在键盘上键入的命令 ,解决的办法是I/O多路复用技术。基本思想是,使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。

       select函数如下:

                      

     程序示例如下:

                                     

     使用select函数的过程如下:

     第一步,初始化fd_set集,19~22行;

     第二步,调用select,25行;

     第三步,根据fd_set集合现在的值,判断是哪种I/O事件,26~31行。

    2、基于I/O多路复用的并发事件驱动服务器

        I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的,一般概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移。

        并发事件驱动程序中echo服务器中逻辑流的状态机,如下图所示:

                                                                                      

三、基于线程的并发编程

    1、线程运行在进程上下文中的逻辑流。线程由内核自动调度,每个线程都有它自己的线程上下文。

    2、线程执行模型。多线程的执行模型在某些方面和多进程的执行模型相似。每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从在此刻开始,两个线程就并发地运行。

                                                            

    3、Posix线程

         创建线程:

                    

        获取自身ID:

                   

       终止线程:

       有以下四种方式终止线程:

       当顶层的线程例程返回时,线程会隐式终止;

       线程调用pthread_exit函数,线程会显示终止;如果主线程调用pthread_exit,它会等到所有其他对等线程终止,然后再终止主线程和整个线程,返回值为thread_return;

       某个对等线程调用exut函数,则函数终止进程和所有与该进程相关的线程;

       另一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程。

                      

      

                     

     回收已终止线程的资源:

                   

     pthread_join函数会终止,直到线程tid终止。和wait不同,该函数只能回收指定id的线程,不能回收任意线程。

     分离线程:

     一个可结合的线程能够被其他线程回收其资源和杀死,在被其他线程回收之前,它的存储其资源是没有被释放的;相反,一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源是在它终止时系统自动释放的。默认情况下,线程被创建成可结合的。但现实程序中,有很好的理由要使用分离线程。

                 

     初始化线程:该函数用来初始化多个线程共享的全局变量。

                

     一个基于线程的并发服务器:

                                      

                                      

      以上程序可能会出错,因为在对等线程的赋值语句和主线程的accept的语句见引入了竞争——如果赋值语句在下一个accept之前完成,则不会出错;如果赋值语句是在accept之后完成,那么对等线程的局部变量connfd就得到下一次连接的描述符。解决办法是,必须将每个accept返回的描述符分配到它自己的动态分配的存储器块。(21~23行)  32行:动态内存空间释放,释放那个指向动态内存的指针即可,不一定非要是malloc当时生成的指针。

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

          每个线程都有它自己独自的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。寄存器是从不共享的,而虚拟存储器总是共享的。线程化的c程序中变量根据它们的存储器类型被映射到虚拟存储器:全局变量,本地自动变量(不共享),本地静态变量。

    5、用信号量同步线程

        共享变量引入了同步错误。

        进度图:                                                                                           轨迹线示例:                                                                    临界区(不安全区):

                   

     信号量:是用信号量解决同步问题,信号量s是具有非负整数值的全局变量,有两种特殊的操作来处理(P和V):

                P(s):如果s非零,那么P将s减1,并且立即返回。如果s为0,那么就挂起这个线程,直到s变为非零;

                V(s):V操作将s加1。

    使用信号量实现互斥:

                                    

      利用信号量调度共享资源:在这种场景中,一个线程用信号量操作来通知另一个线程,程序状态中的某个条件已经为真了。两个经典应用:

      a)生产者——消费者问题

                                 

      要求:必须保证对缓冲区的访问是互斥的;还需要调度对缓冲区的访问,即,如果缓冲区是满的(没有空的槽位),那么生产者必须等待直到有一个空的槽位为止,如果缓冲区是空的(即没有可取的项目),那么消费者必须等待直到有一个项目变为可用。

                                           

                                         

                                        

       注释:5~13行,缓冲区初始化,主要是对缓冲区结构体进行相关操作;16~19行,释放缓冲区存储空间;22~29行,生产(有空槽的话,在空槽中插入内容);32~4行,消费(去除某个槽中的内容,使该槽为空)

      b)读者——写者问题

        修改对象的线程叫做写者;只读对象的线程叫做读者。写着必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。读者——写者问题基本分为两类:第一类,读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。换句话说,读者不会因为有一个写者等待而等待;第二类,写者优先,要求一定能写者准备好可以写,它就会尽可能地完成它的写操作。同第一类问题不同,在一个写者后到达的读者必须等待,即使这个写者也是在等待。以下程序给出了第一类读者——写者问题的解答:

                                         

      注释:信号量w控制对访问共享对象的临界区的访问。信号量mutex保护对共享变量readcnt的访问,readcnt统计当前临界区的读者数量。每当一个写者进入临界区,它就对互斥锁w加锁,每当它离开临界区时,对w解锁,这就保证了任意时刻临界区最多有一个写者;另一方面,只有第一个进入临界区的读者对w加锁,而只有最后一个离开临界区的读者对w解锁。

      综合:基于预线程的并发服务器之前介绍的基于线程的并发服务器,需要为每个客户端新建一个新线程,导致不小的代价。一个基于预线程化的服务器通过使用如下图所示的生产者——消费者模型来降低这种开销。服务器是由一个主线程和一组工作组线程构成的。主线程不断地接受来自客户端的连接请求,并将得到的连接描述符放在一个有限缓冲区中。每一个工作组线程反复地从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。

                                  

      程序示例如下图:

                                                              

                                                               

                                                              

                                                             

                                                           

       注释:26~27行,产生工作组线程;29~32行,接受客户端的连接请求,并把这些描述符放到缓冲区;35~43行,每个线程所要完成的工作;19行,初始化线程共享的全局变量。初始化有两种方式,一种是它要求主线程显示地调用一个初始化函数;第二种是,在此显示的,当第一次有某个线程调用echo_cnt函数时,使用pthread_once函数去调用初始化函数。

      6、其他并发问题

           a)四种不安全函数;

           b)可重入函数。可重入函数是线程安全函数的一个真子集,它不访问任何共享数据。可重入安全函数通常比不可重入函数更有效,因为它们不需要任何同步原语。

           c)竞争。当程序员错误地假设逻辑流该如何调度时,就会发生竞争。为了消除竞争,通常我们会动态地分配内存空间。

           d)死锁。当一个流等待一个永远不会发生的事件时,就会发生死锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
深入理解计算机系统(CSAPP)」是一本经典的计算机系统教材,其英文名称是《Computer Systems: A Programmer's Perspective》。本书以程序员的视角出发,从底层的硬件到高层的应用层,系统地介绍了计算机系统的各个方面,包括机器级代码、处理器体系结构、操作系统、网络和并发等内容。 这本书的核心理念是将计算机系统作为一个层次化的系统进行学习和理解。它从计算机硬件的基本组成部分开始,如处理器、存储器和I/O设备,介绍了它们的工作原理和与程序员的交互方式。接着,它讲解了机器级码和汇编语言,帮助读者理解程序是如何被机器执行的。然后,本书逐步深入介绍了处理器的功能和设计、虚拟存储器的概念和管理、链接和加载等内容。此外,还涵盖了操作系统原理、网络编程和并发编程等高级概念。 通过阅读「深入理解计算机系统」,读者能够获得对计算机系统的更深入和全面的认识。它强调了程序员和底层硬件之间的交互,使读者不仅能够编写高效的代码,还能够理解和调试底层的机器代码。这对于提高代码质量、性能和调试效率都非常重要。 此外,本书的内容结构清晰,语言简明扼要,兼具理论和实践,为读者提供了理论基础和实践指导。它不仅适用于计算机科学相关专业的学生,也适用于任何对计算机系统感兴趣的人。无论是想要提升编程技能,还是对计算机底层原理感兴趣的读者,「深入理解计算机系统」都是一本值得推荐的书籍。 总之,「深入理解计算机系统」以其全面深入的内容和理论结合实践的特点,成为计算机系统教材中的经典之作。它帮助读者全面理解计算机系统的工作原理和设计思想,提高编程技能和系统调试能力。对于热爱计算机科学和计算机系统的人来说,这本书是必不可少的参考资料。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值