多线程互斥资源管理方案
多线程互斥资源管理方案及线程池的实现
多线程互斥资源管理方案
简介:介绍开发中常用三类访问互斥资源访问方案。
引言
在操作系统中,若某个资源在某一个时刻只能被一个运行中的程序访问时,就称为互斥资源。不可避免的,某些资源会被多个程序同时访问,若在不限制的情况下,多个进程同时访问修改互斥资源,就会造成数据不一致。为了解决这个问题引入两种解决方案:原子指令;操作系统机制。合理运用这些方案,就能实现互斥资源的同步访问。
问题引入
假设现在有10程序需要访问同一个变量count
,并都在其对应的回调函数中对该变量自加100。若各个程序的回调函数按照串行方法访问该变量可得到结果1000。可在计算机中,各个资源的运行时间是由多个层面的调度管理,导致各个程序并不是按照串行方式访问变量count
。因为变量自加在汇编语言层面上并不是一个原子操作,count++
翻译成汇编语言:
mov [count], eax;
inc eax;
mov eax, [count];
可以看到,CPU首先将count
变量的值放入寄存器,对寄存器中的值进行自增,最后把寄存器中的值放入count
变量中。若在该过程中的任意一过程有其他程序对count
进行操作,就会造成结果的不可靠性。
在情况1中,线程一线程二依次通过寄存器来修改count
值,在这个情况下count
正确增加两次,得到了正确的结果。而在情况二中,线程一在将count
中的值放入eax中时,发生了线程切换,此时线程二被分配CPU处理。CPU将count
值放入eax中,对eax进行自增操作,再将eax值放入count
中,线程二运行结束,CPU切换为线程一。此时的eax仍是线程一的count
值,对其自增后将eax值放入count
中。假如count
在一开始值为50,到最后线程1和线程2结果都为51!这就是对互斥资源不当操作所造成的数据一致性错误。
互斥锁及原子操作
基于上述问题,提出了两种解决方案,①操作系统机制②原子操作。
线程基本操作
线程相关函数包含在pthread.h
头文件中。类比于每个人都有自己的身份证号,每一个创建的线程都有其对应的pthread_t
类型的id,在大部分的线程操作中都需要线程id作为参数。用于解决互斥信息的访问冲突的互斥变量:pthread_mutex_t
类型的mutex
变量、pthread_spinlock_t
类型的spinlock
变量,需要在控制线程之前对这两类变量进行初始化,pthread_mutex_init()
需要两个参数:互斥变量的地址以及互斥锁的属性,以此类推pthread_spin_init()
函数参数同上。初始化以上参数,使用pthread_create()
创建新的线程,分别需要四个参数:threadid
指定线程的id、线程的属性、该线程要执行的函数、执行线程对应函数的参数。最后线程使用完毕销毁线程pthread_mutex_destroy
。
互斥锁与自旋锁
现目前接触到的两类互斥变量,在对互斥变量管理时其工作原理也不尽相同。
首先对于互斥锁,使用互斥锁将互斥资源加锁,若后续有新的线程访问互斥资源,发现当前资源被占用,会主动让出CPU;而自旋锁,同样对互斥资源加锁,其余线程发现当前资源被占用后,会不断在原地循环直到该资源被解锁释放。
使用互斥锁对互斥资源加锁:
pthread_mutex_lock(&mutex);
/*****互斥资源*****/
pthread_mutex_unlock(&mutex);
使用自旋锁对互斥资源加锁:
pthread_spin_lock(&spinlock);
/*****互斥资源*****/
pthread_spin_unlock(&spinlock);
因为互斥锁会使得后续进程主动让出CPU,所以互斥锁常用于加锁部分内容较多的情况,反之因为自旋锁会导致其余线程循环等待,因此尽可能让锁的内容少。
原子操作
之前分析可知,导致数据一致性错误根本原因在于对变量count
`的操作在汇编层面被解析成了三条指令,从而当新的线程到来会导致三条指令的运行状态被打乱。那么有没有方法让本来是三条汇编指令的操作变成只需要一条汇编就能实现呢?
从定义上来说,仅需要一条汇编指令运行的程序,就具有原子性,不可分割要么运行要么不运行。在C语言中提供了一种方法来编写编译指令,针对GCC编译集有关键字asm
定义汇编指令。对当前场景有如下代码:
int inc(int* value,int add){
int old;
asm volatile(
"lock; xaddl %2, %1;"
: "=a" (old)
: "m" (*value), "a"(add)
: "cc", "memory"
);
}
lock
确保指令在多线程环境下原子执行,xaddl
将%2
加到%1
并将%1
的旧值存入%2
中最终赋值给old
。"=a" (old)
将结果存入寄存器再赋值给old
。"cc"
代表标志寄存器被修改,"memory"
代表内存被修改。
总结
在多核/多线程环境下的程序,往往需要对互斥资源进行访问,包括读写等操作。本文先是基于简单的例子分析了多线程下的资源互斥访问情况,而后提出了解决该问题的两类方案:加锁、原子操作,在后续开发中合理运用以上方法,开发出更高效且同时兼备线程安全的程序。