没有加锁而造成的数据竞争
任务:使用10个线程,同时对一个count加100000;最后我们期望的结果是100000;
实验代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>#define THREAD_COUNT 10//线程回调函数
void *thread_callback(void *args)
{int *pcount = (int *)args;int i = 0;//依次增加100000while(i ++ < 100000){(*pcount)++; //无锁 usleep(1); //睡眠1微秒}}int main()
{clock_t start, finish;void *thread_result;pthread_t threadid[THREAD_COUNT] = {0};start = clock();int i = 0;int count = 0;for(i = 0; i < THREAD_COUNT; ++i){//创建线程pthread_create(&threadid[i], NULL, thread_callback, &count);}for(i = 0; i < THREAD_COUNT; ++i) {pthread_join(threadid[i], &thread_result);}finish = clock();printf("count: %d\n", count);printf("NoLock : %f\n", (double)(finish - start) / CLOCKS_PER_SEC);return 0;
}
实验结果
观察发现,count并没有加到1000000,原因如下:
(*pcount)++
可以分解为3条汇编指令
mov [count], eax;
inc eax;
mov eax, [count];
在并发环境下,会出现如下的执行顺序
如:count = 50, 线程1先执行 mov [count], eax;
即eax = 50
然后转到线程2执行三条汇编指令,执行后count为51;
此时再转回去线程1执行剩余两条语句,由于前面eax = 50,经过inc eax
之后eax变为51,再经过mov eax, [count]
,count 变为51,发现两次执行(*pcount)++
之后 *pcount只自增了1。
*解决办法:在进行 (pcount)++时进行加锁
互斥锁(mutex)
实现代码如下
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>#define THREAD_COUNT 10pthread_mutex_t mutex;
//线程回调函数
void *thread_callback(void *args)
{int *pcount = (int *)args;int i = 0;//依次增加100000while(i ++ < 100000){pthread_mutex_lock(&mutex); //加互斥锁(*pcount)++;pthread_mutex_unlock(&mutex); //解锁usleep(1); //睡眠1微秒}}int main()
{clock_t start, finish;void *thread_result;pthread_t threadid[THREAD_COUNT] = {0};start = clock();pthread_mutex_init(&mutex, NULL);int i = 0;int count = 0;for(i = 0; i < THREAD_COUNT; ++i){//创建线程pthread_create(&threadid[i], NULL, thread_callback, &count);}for(i = 0; i < THREAD_COUNT; ++i) {pthread_join(threadid[i], &thread_result);}finish = clock();printf("count: %d\n", count);printf("mutex : %f\n", (double)(finish - start) / CLOCKS_PER_SEC);pthread_mutex_destroy(&mutex);return 0;
}
实验结果
可以看出,count最终加到了1000000,解决了数据竞争的问题,但是互斥锁会引起线程切换,适合于锁的内容比较多的场景使用,比如线程安全的rbtree
添加节点。
自旋锁(spinlock)
自旋锁不会引起线程切换,不会让出CPU,一直在原地空转。适合于锁的内容较少的情况下使用。
代码如下
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>#define THREAD_COUNT 10pthread_spinlock_t spinlock;
//线程回调函数
void *thread_callback(void *args)
{int *pcount = (int *)args;int i = 0;//依次增加100000while(i ++ < 100000){pthread_spin_lock(&spinlock);(*pcount)++;pthread_spin_unlock(&spinlock);//usleep(1); //睡眠1微秒}}int main()
{clock_t start, finish;void *thread_result;pthread_t threadid[THREAD_COUNT] = {0};start = clock();pthread_spin_init(&mutex, NULL);int i = 0;int count = 0;for(i = 0; i < THREAD_COUNT; ++i){//创建线程pthread_create(&threadid[i], NULL, thread_callback, &count);}for(i = 0; i < THREAD_COUNT; ++i) {pthread_join(threadid[i], &thread_result);}finish = clock();printf("count: %d\n", count);printf("mutex : %f\n", (double)(finish - start) / CLOCKS_PER_SEC);pthread_spin_destroy(&mutex);return 0;
}
实验结果
可以看出也能解决数据竞争问题,但是时间效率低。
原子操作
原子操作就是通过汇编实现单条CPU指令实现(*pcount++)
操作
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>#define THREAD_COUNT 10//将三条汇编合成一个原子操作
int inc(int *value, int add)
{int old;__asm__ volatile("lock; xaddl %2, %1;" //%1 = %2 + %1: "=a" (old): "m" (*value), "a"(add) //%1为 *value, %2为add: "cc", "memory");return old;}
//线程回调函数
void *thread_callback(void *args)
{int *pcount = (int *)args;int i = 0;//依次增加100000while(i ++ < 100000){//封装成原子操作inc(pcount, 1); }}int main()
{clock_t start, finish;void *thread_result;pthread_t threadid[THREAD_COUNT] = {0};start = clock();int i = 0;int count = 0;for(i = 0; i < THREAD_COUNT; ++i){//创建线程pthread_create(&threadid[i], NULL, thread_callback, &count);}for(i = 0; i < THREAD_COUNT; ++i) {pthread_join(threadid[i], &thread_result);}finish = clock();printf("count: %d\n", count);printf("atomic : %f\n", (double)(finish - start) / CLOCKS_PER_SEC);return 0;
}
实验结果
可以看出原子操作的时间开销相比于互斥锁以及自旋锁是最低的。