场景:车站里有很多的车票。很多人来抢购车票。
上面场景中车站所拥有的车票,是所有用户共同操作的资源。我们把这种资源称为临界资源。在并发条件下,对临界资源的操作,经常会出现非原子变量赋值出现错误。原因如下:
- 非原子赋值大多数都不是一条指令能完成的。
- 线程1将临界资源中的非原子变量改到一半,线程2正好就来读取了这个半成品变量,并基于此去做计算,可能会造成错误。
举例
有一个池子number,十台机器。每个机器会一个一个球地往池子里放球,十个机器同时往池子里放球。
各机器各放一万个球。
不熟悉pthread操作的可以看文末的注解。1
代码如下:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define thread_count 10
//对线程操作的函数。每个线程对number进行加10000
void* number_count(void *number){
int* pnum = (int *)number;
for(int i = 0;i < 10000; i++){
//这个赋值操作是关键
(*pnum) ++;
usleep(1);
}
}
int main(){
// 初始化线程id
pthread_t thread_id[thread_count] = {0};
// number是临界资源
int number = 0;
//创建十条线程共同操作number这个临界资源
for(int i = 0;i < thread_count; i++){
pthread_create(&thread_id[i],NULL,&number_count,&number);
}
//每隔一秒打印一次number的值
for(int j = 0;j < 100; j++){
printf("number:%d\n",number);
sleep(1);
}
}
以上代码不考虑非原子操作,最后number应该是能加到十万的。然而结果如下:
最后number并没有加到十万。为什么呢???
临界资源的操作
上例中的临界资源是number。其中语句(*pnum)++是非原子变量赋值。非原子变量赋值的意思是这个语句虽然是一句,但是线程实际操作的时候是被拆分多个原子语句执行的。这里的(*pnum)++便可拆分为以下原子语句
// eax为寄存器。先把number的数给寄存器eax,
// 然后eax自增1.最后把eax的值给number。
mov [number],eax;
inc eax;
mov eax,[number];
我们之前预想的情况是一个线程执行完,另一个线程再执行。这样临界资源就能加到十万了。但是实际情况是总有多个线程同时操作临界资源。那就会有如下结果:
// 此时线程1在操作临界资源。number为100。
mov [number],eax;
inc eax;
// 线程1还没运行完。线程2也开始操作number
// 线程2此时的number也是为100
mov [number],eax;
inc eax;
mov eax,[number];// 线程2操作后的number为101
//下面的语句是线程1的语句。此时寄存器内的值为101
mov eax,[number];// 线程1操作后的number也为101
如上情况,两个线程都操作了number,但是只加了1。最后导致结果的错误。那如何解决这个问题呢?
- 一是,我们可以把这几个原子语句“捆绑”起来。就是我这几个语句还没执行完,其他的线程就先别执行了。
- 二是,我们直接把这个非原子语句。直接转化为原子语句。
互斥锁(mutex)
互斥锁能实现将几个原子语句“捆绑”起来的效果。当我们在这个原子语句前加个互斥锁后 ,当一个线程在操作这个临界资源的时候 ,其他线程检测到这个临界资源是关锁的状态,其他线程会切换成等待,等待下次被调用 。也就是在同一时刻,只有一个线程能操作这个临界资源。
使用互斥锁的场景:锁的内容比较多的场景 。如:线程安全的rbtree,添加可以使用mutex。
代码如下:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define thread_count 10
#define INFO printf
//定义一把互斥锁
pthread_mutex_t mutex;
void* number_count(void *number){
int* pnum = (int *)number;
for(int i = 0;i < 10000; i++){
#if 0
(*pnum) ++;
#else
// 使用互斥锁
pthread_mutex_lock(&mutex);
(*pnum)++;
// 开锁
pthread_mutex_unlock(&mutex);
#endif
usleep(1);
}
}
int main(){
// 初始化线程id
pthread_t thread_id[thread_count] = {0};
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
int number = 0;
for(int i = 0;i < thread_count; i++){
pthread_create(&thread_id[i],NULL,&number_count,&number);
}
for(int j = 0;j < 100; j++){
INFO("number:%d\n",number);
sleep(1);
}
}
结果如下:
从结果可知,number成功加到了十万。也说明了互斥锁(mutex)的有效性。
自旋锁
自旋锁,跟互斥锁功能类似。也能实现多个原子语句的捆绑。其原理是当临界资源被锁后,其他线程会不断地检测临界资源是否开锁,相当于while(1);
好处:可以很快地获得到锁,效率较高。
坏处:因为在不断地检测。如果等待的时间较长,很浪费cpu的资源 。
适合的场景:锁的内容较少的场景
代码如下 :
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define thread_count 10
#define INFO printf
//定义一把互斥锁
pthread_mutex_t mutex;
//定义一把自旋锁
pthread_spinlock_t spin;
void* number_count(void *number){
int* pnum = (int *)number;
for(int i = 0;i < 10000; i++){
#if 0
(*pnum) ++;
#elif 0
//使用互斥锁
pthread_mutex_lock(&mutex);
(*pnum)++;
pthread_mutex_unlock(&mutex);
#else
//使用自旋锁
pthread_spin_lock(&spin);
(*pnum)++;
pthread_spin_unlock(&spin);
#endif
usleep(1);
}
}
int main(){
pthread_t thread_id[thread_count] = {0};// 这里要注意初始化
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//初始化自旋锁
pthread_spin_init(&spin,PTHREAD_PROCESS_SHARED);
int number = 0;
for(int i = 0;i < thread_count; i++){
pthread_create(&thread_id[i],NULL,&number_count,&number);
}
for(int j = 0;j < 100; j++){
INFO("number:%d\n",number);
sleep(1);
}
}
原子操作
原子操作指的是一个或者一系列不可拆分的操作。也就是不会被线程调度机制打断的操作,运行期间不会发生任何的上下文切换。使用原子操作可以将多个指令转化为一个指令,使其没有被分割的可能。
例子的代码实现:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define thread_count 10
#define INFO printf
int inc(int *count,int step){
int old;
__asm__ volatile(
"lock; xaddl %2, %1;"
: "=a" (old)
: "m" (*count),"a"(step)
: "cc", "memory"
);
return old;
}
void* number_count(void *number){
int* pnum = (int *)number;
for(int i = 0;i < 10000; i++){
#if 0
(*pnum) ++;
#else
inc(number,1);
#endif
usleep(1);
}
}
int main(){
pthread_t thread_id[thread_count] = {0};
int number = 0;
for(int i = 0;i < thread_count; i++){
pthread_create(&thread_id[i],NULL,&number_count,&number);
}
for(int j = 0;j < 100; j++){
INFO("number:%d\n",number);
sleep(1);
}
}
汇编指令解释,请参考注解链接2