没有加锁而造成的数据竞争
任务:使用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;
//依次增加100000
while(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 10
pthread_mutex_t mutex;
//线程回调函数
void *thread_callback(void *args)
{
int *pcount = (int *)args;
int i = 0;
//依次增加100000
while(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 10
pthread_spinlock_t spinlock;
//线程回调函数
void *thread_callback(void *args)
{
int *pcount = (int *)args;
int i = 0;
//依次增加100000
while(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;
//依次增加100000
while(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;
}
实验结果
可以看出原子操作的时间开销相比于互斥锁以及自旋锁是最低的。