并发:逻辑控制流在时间上的重叠。
构造并发程序的方法:
- 进程
- I/O多路复用
- 线程
基于进程的并发编程
假设我们有两个客户端和一个服务器,服务器正在监听一个监听表述符上的请求。现在假设服务器接受了客户端1的连接请求。
*父进程关闭它的已连接描述符的拷贝是至关重要。
几个piont:
- 需要包括一个SIGCHID处理程序,来收回将死进程。
- 父子进程必须关闭它们各个的connfd拷贝。
- 因套接字的文件表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。
父子进程共享状态信息,进程模型:共享文件表,但是不共享用户地址空间。
基于I/O多路复用的并发编程
基本思路:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select函数处理类型为fd_set的集合,也叫做描述符集合,并在逻辑上描述为一个大小为n的位向量:b_n-1,...b_1,b_0*
*
描述符能做的三件事:
- 分配他们
- 将一个此种类型的变量赋值给另一个变量
- 用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏指令来修改和检查它们
基于I/O多路复用的并发事件驱动服务器
一个状态机就是一组状态,输入事件,和转移。
自循环:同一输入和输出状态之间的转移。
优点:
1.比基于进程的设计给了程序员更多的对程序行为的控制。
2.一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。,使得在流之间共享数据变得容易。
3.事件驱动设计常常比基于进程的设计要高效得多,因为它们不需要进程上下文切换来调度新进程。
基于线程的并发进程
线程就是运行在进程上下文中的逻辑流。
每个线程都有自己的线程上下文,包括:
一个唯一的整数线程ID
栈
栈指针
程序计数器
通用目的寄存器
条件码
所有运行在一个进程里的线程共享该进程的整个虚拟地址。
结合了两种方法的特性:
1.和进程一样,由内核调度,并内核通过一个整数ID来识别线程。
2.同基于I/O多路复用的流一样,多个线程运行在单一进程的上下文中。因此共享这个进程的虚拟地址空间的整个内容:代码啊,数据,堆,共享库和打开文件。
主线程:每个进程开始生命周期时都是单一线程,这个线程称为主线程
对等线程:被主线程创建,后与主线程并发运行。
主线程和其他线程的区别:总是进程中第一个运行的线程。
对等(线程)池概念的影响:一个线程可以杀死它的任何对等线程,或等待它的任何对等线程终止。
*每个对等线程都能读写相同的共享数据。
posix线程
线程代码和本地数据都被封装在一个线程例程中。
每个线程都以一个通用指针作为输入,并返回一个通用指针。
想向线程例程中传递参数?=>将参数放在一个结构中,并传递一个指向该结构的指针。
想返回多个参数?=>返回一个指向参数结构的指针。
创建线程
线程调用pthread_create函数来创建其他线程。
- 输入变量arg
- 在新进程中运行线程例程f
- 返回时参数tid包含新线程的ID,新线程可以通过调用pthread_self函数来获得自己的线程ID
能用attr来改变新创建的线程的默认属性。
终止线程
线程终止的方式:
- 顶层的线程返回,隐式终止。
- 调用pthread_exit函数,显示终止。(会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回thread return)
- 某个对等线程调用Unix的exit函数,该函数终止进程以及所有与该进程相关的线程。
- 另一个对等线程以当前线程ID作为参数调用pthread_cancle终止当前进程。
pthread_exit
pthread_cancle
回收已终止进程资源
线程调用pthread_join函数等待其他线程终止
这个函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源.
分离线程
在任何一个时间点上,进程是可结合的或分离的。
可结合进程:
能够被其他进程回收其资源和杀死/调用pthread_detach分离
回收之前,它的储存器资源是没有被释放的
分离进程:
是不能被其他进程杀死或回收的。
储存器资源终止时系统自动释放。
默认情况线程创建为可结合的。
pthread_detach:
初始化进程
调用pthread_once来初始化与线程的相关状态;
once_ control是一个全局或静态变量,总是被初始化为PTHREAD_ ONCE_ INIT.
多线程程序中的共享变量
将变量隐射到存储器
根据存储类型被映射到虚拟存储器。
- 全局变量。函数以外的变量。运行时,虚拟存储器的读/写区域只包含每个全局变量的一个实例,任何线程可用。
- 本地自动变量定义在函数内部没有static属性的变量。运行时,每个线程都含有自己的所有本地自动变量实例
- 本地静态变量。在函数内部,有static。和全局变量一样,虚拟存储器的读/写区域只包含在程序中声明的本地静态变量的一个实例。
共享变量
说一个变量v是共享的,当且仅当它的一个实例被一个以上的线程引用。
一般而言没有办法预测操作系统是否将为你的让进程选择一个正确的顺序。
进度图
进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线。
将指令执行模型为一种状态到另一种状态的转换。
合法转换:向右或向上。
点(L1,S2)对应线程完成L1,而线程2完成S2状态
临界区:对于线程i,操作共享变量cnt内容的指令(Li,Ui,Si)构成了一个关于共享变量cnt的临界区。
不安全区:两个临界区的交集形成的状态空间区域
安全轨迹线:绕开不安全区的轨迹线
接触=>不安全轨迹线
信号量
信号量:是用信号量解决同步问题,信号量s是具有非负整数值的全局变量,有两种特殊的操作来处理,称为P和V:
P(s):如果s非零,那么P将s减1,并且立即返回。
V(s):V操作将s加1。
信号量不变性:
P,V确保一个正在运行的程序决不可能进入这样的一种状态,也就是一个正确耳初始化的信号有了一个负值。
使用信号量来实现互斥
基本思想:
将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。
二元信号量:用这种方式来保护共享变量的信号量叫做二元信号量,取值总是0或者1.
互斥锁:以提供互斥为目的的二元信号量
加锁:对一个互斥锁执行P操作
解锁:对一个互斥锁执行V操作
计数信号量:被用作一组可用资源的计数器的信号量
利用信号量来调度共享资源
1.生产者-消费者问题
因为插入和取出项目都涉及更新共享变量,所以要保证:
对缓冲区的访问是互斥的。
调度对缓冲区的访问:
如果缓冲区是满的(没有空的槽位),那么生产者必须等待直到有一个空的槽位为止,如果缓冲区是空的(即没有可取的项目),那么消费者必须等待直到有一个项目变为可用。
2.读者-写者问题
修改对象的线程叫做写者
只读对象的线程叫做读者
写者必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。
-
读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。读者不会因为有一个写者等待而等待。
-
写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。同第一类问题不同,在一个写者后到达。
解答:
信号量mutex保护对共享变量readcnt的访问,readcnt统计当前临界区的读者数量。每当一个写者进入临界区,它就对互斥锁w加锁,每当它离开临界区时,对w解锁,这就保证了任意时刻临界区最多有一个写者;另一方面,只有第一个进入临界区的读者对w加锁,而只有最后一个离开临界区的读者对w解锁。
饥饿:一个线程无限期等待阻塞,无法进展。
综合:基于预防线程话的并发服务器
预线程化并发服务器的组织结构:
使用线程提高并行性
顺序并发,并行,程序集合间的关系:
其他并发问题
不安全函数类
- 不保护共享变量的函数
- 保持跨越多个调用的状态的函数
- 返回指向静态变量的指针的函数
- 调用线程不安全函数的函数
可重入函数:被多个线程调用时,不会引用任何共享数据。
(是线程安全函数的一个真子集)
-
显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
-
隐式可重入的:调用线程小心的传递指向非共享数据的指针。
*可重入版本名字总以“_r”为后缀结尾。
竞争
原因:
程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
可以动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。
死锁
死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。