天天看点

《CSAPP》并发编程

前言:

     这是该书的最后一章了,从汇编和并行的角度上,彻底解释清楚了两个线程对同一个全局变量执行加一出错的原因。并且引入了进度图的概念,解释清楚死锁产生的原因。

我的github:

我实现的代码全部贴在我的github中,欢迎大家去参观。

https://github.com/YinWenAtBIT

第十二章:并发编程

定义:

如果逻辑控制流在时间上重叠,那么他们就是并发的。

一、并发编程的用处:

1.访问慢速的I/O设备:

等待慢速的IO设备时,可以让CPU转去处理其他的指令。

2.与人交互:

每次用户请求某种操作时,一个独立的逻辑并发流创造出来执行这个操作

3.通过推迟工作来降低延迟:

4. 服务多个网络客户端:

5.在多核机器上进行并行计算:

6. 进程

7. IO多路复用

8. 线程

二、基于进程的并发编程:

三、基于IO多路复用的并发编程:

这两个部分已经在《UNIX网络编程》中已经有了详细的叙述了,因此略过。

线程:

一、基于线程的特点:

1. 线程就是运行在进程上下文中的逻辑流:

线程由内核自动调度运行

2. 每个线程都有自己的线程上下文:

唯一的整数线程ID,栈,栈指针,程序计算器,通用目的计数器和条件码。

3. 所有运行在一个进程里的线程都共享该进程的整个虚拟地址空间

二、线程执行模型:

1.进程开始时那个线程称为主线程:

主线程创建的其他线程都是对等线程

2. 和一个进程相关的线程组成一个对等的线程池:

主线程和其他线程的区别仅仅在于它是第一个运行的线程

3. 一个线程可以杀死任何的对等线程,或者等待它的任意对等线程终止

4. 每个对等线程可以读写相同的共享数据

三、线程函数:

1. 创建线程,返回线程ID:

int pthread_create

pthread_t pthread_self

2. 终止线程:

void pthread_exit(void * thread_return)

当顶层的线程例程返回时,线程会隐式的终止

通过调用pthread_exit函数,线程会显式的终止。

如果主线程调用pthread_ext,它会等待其他对等线程终止,然后再终止主线程和整个进程。

某个对等线程调用UNIX的exit函数,该函数终止进程以及所有该进程的线程

另一个对等线程通过以当前线程ID为参数调用pthread_cancle懒终止当前线程

3. 收回已终止的检测的资源:

iint pthread_join(pthread_t tid, void ** thread_return)

这个函数会组设,直到线程tid终止。然后释放线程占用的资源

4. 分离线程:

pint pthread_detach(pthread_t tid)

线程时可结合的或者是可分离的,一个可结合的线程可以被其他线程收回资源或者杀死。

一个分离的线程时不可以被其他线程收回或者杀死的。它的存储器资源在线程终止时由系统自动释放。

四、线程存储器模型:

1.每个线程都有自己独立的线程上下文:

线程ID,栈,栈指针,程序计数器,条件码和通用目的寄存器

2. 每个线程与其他线程一起共享进程上下文的剩余部分:

整个用户的虚拟地址空间,由只读代码段,读写数据区,堆,共享库代码和数据区域组成。共享已经打开的文件集合。

3. 寄存器是不共享的,虚拟存储器总是共享的。

五、变量映射到储存器:

1. 全局变量:

运行时,虚拟储存器的读写区域包含每个全局变量的一个实例,任何线程都可以访问

2. 本地自动变量:

每个线程都包含它自己所有的本地变量的实例

3. 本地静态变量:

定义在函数内部并且有static属性的变量,在虚拟存储器的读写区域有每个静态变量的一个实例,每个线程都可以访问。

信号量同步线程:

一、对共享变量调用的方式:

在汇编中,每个线程都从储存器中取出变量,然后加载到自己的寄存器上,然后对变量进行操作,操作完之后再写回储存器中。

这样在几个线程同时对共享变量操作的时候,如果一个线程刚刚更新完了共享变量,还没来得及写回去,另一个线程就从储存器中取出了变量,将会导致同步错误。

一般而言,没办法预测操作系统是否会为线程选择一个正确的顺序,所以不能依赖这样的顺序来编写程序。

二、信号量:

可以用信号量来实现互斥,这一部分在APUE中有详细的讲诉,在这里就不细说。

三、生产者消费者问题:

需要使用1个互斥量来控制对资源的访问,一个信号量代表槽,一个信号量代表产品。

四、读写者问题:

有两种模式,一种是读者优先,这样情形使用一个互斥锁,加上读者计数器就可以完成。

第二种写着优先模式:

需要使用多个互斥量和计数,来完成这个复杂的逻辑关系。

五、基于预线程化的并发服务器:

这个问题使用生产者消费者模型可以解决。将每个收到的connfd当做生产出来的资料加入即可。

其他并发问题:

一、线程安全:

1. 定义:

一个函数被称为线程安全你的,仅当被多个线程反复调用时,它会一直产生正确的结果

二、线程不安全种类:

1. 不保护共享变量的函数:

这个就是没有对共享变量进行加锁,修改的方式只要加上互斥量即可。改完之后速度会因为同步导致变慢

2. 保持跨越多个调用状态的函数:

这类函数中有一个static变量,用来保存上一次的结果,比如rand函数。因为这一次调用的结果依赖于上一次调用的结果,那么将导致被多个线程调用的时候结果不唯一。

修正这类函数的唯一办法就是重写它,放弃使用static数据,而是使用调用者传递来的状态信息。这样做的缺点就是已经在使用这个函数的其他代码需要修改。

3. 返回指向静态变量的指针的函数:

结果使用static来保存结果,然后返回一直指向这个结果的指针,这样就会引入竞争问题。

解决办法:

a. 重写函数,让调用者传递存放结果的地址,这样的坏处就是使用了这个函数的代码需要重写。

b. 对这个线程不安全的函数加锁,直到结果被拷贝出去之后再解锁,但是实际上也还是得修改源代码。

4. 调用线程不安全函数的函数:

这种情况就是函数 F 调用了线程不安全的函数 G,

如果G是第二类函数,那么F还是不安全的

如果G是1,3 类函数,可以在F中对G的调用加锁,就可以是的F变成线程安全你的函数了。

三、可重入性:

定义:

可重入的特别在于,当一个可重入函数被多个线程调用时,不用引用任何共享数据。

可重入函数是线程安全函数的一个真子集。

四、死锁:

1. 定义:

死锁是一组线程被阻塞了,等待一个永远不会为真的条件。使用进度图可以非常好的理解死锁发生于避免。

2. 互斥锁加锁规则顺序:

如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放。这样这个程序就是无死锁的。

总结:

这一部分对于并发编程的解释,彻底说清了并发编程的原理,会遇上的问题,以及解决的办法。以后遇上多线程时再也不是问题了。

继续阅读