临界资源和临界区
临界资源:多线程执行流共享的资源就叫临界资源。
临界区:每个线程中,访问临界区的代码,就叫临界区。
互斥:任何时候,互斥保证只有一个执行流进入临界区,访问临界资源,通过对临界资源起到保护作用
原子性:不会被任何调度机制打断的操作,该操作有两态,要么完成,要么未完成。
局部变量和共享变量
- 局部变量:变量的地址空间在线程栈空间内,变量归属于单个线程,其他线程无法看到这个变量
- 共享变量:变量可以在线程间共享,完成线程之间的交互
当多个线程共发操作共享变量,会出现问题!
例如:对g_val全局变量进行--操作,在汇编层面会被分成三步:
- load:从内存中读取数据到寄存器中
- update:更新寄存器的值,执行-1操作
- store:将新值,将寄存器写回共享变量g_val的内存地址
所以在一个线程执行三步操作期间,发生线程切换,其他线程对g_val变量进行操作,就会导致问题。问题就在于:共享资源没有被保护起来
保护临界资源的方法
- 代码需要互斥行为,当代码进入临界区执行时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区代码,并且临界区没有线程在执行,那么只允许一个线程进入该临界区
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
方法就是:需要在临界区之间加锁
互斥锁操作的原理
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的,总线周期也有先后,一个处理器的交换指令执行时另一个处理器的指令只能等待总线周期。
交换的本质:所有线程在争锁时,只有"1",交换是一条汇编-->原子的
lock操作:
- %al寄存器值变成0
- 交换%al寄存器值和内存的值(mutex的值变成0,%al的值变成1)
- 判断%al寄存器的值(如果此时发生线程切换,原线程会把%al的值带走,新线程使用%al的寄存器和mutex的0交换,新线程判断false)
- goto会重新执行上面代码,重新让%al=0
unlock操作:
- 将mutex=1
- 唤醒等待mutex的线程,让他们竞争锁
可重入和线程安全
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见于对全局变量和静态变量进行操作,但是在没有锁保护的情况下,会出现该问题
- 重入:同一个函数被不同的执行流,当前一个流程还没有执行完,还有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行情况不会出现任何问题,则称为函数是可以重入的,否则,是不可重入函数
常见线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着调用,状态发生变化的函数
- 返回指向静态变量的指针的函数
- 调用线程不安全的函数
常见不可以重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表管理的
- 调用了标准的I/O库函数,标准I/O库的实现都是以不可重入的方式使用全局数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用malloc或者new开辟出来的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都由函数的调用者提供
- 使用本地数据,或者通过制作全局数据本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那线程就是安全的
- 函数是不可重入的,那不能多个线程使用,有可能引发线程安全
- 如果函数有全局变量,那么函数既不是线程安全的也不是可重入的
- 可重入函数是线程安全的一种
- ‘线程安全不一定是可重入的,但是可重入的一定是线程安全的
- 如果对临界资源上锁,那么这个函数是线程安全的,但是如果这个函数的锁没有被释放会产生死锁,因此是不可重入的