4、线程同步
1、线程同步的概念
线程同步:指某一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
2、线程同步的例子
创建两个线程,让两个线程共享一个全局变量 int number,然后让每个线程数 5000 次,看最后打印出这个 number 值是多少?
线程A代码片段
线程B代码片段
- 代码片段说明
- 代码中使用 usleep 是为了让两个线程能够轮流使用 CPU,避免一个子线程在一个 CPU 时间片内完成 5000 次数数。
- 对 number 执行 ++ 操作,使用了中间变量 cur 是为了尽可能的模拟 cpu 时间片用完而让出 CPU 的情况。
- 测试结果:
- 经过多次测试最后的结果显示,有可能会出现 number 值少于 5000 * 2 = 10000 的情况
- 原因分析:
- 加入子线程 A 执行完了 cur++ 操作,还没有将 cur 的值赋值给 number 时失去了 CPU 的执行权,子线程 B 得到了 CPU 的执行权,而子线程 B 最后执行完了 number = cur ,而后失去了 CPU 的执行权;此时子线程 A 又重新得到 CPU 的执行权,并接着执行 number = cur; 操作,但是这样就会把子线程 B 刚刚写入到 number 的值覆盖掉,造成 number 值没有变化,不符合预期的值。
-
数据混乱的原因
- 资源共享(独享资源就不会产生该现象)
- 调度随机(线程操作共享资源的先后顺序是不确定的)
- 线程间缺乏必要的同步机制
以上3点中,前两点不能改变,想要提高效率,传递数据,资源必须共享。只要共享资源,就一定会产生资源竞争。只要有竞争关系,数据就很容易出现数据混乱。所以只能从第三点来解决。让多个线程在访问共享资源的时候,出现互斥即可。
-
如何解决问题
- 原子操作的概念
- 原子操作指的是该操作要么就不做,要么就完成,只能出现一个。
- 使用互斥锁解决同步问题
- 使用互斥锁其实是模拟原子操作,互斥锁示意图
- 原子操作的概念
Linux中提供一把互斥锁 mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁后才能操作,操作结束之后进行解锁。
资源还是共享的,线程间也还是竞争的,但是通过“锁”就能将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
线程 1 访问共享资源的时候先要判断锁是否锁着,如果锁着就阻塞等待;如果锁是解开的那么就先把锁加锁,此时就可以访问共享资源了,访问完成后释放锁即可,这样其他线程也就有机会获得这把锁了。
注意:
图中同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不会释放锁。
使用完互斥锁之后,两个线程由并行操作变成了串行操作,效率虽然降低了,但是数据不一致的问题就解决了。
3、互斥锁主要相关函数
-
pthread_mutex_t 类型
- 其本质是一个结构体,为了简化理解,在使用时可以忽略其实现细节,简单当成整数看待即可。
- pthread_mutex_t mutex; 变量 mutex 只有两种取值 0 和 1
-
pthread_mutex_init 函数
-
函数描述:初始化一个互斥锁(互斥量),初值可以看作 1
-
函数原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
-
函数参数:
-
mutex:传出参数,调用时应传递 &mutex
-
attr:互斥锁属性。是一个传入参数,通常传递NULL,选用默认属性(线程间共享)
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存的中内容的操作,只能通过本指针来完成。不能通过除本指针以外的其他变量或者指针来修改互斥量 mutex 的两种初始化方式:
-
静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或者加了 static 关键字修饰),可以直接使用宏进行初始化。
-
动态初始化:局部变量应该采用动态初始化
pthread_mutex_init(&mutex, NULL);
-
-
-
-
pthread_mutex_destory 函数
- 函数描述:销毁一个互斥锁
- 函数原型:int pthread_mutex_destory(pthread_mutex_t *mutex);
- 函数参数:mutex 互斥锁变量
-
pthread_mutex_lock 函数
- 函数描述:对互斥锁加锁,可以理解为 mutex –
- 函数原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
- 函数参数:mutex 互斥锁变量
-
pthread_mutex_unlock 函数
- 函数描述:对互斥锁解锁,可以理解为将 mutex ++
- 函数原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 函数参数:mutex 互斥锁变量
-
pthread_mutex_trylock 函数
- 函数描述:尝试加锁
- 函数原型:int pthread_mutex_trylock(pthread_mutex_t *mute);
- 函数参数:mutex 互斥锁变量
4、加锁和解锁
- lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥锁(互斥量)的其它线程解锁为止。
- unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于线程优先级和线程调度。默认:先阻塞的先唤醒
练习:使用互斥锁解决两个线程数数不一致的问题
代码片段:在访问共享资源的时候,访问结束后理解解锁,注意,锁的粒度应该越小越好,一般都是共享资源操作前后加锁
总结:使用互斥锁之后,两个线程由并行变成了串行,效率虽然降低了,但是可以让两个线程同步操作共享资源,从而解决了数据不一致的问题。
线程同步:
互斥锁: 线程A和线程B共同访问共享资源, 当线程A想访问共享资源的时候,
要先获得锁, 如果锁被占用, 则加锁不成功需要阻塞等待对方释放锁;
若锁没有被占用, 则获得锁成功–加锁, 然后操作共享资源, 操作完之后,
必须解锁, 同理B也是和A一样.
---->也就是说, 同时不能有两个线程访问共享资源, 属于互斥操作.
5、互斥锁
1、互斥锁的使用步骤
-
创建一把互斥锁
- pthread_mutex_t mutex;
-
初始化互斥锁
- pthread_mutex_init(&mutex); 相当于 mutex=1
-
在代码中寻找共享资源(也称为临界区)
-
pthread_mutex_lock(&mutex); mutex=0
[临界区代码]
pthread_mutex_unlock(&mutex); mutex=1
-
-
释放互斥锁资源
- pthread_mutex_destroy(&mutex);
注意:
必须在所有操作共享资源的线程上都加上锁否则不能起到同步的效果
2、练习使用互斥锁
编写思路
-
定义一把互斥锁,应该为全局变量
pthread_mutex_t mutex;
-
在 main 函数中对 mutex 进行初始化
pthread_mutex_init(&mutex);
-
创建两个线程,在两个资源中加锁和解锁
-
主线程释放互斥锁资源
pthread_mutex_destroy(&mutex);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
//定义一把锁
pthread_mutex_t mutex;
void *mythread1(void *args)
{
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand()%3);
printf("world\n");
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
pthread_exit(NULL);
}
void *mythread2(void *args)
{
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//随机数种子
srand(time(NULL));
//互斥锁初始化
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&thread1, NULL, mythread1, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
3、死锁
死锁并不是Linux提供给用户的一种使用方法,而是由于用户使用互斥锁不当引起的一种现象。
-
常见死锁有两种
-
第一种:自己锁自己
-
第二种:线程 A 拥有 A 锁,请求获得 B 锁;线程 B 拥有 B 锁,请求获得 A 锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁:
-
-
如何解决死锁:
- 让线程按照一定的顺序去访问共享资源
- 在访问其他锁的时候,需要先将自己当前的锁解开
- 调用 pthread_mutex_trylock,如果加锁不成功会立刻返回,不会阻塞等待
注意
线程在异常退出的时候也需要解锁
6、读写锁
1、什么是读写锁
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。也就是,写独占,读共享。
读写锁是一把锁
- 读写锁使用场合
- 读写锁非常适合对于数据结构读的次数远远大于写的情况
2、读写锁特性
- 读写锁是 “写模式加锁” 时,解锁前,所有对该锁想要进行加锁的线程都会被阻塞。
- 读写锁是 “读模式加锁” 时,如果线程以读模式对其加锁时会成功;如果线程以写模式加锁就会阻塞。
- 读写锁是 “读模式加锁” 时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞后面的读模式加锁请求,优先让写模式加锁先获得锁。也就是说,读锁和写锁并行执行时,写锁的优先级更高。
3、读写锁场景练习
- 线程A加写锁成功, 线程B请求读锁
- 线程B阻塞,
当线程A解锁之后, 线程B加锁成功
- 线程B阻塞,
- 线程A持有读锁, 线程B请求写锁
- 线程B会阻塞;
当线程A解锁之后, 线程B加锁成功
- 线程B会阻塞;
- 线程A拥有读锁, 线程B请求读锁
- 线程B请求锁成功
- 线程A持有读锁, 然后线程B请求写锁, 线程C请求读锁
- 线程B和C都阻塞;
当A释放锁之后, B先获得锁, C阻塞
当B释放锁之后, C获得锁
- 线程B和C都阻塞;
- 线程A持有写锁, 然后线程B请求读锁, 然后线程C请求写锁
- 线程B和C都阻塞;
当线程A解锁之后, C先获得锁, B阻塞;
当C解锁之后, B获得锁
- 线程B和C都阻塞;
读写锁总结
写独占, 读共享, 当读和写一起等待锁的时候, 写的优先级高
4、读写锁主要操作函数
-
定义一把读写锁
- pthread_rwlock_t rwlock;
-
初始化读写锁
-
int pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock,
const ptherad_rwlockattr_t *restrict attr);
-
函数参数:
- rwlock:读写锁
- attr:读写锁属性,传 NULL 为默认属性
-
-
销毁读写锁
- int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
-
加读锁
- int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
-
尝试加读锁
- int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
-
加写锁
- int pthread_rwlock_wrlock(pthread_rwlock_t *wrlock);
-
尝试加写锁
- int pthread_rwlock_trywrlock(pthread_rwlock_t *wrlock);
-
解锁
- int pthread_rwlock_unlock(&pthread_rwlock_t *rwlock);
读写锁使用步骤:
1 先定义一把读写锁:
pthread_rwlock_t rwlock;
2 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
3 加锁
pthread_rwlock_rdlock(&rwlock);---->加读锁
pthread_rwlock_wrlock(&rwlock);---->加写锁
共享资源出现的位置
/
4 解锁
pthread_rwlock_unlock(&rwlock);
5 释放锁
pthread_rwlock_destroy(&rwlock);
练习:3个线程不定时写同一块全局资源,5个线程不定时读同一块共享资源
//读写锁测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void *thread_write(void *arg)
{
int i = *(int *)arg;
int cur;
while(1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur = number;
cur++;
number = cur;
printf("[%d]-W:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
//读线程回调函数
void *thread_read(void *arg)
{
int i = *(int *)arg;
int cur;
while(1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur = number;
printf("[%d]-R:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
int main()
{
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//读写锁初始化
pthread_rwlock_init(&rwlock, NULL);
//创建3个写子线程
for(i=0; i<3; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读子线程
for(i=3; i<n; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j = 0;
for(j=0;j<n; j++)
{
pthread_join(thread[j], NULL);
}
//释放锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
7、条件变量
- 条件变量本身不是锁,但是它也可以造成线程阻塞。通常配合互斥锁一起使用。这样就会给多线程提供一个会和的机会。
- 使用互斥锁保护共享数据
- 使用条件变量可以使线程阻塞,等待某个条件的产生,当条件满足的时候线程就会解除阻塞
- 条件变量的两个动作
- 条件不满足,阻塞线程
- 条件满足,通知阻塞的线程解除阻塞,该线程开始工作
1、条件变量相关函数
-
pthread_cond_t cond;
- 函数描述:定义一个条件变量
-
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
-
函数描述:初始化条件变量
-
函数参数:
cond:条件变量
attr:条件变量属性,通常传 NULL
-
函数返回值:
成功:返回 0
失败:返回 错误号
-
-
int pthread_cond_destroy(pthread_cond_t *cond);
- 函数描述:销毁条件变量
- 函数参数:条件变量
- 函数返回值:成功返回 0,失败返回 错误号
-
int pthread_cond_wait(pthread_cond_t *restrict cond,
const ptherad_mutex_t *restrict mutex);
-
函数描述:条件不满足,引起线程阻塞并解锁
条件满足,解除线程阻塞,并加锁
-
函数参数:
- cond:条件变量
- mutex:互斥锁变量
-
函数返回值:成功 0,失败返回 错误号
-
-
int pthread_cond_signal(pthead_cond_t *cond);
- 函数描述:唤醒至少一个阻塞在该条件变量上的线程
- 函数参数:条件变量
- 函数返回值:成功返回 0,失败返回 错误号
2、生产者和消费者模型
使用条件变量和互斥锁模拟生产者和消费者模型
代码
//使用条件变量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
NODE *head = NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者线程
void *producer(void *arg)
{
NODE *pNode = NULL;
int n = *(int *)arg;
while(1)
{
//生产一个节点
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("P[%d]:[%d]\n", n, pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next = head;
head = pNode;
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand()%3);
}
}
//消费者线程
void *consumer(void *arg)
{
NODE *pNode = NULL;
int n = *(int *)arg;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
if(head==NULL)
{
//若条件不满足,需要阻塞等待
//若条件不满足,则阻塞等待并解锁;
//若条件满足(被生成者线程调用pthread_cond_signal函数通知),解除阻塞并加锁
pthread_cond_wait(&cond, &mutex);
}
if(head==NULL)
{
//解锁
pthread_mutex_unlock(&mutex);
continue;
}
printf("C[%d]:[%d]\n", n, head->data);
pNode = head;
head = head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main()
{
int ret;
int i = 0;
pthread_t thread1[5];
pthread_t thread2[5];
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//条件变量初始化
pthread_cond_init(&cond, NULL);
int arr[5];
for(i=0; i<5; i++)
{
arr[i]= i;
//创建生产者线程
ret = pthread_create(&thread1[i], NULL, producer, &arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2[i], NULL, consumer, &arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//等待线程结束
for(i=0; i<5; i++)
{
pthread_join(thread1[i], NULL);
pthread_join(thread2[i], NULL);
}
//释放互斥锁
pthread_mutex_destroy(&mutex);
//释放条件变量
pthread_cond_destroy(&cond);
return 0;
}
代码分析
注意:
上述代码,生产者调用 pthread_cond_signal 函数会使消费者线程在 pthread_cond_wait 处解除阻塞
另外,pthread_cond_wait 函数如果在条件不满足时会阻塞等待,并且解锁
条件满足后就会解除阻塞,同时加锁,因为不满足的时候已经解锁了,需要再次加锁
条件变量:
1 定义条件变量
pthread_cont_t cond;
2 初始化条件变量
pthread_cond_init(&cond, NULL);
3 在生成者线程中调用:
pthread_cond_signal(&cond);
4 在消费者线程中调用:
pthread_cond_wait(&cond, &mutex);
5 释放条件变量
pthread_cond_destroy(&cond);
多个生成者和多个消费者程序在执行的时候core掉的原因分析:
假若只有一个生产者生产了一个节点, 此时会调用pthread_cond_signal通知
消费者线程, 此时若有多个消费者被唤醒了, 则最终只有一个消费者获得锁, 然后进行
消费, 此时会将head置为NULL, 然后其余的几个消费者线程只会有一个线程获得锁,
然后读取head的内容就会core掉.所以需要判断 head是否为空,使用 pthread_cond_wait函数进行阻塞
在使用条件变量的线程中, 能够引起线程的阻塞的地方有两个:
1 在条件变量处引起阻塞---->这个阻塞会被pthread_cond_signal解除阻塞
2 互斥锁也会使线程引起阻塞----->其他线程解锁会使该线程解除阻塞.
8、信号量
信号量相当于多把锁,可以理解为加强版的互斥锁
1、信号量相关函数
- 定义信号量 sem_t sem;
- int sem_init(sem_t *sem,int pshared,unsigned int value);
- 函数描述:初始化信号量
- 函数参数:
- sem:信号量 变量
- pshared:0 表示线程同步,1 表示进程同步
- value:最多有几个线程操作共享数据
- 函数返回值:
- 成功:返回 0
- 失败:返回 -1,并设置 errno值
- int sem_wait(sem_t *sem);
- 函数描述:调用该函数一次,相当于 sem --,当 sem 等于 0 的时候,会引起阻塞
- 函数参数:信号量变量
- 函数返回值:成功返回 0,失败返回 -1,并设置 errno 值
- int sem_post(sem_t *sem);
- 函数描述:调用一次,相当于 sem++
- 函数参数:信号量变量
- 函数返回值:成功返回 0,失败返回 -1,并设置 errno 值
- int sem_destroy(sem_t *sem);
- 函数描述:销毁信号量
- 函数参数:信号量变量
- 函数返回值:成功返回 0,失败返回 -1,并设置 errno 值
2、生产者消费者模型
使用信号量模拟生产者消费者模型
//使用信号量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
NODE *head = NULL;
//定义信号量
sem_t sem_producer;
sem_t sem_consumer;
//生产者线程
void *producer(void *arg)
{
NODE *pNode = NULL;
while(1)
{
//生产一个节点
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("P:[%d]\n", pNode->data);
//加锁
sem_wait(&sem_producer); //sem_producer-- 为0时会阻塞
pNode->next = head;
head = pNode;
//解锁
sem_post(&sem_consumer); //相当于++
sleep(rand()%3);
}
}
//消费者线程
void *consumer(void *arg)
{
NODE *pNode = NULL;
while(1)
{
//加锁
sem_wait(&sem_consumer); //相当于sem_consumer-- 为0时会阻塞
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
//解锁
sem_post(&sem_producer); //相当于++
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//初始化信号量
sem_init(&sem_producer, 0, 5);
sem_init(&sem_consumer, 0, 0);
//创建生产者线程
ret = pthread_create(&thread1, NULL, producer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2, NULL, consumer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放信号量资源
sem_destroy(&sem_producer);
sem_destroy(&sem_consumer);
return 0;
}
信号量:
1 定义信号量变量
sem_t sem1;
sem_t sem2;
2 初始化信号量
sem_init(&sem1, 0, 5);
sem_init(&sem2, 0, 5);
3 加锁
sem_wait(&sem1);
//共享资源
sem_post(&sem2);
sem_wait(&sem2);
//共享资源
sem_post(&sem1);
4 释放资源
sem_destroy(sem1);
sem_destroy(sem2);