为什么需要线程同步?
- 线程同步是为了对共享资源的访问进行保护
- 保护的目的是为了解决数据一致性的问题
- 出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问
Linux系统提供了多种用于实现线程同步的机制,常见的方法有:互斥锁、条件变量、自旋锁以及读写锁等。
互斥锁
这篇文章讲的蛮好的。
但其实在freertos和驱动开发中,都有接触到互斥这个概念。
我感觉有点像是驱动开发里的原子操作。
简单来说,就是在访问共享资源之前对互斥锁进行上锁,在访问完成之后释放互斥锁,对互斥锁进行上锁之后,任何其他试图再次对互斥锁进行加锁的线程都会被阻塞,知道当前线程释放互斥锁。
初始化。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
初始化示例。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
加锁与解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁互斥锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex)
写一段简单的代码测试一下。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg){
int loops = *((int*)arg);
printf("loops = %d\n",loops);
int l_count,j;
for(j = 0;j<loops;j++){
pthread_mutex_lock(&mutex);
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex);
}
return (void*)0;
}
int main(void){
pthread_t tid1,tid2;//创建线程id
int ret;
int loops = 100000000;
pthread_mutex_init(&mutex,NULL);
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "ERROR:%s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret)
{
fprintf(stderr, "ERROR:%s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid1,NULL);
if(ret){
fprintf(stderr,"pthread_join err:%s\n",strerror(ret));
exit(-1);
}
ret = pthread_join(tid2,NULL);
if(ret){
fprintf(stderr,"pthread_join err:%s\n",strerror(ret));
exit(-1);
}
printf("g_count = %d\n",g_count);
pthread_mutex_destroy(&mutex);
exit(0);
}
可以发现,得到了我们预期的结果。
条件变量
使用条件变量主要包括两个动作:
- 一个线程等待某个条件满足而被阻塞
- 另一个线程中,条件满足时发出信号
条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。
条件变量允许一个线程休眠(阻塞等待)直至获取另一个线程的通知(收到信号)再去执行自己的操作。
初始化。
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
通知和等待条件变量。
int pthread_cond_broadcast(pthread_cond_t *cond);//能唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond);//至少能唤醒一个线程
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//等待
注意的是,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。当调用pthread_cond_broadcast唤醒所有线程时,互斥锁也只能被某一线程锁住,其他都会因为获取失败进入阻塞。
示例。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
static pthread_mutex_t mutex;//定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_count = 0; //全局共享资源
static void *new_thread_start(void *arg){
for(;;){
pthread_mutex_lock(&mutex);
while (0>=g_count)
pthread_cond_wait(&cond,&mutex);//等待条件满足
while(0<g_count)
g_count--;
printf("g_count = %d\n",g_count);
pthread_mutex_unlock(&mutex);
}
return (void*)0;
}
int main(void)
{
pthread_t tid1;//创建线程id
int ret;
//初始化互斥锁和条件变量
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
ret = pthread_create(&tid1, NULL, new_thread_start, NULL);
if (ret)
{
fprintf(stderr, "ERROR:%s\n", strerror(ret));
exit(-1);
}
for(;;){
pthread_mutex_lock(&mutex);
g_count++;
printf("g_count0 = %d\n",g_count);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);//发送信号,至少唤醒一个
}
ret = pthread_join(tid1,NULL);
if(ret){
fprintf(stderr,"pthread_join err:%s\n",strerror(ret));
exit(-1);
}
pthread_mutex_destroy(&mutex);
exit(0);
}
自旋锁
自旋锁与互斥锁很相似,自旋锁相较于互斥锁更加的底层。
区别:
- 开销上:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒。获取不到自旋锁会原地“自旋”,直到获取到锁。休眠与唤醒开销时很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁。但自旋锁不适用于等待时间较长的情况。
- 使用场景:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多。自旋锁可以在中断服务函数中使用,但是互斥锁不行。
这个在驱动开发里总结过了,就不总结了。
下一个。
读写锁
读写锁有三种状态:读模式下的加锁状态,写模式下的加锁状态和不加锁状态。
一次只有一个线程可以占有写模式的读写锁,可以有多个线程同时占有读模式的读写锁。
两个规则:
- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是写模式加锁)的线程都会被阻塞。
- 当读写锁处于读加锁状态时,所有试图以读模式对他进行加锁的线程都可以加锁成功,任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
读写锁非常适合于对共享数据读的次数大于写的次数的情况。
参考正点原子linux应用开发教程