在多线程编程中,对共享资源的并发访问是一个常见且棘手的问题。如果多个线程同时对共享资源进行读写操作,很容易出现数据竞争和不一致的情况,从而导致程序出现难以调试的错误。为了解决这个问题,我们通常会使用各种同步机制,如互斥锁、自旋锁和原子操作。本文将通过一个具体的 C 语言示例,详细介绍这些同步机制的使用和特点。
1.我们的示例代码旨在模拟多个线程同时对一个共享计数器进行递增操作的场景。通过使用不同的同步机制,我们可以观察到它们在性能和数据一致性方面的差异。以下是完整的代码
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#define THREAD_COUNT 10
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;
int inc(int *value,int add){
int old;
__asm__ volatile(
"lock;xaddl %2,%1"
:"=a"(old)
:"m"(*value),"a"(add)
:"cc","memory"
);
}
void *thread_callback(void*arg){
int *pcount=(int*)arg;
int i =0;
while (i++<100000){
#if 0
(*pcount)++;
#elif 0
//互斥锁 适用场景:锁的内容比较多,比如,线程安全的rbtree,添加可以用mutex
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);
#elif 0
//自旋锁 适用场景:锁的内容少
pthread_spin_lock(&spinlock);;
(*pcount)++;
pthread_spin_unlock(&spinlock);
#else
//原子操作 单条CPU指令实现
inc(pcount,1);
#endif
usleep(1);
}
}
int main(){
pthread_t threadid[THREAD_COUNT] = {0};
pthread_mutex_init(&mutex,NULL);
pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);
int i =0;
int count=0;
for( i =0;i<THREAD_COUNT;i++){
pthread_create(&threadid[i],NULL, thread_callback,&count);
}
for (int i = 0; i <100; i++) {
printf("count: %d\n",count);
sleep(1);
}
}
2.锁的定义
pthread_mutex_t
:互斥锁类型,用于保护共享资源,确保同一时间只有一个线程可以访问。pthread_spinlock_t
:自旋锁类型,线程在等待锁时会一直忙等待,直到锁可用。
3.锁的实现
互斥锁:使用 pthread_mutex_lock
和 pthread_mutex_unlock
保护对 count
的访问,确保同一时间只有一个线程可以修改 count
。互斥锁适用于锁的内容较多的场景,因为线程在等待锁时会被阻塞,不会消耗 CPU 资源。
自旋锁:使用 pthread_spin_lock
和 pthread_spin_unlock
保护对 count
的访问。自旋锁适用于锁的内容较少的场景,因为线程在等待锁时会一直忙等待,会消耗 CPU 资源,但避免了线程上下文切换的开销
4.原子操作
- 该函数使用内联汇编实现了原子操作。
lock;xaddl %2,%1
是一条 x86 汇编指令,lock
前缀确保该操作是原子的,xaddl
指令将add
的值加到*value
上,并返回*value
的旧值。 - 通过这种方式,我们可以在不使用锁的情况下实现对共享变量的原子递增操作
- 调用
inc
函数实现原子递增,这种方式使用单条 CPU 指令完成操作,不需要额外的锁机制,性能较高。
5.锁的销毁
应该在程序结束前调用 pthread_mutex_destroy
和 pthread_spin_destroy
来释放锁资源。