《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】

欢迎查看《深入理解计算机系统》系列博客

《深入理解计算机系统》笔记(一)栈

《深入理解计算机系统》笔记(二)内存和高速缓存的原理

《深入理解计算机系统》笔记(三)链接知识

《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收

《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】(本篇)

——————————————————————————————————————–

该书中第11章是写web服务器的搭建,无奈对web还比较陌生。还没有搞明白。

    这些所谓的并发,其实都是操作系统做的事情,比如,多进程是操作系统fork函数实现的、I/O多路复用需要内核挂起进程、多线程需要内核创建和挂起线程。我么只是使用以下操作系统的这项并发技术。但是我们必须处理一些存在问题。

    ●进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用进程间通信(IPC)。

    ●I/O多路复用。这种形式的并发,应用程序在一个进程的上下文中显示地调度它们自己的逻辑流。逻辑流被模拟为“状态机”,数据到达文件描述符后,主程序显示地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享一个地址空间。

    ●线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。线程可以看做是进程和I/O多路复用的合体,像进程一样由内核调度,像I/O多路复用一样共享一个虚拟地址空间。

12.1 基于进程的并发编程

    构造并发最简单的就是使用进程,像fork函数。例如,一个并发服务器,在父进程中接受客户端连接请求,然后创建一个新的字进程来为每个新客户端提供服务。


    关于进程的优劣,对于在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。进程有独立的地址控件爱你既是优点又是缺点。由于独立的地址空间,所以进程不会覆盖另一个进程的虚拟存储器。但是另一方面进程间通信就比较麻烦,至少开销很高。

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

    比如一个服务器,它有两个I/O事件:1)网络客户端发起连接请求,2)用户在键盘上键入命令行。我们先等待那个事件呢?没有那个选择是理想的。如果accept中等待连接,那么无法相应输入命令。如果在read中等待一个输入命令,我们就不能响应任何连接请求(这个前提是一个进程)。

    针对这种困境的一个解决办法就是I/O多路复用技术。基本思想是:使用select函数,要求内核挂起进程,只有在一个或者多个I/O事件发生后,才将控制返给应用程序。如图所示:横向的方格可以看作是一个n位的描述符向量。现在,我们定义第0位描述是“标准输入”,第3位描述符是“监听描述符”。

    

I/O多路复用的优劣:由于I/O多路复用是在单一进程的上下文中的,因此每个逻辑流程都能访问该进程的全部地址空间,所以开销比多进程低得多;缺点是编程复杂度高。

12.3基于线程的并发编程

    每个线程都有自己的线程上下文,包括一个线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。由于线程运行在单一进程中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。所以我认为不存在线程间通信,线程间只有锁的概念。

    线程执行的模型。线程和进程的执行模型有些相似。每个进程的声明周期都是一个线程,我们称之为主线程。但是大家要有意识:线程是对等的,主线程跟其他线程的区别就是它先执行。

    一般来说,线程的代码和本地数据被封装在一个线程例程中(就是一个函数)。该函数通常只有一个指针参数和一个指针返回值。

    在Unix中线程可以是joinable(可结合)或者detached(分离)的。joinable可以被其他线程杀死,detached线程不能被杀死,它的存储器资源有系统自动释放。

    线程存储器模型,每个线程都有它自己的独立的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器。每个线程和其他线程共享剩下的部分,包括整个用户虚拟地址空间,它是由代码段、数据段、堆以及所有的共享库代码和数据区域组成。不同线程的栈是对其他线程不设防的,也就是说:如果一个线程以某种方式得到一个指向其他线程的指针,那么它可以读取这个线程栈的任何部分。

12.4多线程中共享变量

    ●全局变量和static 变量是存储在数据段,所以,多线程共享之!

    ●由于线程的栈是独立的,所有线程中的自动变量是独立的。即使多个线程运行同一段代码总的自动变量,那么他们的值也是根据线程的不同而不同。

    ●比如C++中,类属性不是在用户栈中的。所以线程共享之!

12.5用信号量同步线程。

    信号量通常称之为PV操作(发明PV操作的是荷兰一个哥们),虽然它的思想是将临界代码保护起来,达到互斥效果。这里面操作系统使用到了线程挂起!PV操作是就不再做过多的解析。下面展示一个多线程对共享变量修改的进度图的解释:

 

现在有两个线程thread1和thread2.那么操作系统并行thread1 和thread2。汇编的执行顺序如下(可能的顺序,一共是10步):

 H1、L1、U1、S1、T1、H2、L2、U2、S2、T2//可以得到cnt =2 

 H1、L1、U1、H2、L2、S1、T1、U2、S2、T2//得到错误的cnt =1

那么,这种排列组合会有很多种情况。这十步会给我们带来奇妙的答案。下面我们用进度图(是一种二维笛卡尔坐标系)来表达:


    黄色的区域是临界区,绿色的轨迹是不安全的,而蓝色的轨迹是安全的。线程锁就是当线程即将进入临界区的时候,挂起自己等待其他线程走完临界区,自己再执行。当然,二维笛卡尔已经不能描述线程锁的原理了。

    剩余章节还介绍了“生产者——消费者”问题,“读者——写者”问题,略过。

12.6 使用线程提高并行性

12.7线程安全

    我们编程过程中,尽可能编写线程安全函数,即一个函数当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。如果做不到这个条件我们称之为线程不安全函数。下面介绍四类线程不安全函数:
    ●不保护共享变量的函数。解决办法是PV操作。
    ●保持跨越多个调用的状态函数。比如使用静态变量的函数。解决方法是不要使用静态变量或者使用可读静态变量。
    ●返回指向静态变量的指针的函数。解决方法是lock-and-copy(枷锁-拷贝)
    ●调用线程不安全函数的函数

    死锁。由于PV操作不当,可能造成死锁现象。这在程序中也会出现。是很头疼的事情。

总结

    无乱那种并发机制,同步对共享数据的并发都是一个困难的问题。该书没有更详细说明并行程序的过程。但是我相信:只要操作系统有挂起的功能,那么并发的形式应该是多种多样的!

    《深入理解计算机系统》昨天已经看完了,今天把最后一章存档为博客,方便以后自己查阅并加深理解,这篇是终结篇!

    纵览该书,其重要程度不言而喻。囊括了二进制表示、汇编、指令、高速缓存、CPU体系结构、存储器、虚拟存储器、栈、堆、异常、进程、编译、链接、静态库和动态库、运行、网络、线程和并发。可以作为从事计算机行业着知识的基石,只恨自己大学时候没有读这本书。

    这两周我边读、边写、边画图,目的是增加记忆,并分享给网友。给那些初学者或者已经上班一段时间但是对这些东没有定义式解释的同学一点分享。其他四篇博客地址如下:

    《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收【插图】点击打开链接

    《深入理解计算机系统》笔记(三)链接知识点击打开链接

    《深入理解计算机系统》笔记(二)内存和高速缓存的原理【插图】 点击打开链接

    《深入理解计算机系统》笔记(一)【插图】点击打开链接


        </div>
            </div>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值