1.并发
并发不仅仅局限于内核,它也可以在应用程序中扮演重要的角色。
应用级并发的应用场景:
-->访问慢速I/O设备;
-->与人交互;
-->通过推迟工作以降低延迟;
-->服务多个网络客户端;
-->在多核机器上进行并行计算;
-->进程;
-->I/O多路复用;
-->线程。
2.基于进程的并发编程
构造并发程序最简单的方法就是用进程,常用的函数有fork、exec和waitpid。
例如,一个构造并发服务器的自然方法就是,在父进程中接受客户端的连接请求,然后创建一个新的子进程来为每个新客户端提供服务。
进程的优劣:
对于在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。进程有独立的地址空间既是优点也是缺点。优点是,一个进程不可能不小心覆盖另一个进程的虚拟内存;缺点是,独立的地址空间使得进程共享状态信息变得更加困难,为了共享信息,它们必须使用显示的IPC(进程间通信)机制,因为进程控制和IPC的开销很高,所以处理速度会很慢。
3.基于线程的并发编程
创建并发逻辑流的方法:
-->第一种方法:我们为每个流使用了单独的进程,内核会自动调度每个进程,而每个进程有它自己的私有地址空间,这使得流共享数据很困难。
-->第二种方法:我们创建自己的逻辑流,并利用I/O多路复用来显示地调度流,因为只有一个进程,所有的流共享整个地址空间。
-->第三种方法:基于线程,是以上两种方法的混合。
线程(thread)就是运行在进程上下文中的逻辑流,
线程由内核调度,每个线程都有它自己的线程上下文(thread context),包括一个唯一的整数线程ID(thread id,TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。
基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特性。同进程一样,线程由内核自动调度,并且内核通过一个整数ID来识别线程;同基于I/O多路复用的流一样,多个线程运行在单一线程的上下文中,因此共享这个进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。
4.线程执行模型
多线程的执行模型在某些方面和多进程的执行模型是相似的。
每个进程开始生命周期时都是单一线程,这个线程称为主线程(main thread),在某一时刻,主线程创建一个对等线程(peer thread),从这个时间点开始,两个线程就并发地运行。
在一些重要方面,线程执行是不同于进程的。因为一个线程的上下文要比一个进程的上下文小得多,所以线程的上下文切换比进程快的多。
另一个不同就是线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程组成一个对等(线程)池,独立于其他线程创建的线程。主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。对等(线程)池概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。
另外,每个对等线程都能读写相同的共享数据。
5.使用信号量来实现互斥
信号量提供了一种很方便的方法来确保对共享变量的互斥访问。基本思想是将每个共享变量(或一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。
以这种方式来保护共享变量的信号量叫做二元信号量(binary semephore),因为它的值总是0或者1。以提供互斥为目的的二元信号量常常也称为互斥锁(mutex)。在一个互斥锁上执行P操作称为对互斥锁加锁,类似地,执行V操作称为对互斥锁解锁。对一个互斥锁加了锁但还没有解锁的线程称为占用这个互斥锁。一个被用作一组可用资源的计数器的信号量被称为计数信号量,如下图,
6.利用信号量来调度共享资源
除了提供互斥之外,信号量的另一个重要作用是调度对共享资源的访问。在这种场景中,一个线程用信号量操作来通知另一个线程,程序状态中的某个条件已经为真了。两个经典有用的例子是生产者-消费者和读者-写者问题。
-->生产者-消费者问题:生产者和消费者线程共享一个有n个槽的有限缓冲区。生产者线程反复地生成新的项目(item),并把它们插入到缓冲区,消费者线程不断地从缓冲区中取出这些项目,然后消费(使用)它们。
因为插入和取出项目都涉及更新共享变量,所以必须保证对缓冲区的访问是互斥的,然后才调度对缓冲区的访问。
-->读者-写者问题:它是互斥问题的一个概括。一组并发的线程要访问一个共享对象(例如一个主存中的数据结构,或者一个磁盘上的数据库)。有些线程只读对象,而其他的线程只修改对象,修改对象的线程叫做写者,只读对象的线程叫做读者。写者必须拥有对对象的独占的访问,而读者可以和无限多个其他的读者共享对象。一般来说,有无限多个并发的读者和写者。
7.刻画并行程序的性能
8.死锁
信号量引入了一种潜在的令人厌恶的运行时错误,叫做死锁(deadlock),它指的是一组线程被阻塞了,等待一个永远也不会为真的条件,如下图,
(1)程序员使用P和V操作顺序不当,以至于两个信号量的禁止区域重叠。如果某个执行轨迹线碰巧到达了死锁状态d,那么就不可能有进一步的进展了,因为重叠的禁止区域阻塞了每个合法方向上的进展。换句话说,程序死锁是因为每个线程都在等待其他线程执行一个根本不可能发生的V操作。
(2)重叠的禁止区域引起了一组称为死锁区域(deadlock region)的状态。如果一个轨迹线碰巧到达了一个死锁区域的状态,那么死锁就是不可避免的了。轨迹线可以进入死锁区域,但是它们不可能离开。
(3)死锁是一个相当困难的问题,因为它不总是可以预测的。
互斥锁加锁顺序规则:给定所有互斥操作的一个全序,如果每个线程都是以一种顺序获得互斥锁并以相反的顺序释放,那么这个程序就是无死锁的。
第十二章小结