线程的同步与互斥
文章目录
一、线程互斥方式 — 互斥锁 /互斥量
1.互斥锁及特点
互斥锁是专门用于处理线程互斥的一种方式,它有两种状态:上锁状态/解锁状态。
特点:如果互斥锁处于上锁状态,那么再上锁就会造成阻塞,直到这把锁解开了之后,才能上锁。解锁状态依然继续解锁,不会阻塞
2.关于线程互斥锁函数接口
1)定义互斥锁变量 -> 数据类型: pthread_mutex_t
2)初始化互斥锁 -> pthread_mutex_init()
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
参数:
mutex: 未初始化过互斥锁变量的地址
mutexattr:普通属性,NULL
返回值:
成功:0
失败:非0错误码
静态初始化:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
也就是说,以上这句话等价于:
pthread_mutex_t m;
pthread_mutex_init(&m,NULL);
3)上锁-> pthread_mutex_lock()
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功:0
失败:非0错误码
4)解锁 -> pthread_mutex_unlock()
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功:0
失败:非0错误码
4)解锁 -> pthread_mutex_unlock()
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功:0
失败:非0错误码
5)销毁互斥锁 -> pthread_mutex_destroy()
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功:0
失败:非0错误码
6)互斥锁使用场景:
当我们使用一些临界资源时,防止多个线程同时访问,我们可以这么做,在访问临界资源前,
让线程先上锁,然后再访问资源,访问完了之后就解锁,让别的线程去上锁。
说明:
临界资源:共享资源(多线程之间需要共同操作的资源)
代码解释:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> //mutex
int g_val = 10;
//1、先定义一个互斥锁变量
pthread_mutex_t mutex;
void* start_routine1(void*arg)
{
//在访问共享资源的时候先上锁
pthread_mutex_lock(&mutex);
g_val = 20;
sleep(1);
printf("start_routine1 g_val:%d\n",g_val);
//共享资源 使用结束的时候要解锁
pthread_mutex_unlock(&mutex);
}
void* start_routine2(void*arg)
{
//在访问 共享资源的时候 先 上锁 --如果没有拿到锁,会阻塞 等待 有锁
pthread_mutex_lock(&mutex);
g_val = 200;
sleep(2);
printf("start_routine2 g_val:%d\n",g_val);
//共享资源 使用结束的时候要解锁
pthread_mutex_unlock(&mutex);
}
int main()
{
//2、初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//1、子线程1
pthread_t thread1;
pthread_create(&thread1,NULL,start_routine1, NULL);
//2、子线程2
pthread_t thread2;
pthread_create(&thread2,NULL,start_routine2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
小练习: 使用互斥锁 +多线程实现整形矩阵(二维整形数组) 所有元素求和问题
定义一个全局变量total累计 所有元素的和
#define N 5
int array[N][N] = {10,20,30,40,50,
11,22,33,44,55,
66,77,88,99,11,
12,31,12,34,45,
12,43,45,66,77};
数组的初始化可以直接 定义的时候初始化 或者 使用 随机数初始化
每一条线程负责计算一行的元素和 ,最后所有线程的元素和相加就是整形矩阵(二维整形数组) 所有元素和
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define N 5
int array[N][N] = {10,20,30,40,50,
11,22,33,44,55,
66,77,88,99,11,
12,31,12,34,45,
12,43,45,66,77};
数组的初始化可以直接定义的时候初始化或者使用随机数初始化
每一条线程负责计算一行的元素和 ,最后所有线程的元素和相加就是 整形矩阵(二维整形数组)所有元素和
#endif
#define N 5
int array[N][N] = {10,20,30,40,50,
11,22,33,44,55,
66,77,88,99,11,
12,31,12,34,45,
12,43,45,66,77};
//定义一个互斥锁变量
pthread_mutex_t mutex;
int total = 0;
void *start_routine(void*arg)
{
//获取当前子线程 要计算第几行的数据
int row = *(int*)arg;
int sum=0;
for(int i=0; i<N; i++){
sum += array[row][i];
}
printf("[%lu]子线程 计算第%d行 sum:%d total:%d start\n",pthread_self(),row,sum,total);
pthread_mutex_lock(&mutex);//加锁力度要小
total +=sum;
pthread_mutex_unlock(&mutex); //解锁
printf("[%lu]子线程 计算第%d行 sum:%d total:%d end\n",pthread_self(),row,sum,total);
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_t thread[N];
//有多少行 就创建多少条线程
for(int i=0; i<N; i++)
{
pthread_create(&thread[i],NULL,start_routine, &i);
//usleep(1); //1us
sleeep(1);
}
for(int i=0; i<N; i++)
{
pthread_join(thread[i], NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
二、问题的引入
代码解释:
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int g_val = 0;
//1)定义互斥锁变量。 -----》数据类型 pthread_mutex_t
pthread_mutex_t mutex;
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
//写操作,修改内存空间的值
g_val = 100;
int i;
for(i=0; i<5; i++)
{
sleep(1);
g_val += g_val*i;
printf("%d routine1 100 g_val:%d\n",i,g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
//写操作,修改内存空间的值
g_val = 200;
int i;
for(i=0; i<5; i++)
{
sleep(1);
g_val += g_val*i;
printf("routine2 200 g_val:%d\n",g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine3(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
int i;
//读操作,此时仅仅只是将这个值打印出来
for(i=0; i<5; i++)
{
sleep(1);
printf("routine3 g_val:%d\n",g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine4(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
int i;
//读操作,此时仅仅只是将这个值打印出来
for(i=0; i<5; i++)
{
sleep(1);
printf("routine4 g_val:%d\n",g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
int main()
{
//2)初始化 互斥锁
pthread_mutex_init(&mutex,NULL);
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
// 创建一个新的线程3
pthread_t thread3;
pthread_create(&thread3,NULL,routine3,NULL);
// 创建一个新的线程4
pthread_t thread4;
pthread_create(&thread4,NULL,routine4,NULL);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
pthread_join(thread3,NULL);
pthread_join(thread4,NULL);
//5)销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
结果:
为了提高效率,有没有什么方法可以让两条线程在进行读操作的时候,可以同时进行呢?而且在进行读操作的时候不可以进行写操作。写操作之间是互斥的;写操作与读操作之间是互斥的;读操作之间是同步的;
答案:互斥锁无法做到,可以使用读写锁
三、读写锁
1.互斥锁的缺陷
互斥锁无论是读取共享资源,还是修改共享资源,都要上锁,而是在上锁期间,不能被别的线程上锁
2.读写锁的优势
访问资源(一起读一本书) -> 同时上读锁 -> 读锁就是一把共享锁
修改资源 -> 不能同时上写锁 -> 写锁就是一把互斥锁
这把既有读锁,又有写锁的锁,就称之为读写锁
3.读写锁函数接口
1)定义一个读写锁变量 (数据类型: pthread_rwlock_t)
pthread_rwlock_t rwlock;
2)初始化读写锁—>pthread_rwlock_init
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t * rwlock,const pthread_rwlockattr_t * attr);
参数:
rwlock:读写锁变量的地址
attr:属性,一般为NULL
返回值:
成功:0
失败:非0错误码
静态初始化:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
3)读锁上锁—>pthread_rwlock_rdlock
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁变量的地址
4)写锁上锁 —>pthread_rwlock_wrlock
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁变量的地址
5)读写锁解锁 —>pthread_rwlock_unlock
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁变量的地址
6)销毁读写锁---->pthread_rwlock_destroy
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁变量的地址
代码解释:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
//定义一个读写锁变量
pthread_rwlock_t rwlock;
int g_val = 10;
void *start_routine1(void*arg)
{
pthread_rwlock_wrlock(&rwlock);//加写锁
g_val = 20;
sleep(3);
printf("start_routine1: %d\n",g_val);
pthread_rwlock_unlock(&rwlock); //解锁
}
void *start_routine2(void*arg)
{
pthread_rwlock_wrlock(&rwlock);//加写锁
g_val = 200;
sleep(3);
printf("start_routine2: %d\n",g_val);
pthread_rwlock_unlock(&rwlock); //解锁
}
void *start_routine3(void*arg)
{
pthread_rwlock_rdlock(&rwlock);//加读锁
//获取数据 访问(读取) 共享资源但是没有进行修改(写入)
int cnt=5;
while(cnt--){
sleep(1);
printf("start_routine3: %d\n",g_val);
}
pthread_rwlock_unlock(&rwlock); //解锁
}
void *start_routine4(void*arg)
{
pthread_rwlock_rdlock(&rwlock);//加读锁
//获取数据 访问(读取) 共享资源 但是没有进行修改(写入)
int cnt=5;
while(cnt--){
sleep(1);
printf("start_routine4: %d\n",g_val);
}
pthread_rwlock_unlock(&rwlock); //解锁
}
int main()
{
//初始化读写锁
pthread_rwlock_init(&rwlock,NULL);
pthread_t thread1;
pthread_create(&thread1,NULL,start_routine1, NULL);
pthread_t thread2;
pthread_create(&thread2,NULL,start_routine2, NULL);
pthread_t thread3;
pthread_create(&thread3,NULL,start_routine3, NULL);
pthread_t thread4;
pthread_create(&thread4,NULL,start_routine4, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_join(thread4, NULL);
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
现象:
两条读线程同时进行,两条写线程不能同时进行。
读取的值,拿到的是最后一次临界资源的值,但是可以保证在读取的过程中临界资源的值是不会被修改。
总结重点;
写操作之间是互斥的;写操作与读操作之间是互斥的;读操作之间是同步的;
三、条件变量(条件变量必须与互斥锁一起使用)
1.条件变量
线程因为某一个条件/情况不成立下,进入一个变量中等待,这个存放线程的变量就是条件变量。(条件变量必须与互斥锁一起使用)
2.关于条件变量的函数接口
1)先定义一个条件变量。 -> 数据类型: pthread_cond_t
pthread_cond_t cond; //condition
2)初始化条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t * attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态初始化
参数:
cond:条件变量的地址
cond_attr 普通属性,NULL。
返回值:
成功:0
失败:非0错误码
3)进入条件变量中等待(两个功能:1.阻塞等待 2.自动解锁)
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
参数:
cond:条件变量的地址
mutex:互斥锁的地址 -> 进入条件变量中,会自动解锁。
返回值:
成功:0
失败:非0错误码
4)唤醒条件变量中等待的线程 -> 线程离开条件变量时,会自动上锁
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);//广播: 唤醒所有在条件变量中等待的线程
int pthread_cond_signal(pthread_cond_t *cond);//单播: 随机唤醒一个在条件变量中等待的线程
参数:
cond:条件变量的地址
返回值:
成功:0
失败:非0错误码
5)销毁条件变量---->pthread_cond_destroy
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond:条件变量的地址
代码解释:
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <sys/sem.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
/*
练习:有4个小孩,每个小孩的任务就是领取生活费1000,回学校之前,父母先在银行卡中存2000块钱,
2个线程拿到1000块钱之后退出,另外2个线程进去条件变量中等待,父亲再打钱1000,唤醒所有的小孩
起来拿钱, 过一会,再打1000块钱,再唤醒最后一个小孩起来拿钱赶紧走人上学。
*/
int g_money = 2000;
//定义一个互斥锁变量
pthread_mutex_t mutex;
//定义一个条件变量并且静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *start_routine(void*arg)
{
printf("[%lu]子线程 start\n",pthread_self());
pthread_mutex_lock(&mutex);//加锁力度要小
//条件不满足的时候进入 条件变量中等待
while(g_money<1000){
printf("没钱了,进去条件变量中等待父母打钱 并且通知.....\n");
//自动解锁 ,并且阻塞等待
pthread_cond_wait(&cond,&mutex);
printf("父母打钱过来了,已经通知我了,此时余额:%d\n",g_money);
}
//走到这里,说明有钱
g_money -=1000;
printf("[%lu]子线程 拿到钱了,此时银行卡余额:%d\n",pthread_self(),g_money);
pthread_mutex_unlock(&mutex); //解锁
printf("[%lu]子线程 end\n",pthread_self());
//拿钱走人
pthread_exit(NULL);
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_t thread1; //我
pthread_create(&thread1,NULL,start_routine, NULL);
pthread_t thread2; //哥
pthread_create(&thread2,NULL,start_routine, NULL);
pthread_t thread3; //姐
pthread_create(&thread3,NULL,start_routine, NULL);
pthread_t thread4; //弟
pthread_create(&thread4,NULL,start_routine, NULL);
int cnt=5;
while(cnt--){
sleep(1);
printf("主线程(父母) 即将准备打钱....%d\n",cnt);
}
//主线程(父母) 打钱
pthread_mutex_lock(&mutex);//加锁
g_money +=1000;
pthread_mutex_unlock(&mutex); //解锁
pthread_cond_broadcast(&cond);//广播: 唤醒所有在条件变量中等待的线程
cnt=5;
while(cnt--){
sleep(1);
printf("主线程(父母) 即将准备打钱....%d\n",cnt);
}
pthread_mutex_lock(&mutex);//加锁
g_money +=1000;
pthread_mutex_unlock(&mutex); //解锁
pthread_cond_signal(&cond);//单播: 随机唤醒一个在条件变量中等待的线程
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_join(thread4, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
四、线程的死锁
1.死锁概念
死锁指的是由于某种逻辑问题,导致等待一把永远无法获得的锁的困境。比如最简单的是同一线程,连续对同一锁资源进行加锁,就进入了死锁
例子:
pthread_mutex_t m;
int main()
{
pthread_mutex_init(&m, NULL);
// 正常加锁
pthread_mutex_lock(&m);
// 未释放锁前重复加锁,进入死锁状态
pthread_mutex_lock(&m);
// 下面的代码永远无法执行
...
...
}
}
以上死锁的例子,可以通过仔细检查代码得以避免,但在现实场景中,有些产生死锁的情况是无法避免的,比如如下情形:
一条线程持有一把锁,期间不能屏蔽取消指令然后又恰巧被取消指令强制终止,此时死锁的产生变得不可避免
void *routine(void *arg)
{
thread_pool *pool = (thread_pool *)arg;
struct task *p;
while(1)
{
// 操作临界资源之前,加锁
pthread_mutex_lock(&pool->lock);
// 条件不允许时,进入条件量等待
while(pool->waiting_tasks == 0 && !pool->shutdown)
pthread_cond_wait(&pool->cond, &pool->lock);
// 条件允许时,操作临界资源
p = pool->task_list->next;
pool->task_list->next = p->next;
pool->waiting_tasks--;
// !!! 注意 !!!
// 线程若恰好在此处被意外终止,将导致死锁
// 解锁
pthread_mutex_unlock(&pool->lock);
// 其他操作
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
(p->do_task)(p->arg);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
free(p);
}
pthread_exit(NULL);
}
2.死锁的解决办法(添加线程取消例程函数)
上述代码中,若线程在中间被取消,则导致死锁。对于这种情况,一个可行的解决办法是:
1.提前准备一个解锁处理函数,并将其压入线程专用的函数栈中备用。
2.准备操作临界资源,加锁
3.操作临界资源
- 重点:
- 若线程在此期间意外终止,则会自动调用处理函数解锁(线程取消例程函数)
4.解锁
5.在函数栈中弹出处理函数。
说明:
上述做法实际上相当于现实生活中的立遗嘱,因为人去世之后是无法再做任何事情的,因此为了防止死亡在关键阶段意外到来,可以在提前立遗嘱,万一不幸遇到该情况就有了预案(处理函数),但如果并未发生此种情形,那么就将遗嘱作废(弹出处理函数且不执行)即可
// 意外处理函数:
// 自动解锁
void handler(void *arg)
{
pthread_mutex_unlock((pthread_mutex_t *)arg);
}
void *routine(void *arg)
{
thread_pool *pool = (thread_pool *)arg;
struct task *p;
while(1)
{
//================================================//
pthread_cleanup_push(handler, (void *)&pool->lock); // 提前准备好意外处理函数
pthread_mutex_lock(&pool->lock);
//================================================//
// 1, no task, and is NOT shutting down, then wait
while(pool->waiting_tasks == 0 && !pool->shutdown)
pthread_cond_wait(&pool->cond, &pool->lock);
// 2, no task, and is shutting down, then exit
if(pool->waiting_tasks == 0 && pool->shutdown == true)
{
pthread_mutex_unlock(&pool->lock);
pthread_exit(NULL); // CANNOT use 'break';
}
// 3, have some task, then consume it
p = pool->task_list->next;
pool->task_list->next = p->next;
pool->waiting_tasks--;
//================================================//
pthread_mutex_unlock(&pool->lock);
pthread_cleanup_pop(0); // 弹出处理函数且不执行
//================================================//
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
(p->do_task)(p->arg);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
free(p);
}
pthread_exit(NULL);
}
总结
下篇线程池…