线程的存在是为实现资源的共享。 因此多个线程同时访问共享数据时可能会冲突,为了解决这种冲突我们需要引入互斥锁。互斥锁的目的是为了保护公共资源(临界资源),但是互斥锁自身又要被多个线程所看到,因此互斥锁也是一个临界资源。因此在上锁和解锁的时候一定要保证原子操作,否则这个互斥锁就会变得毫无意义,使得多个线程依然可以去同时访问这个临界资源。 多个线程产生访问冲突会导致不可预知的结果,举个例子:
#include<stdio.h> #include<pthread.h> static int count =0; void* pthread_run(void *val) { int i=0; int sum=0; while(i<5000) { ++i; sum=count; printf("process :%d,pthread :%u,count :%d\n",getpid(),pthread_self(),count); count=sum+1; //++count; } } int main() { pthread_t id1; pthread_t id2; pthread_create(&id1,NULL,pthread_run,NULL); pthread_create(&id2,NULL,pthread_run,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); printf("%d\n",count); return 0; }在这里由于我们对count加的时候并非原子操作,因此在线程间切换的时候造成了难以预料的结果,实际结果与预期结果的10000不符合,这就是没有加互斥锁带来的缺陷。 分析为什么与预期结果不符合: 线程间进行切换的时机:在 sum=count;这步赋值操作之后由于使用了系统调用接口(printf)在这个时候我们线程由用户态切换到了内核态,之后又会从内核态再切换回用户态。 在这里所说的用户态和内核态可以分别理解成为我们的普通用户和超级用户。 由于在这个时候我们的count可能还没来得及进行sum+1操作就被切换出去了。但此时i已经++了,因此在发生了多次的线程间切换之后就不可能将count累加到我们预期的10000了。
为了解决这个问题,我们引入了互斥锁的概念: 其实在线程里加上互斥锁,就像我们在进程里为了使进程间通信的时候使用二元信号量来实现互斥机制一样。 获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得 锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成⼀一个原子操作,要么 都执行,要么都不执行,不会执⾏行到中间被打断,也不会在其它处理器上并行做这个操作。
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用 pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用 pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执⾏行。 如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已 经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待
pthread_mutex_init函 数初始化的Mutex可以⽤用pthread_mutex_destroy销毁。如果Mutex变量 是静态分配的(全局变量 或static变量),也可以⽤用宏定义PTHREAD_MUTEX_INITIALIZER
#include<stdio.h> #include<pthread.h> pthread_mutex_t mutex_lock=PTHREAD_MUTEX_INITIALIZER; static int count =0; void* pthread_run(void *val) { int i=0; int sum=0; while(i<5000) { pthread_mutex_lock(&mutex_lock); ++i; sum=count; printf("process :%d,pthread :%u,count :%d\n",getpid(),pthread_self(),count); count=sum+1; pthread_mutex_unlock(&mutex_lock); } } int main() { pthread_t id1; pthread_t id2; pthread_create(&id1,NULL,pthread_run,NULL); pthread_create(&id2,NULL,pthread_run,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); printf("%d\n",count); return 0; }那么Mutex的两个基本操作lock和unlock是如何实现的呢?
我们都知道这个锁要被多个线程都同时看到,因此这把锁也属于公共资源(临界资源),我们也需要保护这把锁,让这把锁里的操作都必须保证原子性。
假设Mutex变 量 的值为1表⽰示互斥锁空闲,这时某个进程调⽤用lock可以获得锁,⽽而Mutex的值为0表⽰示互斥锁 已经被 某个线程获得,其它线程再调⽤用lock只能挂起等待。那么lock和unlock的伪代码如下:
lock: if(mutex>0) { mutex=0; return 0; } else 挂起等待 goto unlock; unlock: mutex=1; 唤醒等待mutex的线程 return 0;观察这段代码我们可以看到解锁是一个原子操作。它翻译成汇编语言就只有一条语句是立即数寻址的方式直接mov 寄存器 立即数,因此解锁一定是原子操作。但是观察上锁我们使用mutex去和0比较大小,翻译成汇编语言是多条指令,首先它要将mutex这个值先拿到寄存器需要使用mov 指令,还要讲这个寄存器里的值去和0进行比较,需要比较的指令,一次上锁的操作一定不是原子操作,那么就有可能在刚刚把mutex的值读进寄存器,但是没有来得及执行比较指令并将其置0,而发生了一次线程间切换,此时另一个线程也会去判断,同样在执行将值mutex的值读进寄存器,但是没有来得及执行比较指令并将其置0,切换到了之前的线程执行的位置,执行完成之后再切换回来执行另一个线程的操作,此时两个线程都认为自己拿到了这把锁,可以访问临界资源,那么访问冲突便没有解决,这个互斥锁也就毫无意义。
所以其实应该是这样的: 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作⽤用是把寄 存器和内存单元的数据相交换,由于只有一条指令,保证了原⼦子性,即使是多处理器平台,访问 内存的 总线周期也有先后,一个处理器上的交换指令执行时另⼀一个处理器的交换指令只能等 待总线周期。 现在我们把lock和unlock的伪代码改⼀一下(以x86的xchg指令为例):
lock: movb $0 %al xchgb %al ,mutex if(al寄存器的内容>0) { return 0; } else 挂起等待 goto lock; unclock: movb $1 ,mutex 唤醒等待线程 return 0;观察这段代码的上锁操作:首先先将自己寄存器里的值清 0,然后使用一条交换指令将此时锁的值和寄存器的值进行交换,如果交换之后此时发生了一次线程间的切换,那么另一个线程也会先将自己 的寄存器的值清0,但是注意此时锁的变量的值是0,因此另一个线程根本就不会获得锁。也不会存在上面一个代码所引出的问题。