三种基本的构造并发程序的方法:进程,I/O多路复用,线程
1、基于进程的并发编程
- 每个逻辑控制流都是一个进程,由内核来调度和维护;有独立的虚拟地址空间;进程间通信采用显式的IPC机制
- 父子进程之间共享文件表但是不共享用户地址空间。他的好处就是因为是有独立的地址空间,所以不会互相不小心覆盖内存,但是坏处也很明显,这样会使进程间共享状态信息变得困难,要使用进程间通信(IPC)机制,而且因为进程控制和IPC的开销很高,所以会比较慢。
- IPC:管道,FIFO,系统V共享内存,系统V信号量
2、基于I/O多路复用的并发编程
- selcet函数会一直阻塞,直到集合中有一个描述符准备好可以读
- 事件驱动编程,相对于基于进程的设计而言,他的优点是让程序员对程序行为有更好的控制;每个逻辑流能共享全部地址空间,共享数据更容易;便于调试;不需要进程间的切换调度,更加高效。现代高性能服务器的选择
- 缺点就是编码复杂,阻塞处理,不能充分利用多核处理器
3、基于线程的并发编程
- 每个线程有唯一的线程ID、栈、栈指针、程序计数器、通用目的寄存器、条件码
- 和进程一样,内核自动调度,通过ID;
- 和I/O多路复用一样,运行在单一进程的的上下文中,共享进程的虚拟地址空间和所有内容,包括代码、数据、堆、共享库和打开的文件;
- 不同于父子进程之间,一个进程相关的线程组成的是对等的线程池,一个线程可以杀死任一对等线程,也可以等待任意对等线程的终止;对等线程可以读写相同的共享数据;
4、共享变量
- 变量的内存模型,如上第一条和第三条
- 各自独立的线程栈通常是被相应的线程独立访问的,但也不一定,如果一个线程通过某种方式得到一个指向其他线程栈的指针,就可以读写这个栈的任何部分,这是因为不同的线程栈是不对其他线程设防的;
5、用信号量进行线程同步
- P操作
- s非零,P操作将s-1,返回;s==0,线程挂起,直到s!=0,V操作重启线程;重启后P操作s-1,返回;
- V操作
- s+1;有线程阻塞等待s!=0,V操作重启线程,s-1完成P操作。
- 使用信号量来实现互斥——互斥锁
- 二元信号量(s=0或1)
- P操作加锁
- V操作解锁
- 使用信号量来调度共享资源
- 一个线程用信号量操作来通知另一个线程
- 生产者—消费者问题
- 生产者
- 消费者
- 条件变量
- 读者—写者问题——读写锁
- 这种方法可能会导致饥饿,即一个线程无限期阻塞,这是因为在V操作无法控制他重启的是哪一个等待线程
- 基于预线程化的并发服务器
- 类似于生产者和消费者问题,主线程创建一组工作者线程,然后进入无限循环将已连接描述符放到缓冲区SBUF中,生产者线程等待直到有已连接的描述符需要处理
- 这是一个事件驱动服务器,带有主线程和工作者线程的简单状态机
6、使用线程提高并行性
- 并行程序是运行在多个处理器上的并发程序
- 使用共享内存时,每次操作都要进行存取和PV操作的开销是很大的,所以要尽量减少存取操作和PV操作。例如使用局部变量存取和更新值,将最终结果存取到共享内存中等等——使用局部变量消除不必要的引用
- 当线程数多于内核数量的时候,会导致一个核上多个线程上下文切换导致的开销,所以一般每个核上只运行一个线程
7、其他并行问题
- 线程不安全
- 解决线程不安全:修改原函数,加锁+复制
-
- 可重入函数是线程安全的
- 显式:传值传递,数据引用都是本地自动栈
- 隐式:引用传递,调用线程时传递指向非共享数据的指针,调用本栈数据
- Linux系统提供大多数线程不安全函数的可重入版本,可重入版本的名字以_r结尾
- 可重入函数是线程安全的
- 竞争
- 线程之间有依赖性,当其一来的工作并不是想象中的样子就会出错
- 死锁
- 信号量引入的运行时错误
- 一个线程被阻塞,等待一个永远不为真的条件,换句话说,程序死锁是因为每个线程都在等待其它线程执行一个根本不可能发生的V操作
- 避免:当使用二元信号量时,可以通过互斥锁加锁顺序规则
- 给定所有互斥操作一个全序,每个线程以一种顺序获得互斥锁并以相反的顺序释放