4.2《深入理解计算机系统》笔记(五)并发、多进程和多线程【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
那么,这种排列组合会有很多种情况。这十步会给我们带来奇妙的答案。下面我们用进度图(是一种二维笛卡尔坐标系)来表达:
黄色的区域是临界区,绿色的轨迹是不安全的,而蓝色的轨迹是安全的。线程锁就是当线程即将进入临界区的时候,挂起自己等待其他线程走完临界区,自己再执行(以前理解的逻辑是 线程锁后本身线程时域独占cpu,,但实际情况恰恰相反,实际情况是自己先不变,等待其他线程走完临界区,自己再执行,,,那问题来了,其他线程中如果有改变锁中的量的情况,岂不是矛盾了?!)。当然,二维笛卡尔已经不能描述线程锁的原理了。
剩余章节还介绍了“生产者——消费者”问题,“读者——写者”问题,略过。
12.6 使用线程提高并行性
12.7线程安全
死锁。由于PV操作不当,可能造成死锁现象。这在程序中也会出现。是很头疼的事情。
总结
无乱那种并发机制,同步对共享数据的并发都是一个困难的问题。该书没有更详细说明并行程序的过程。但是我相信:只要操作系统有挂起的功能,那么并发的形式应该是多种多样的!
《深入理解计算机系统》昨天已经看完了,今天把最后一章存档为博客,方便以后自己查阅并加深理解,这篇是终结篇!
纵览该书,其重要程度不言而喻。囊括了二进制表示、汇编、指令、高速缓存、CPU体系结构、存储器、虚拟存储器、栈、堆、异常、进程、编译、链接、静态库和动态库、运行、网络、线程和并发。可以作为从事计算机行业着知识的基石,只恨自己大学时候没有读这本书。
原文:http://blog.csdn.net/hherima/article/details/8987813