一、线程的互斥
临界资源:系统中多个任务都可以访问的资源。
在多线程编程中,必须要注意对临界资源的保护。
问题:假设在一个办公室内,有一台打印机,这个打印机所有人都可以共享使用,但是在某一个人使用时,希望在这一段时间内只能被自己使用(互斥),如何实现这个功能?
解决方案:不管是谁使用打印机,在使用之前给打印机上锁,用完之后,解锁。
1、线程互斥锁
线程互斥锁解决的问题就是临界资源的独占式使用,当进程中的线程给某一个资源上锁之后,其他的线程想要使用这个资源的时候,必须等待原来的线程解锁之后才能使用这个资源。
如果一条线程想要对一个已经上锁的资源进行上锁,此时线程会进入阻塞,必须等待原来上锁的线程对这个资源解锁之后,才能成功上锁,解除阻塞。
2、相关API
1)互斥锁初始化 pthread_mutex_init()
2)上锁
3)解锁
4)互斥锁销毁
SYNOPSIS
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); //互斥锁初始化
int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex); //互斥锁销毁
==> mutex : 互斥锁变量地址
==> mutexattr : 互斥锁属性变量地址 NULL
返回值: 成功返回0,失败返回错误编码
例子1:
创建两条线程,使用同一个临界资源打印机 (输出缓冲区),线程1输出字符串”hello”,每隔1s打印一个字符,线程2输出字符串”world”, 每隔1s打印1个字符。
没加互斥锁
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void *fun1(void *arg)
{
char *p = "hello";
while(*p)
{
putc(*p,stderr);
p++;
sleep(1);
}
}
void *fun2(void *arg)
{
char *p = "world";
while(*p)
{
putc(*p,stderr);
p++;
sleep(1);
}
}
int main(int argc,char *argv[])
{
//1,创建2条线程分别输出"hello","world"
pthread_t tid[2];
pthread_create(&tid[0],NULL,fun1,NULL);
pthread_create(&tid[1],NULL,fun2,NULL);
//2,等待两条子线程结束
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
return 0;
}
打印结果不是我们想要的
加互斥锁
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
//定义一个全局互斥锁变量
pthread_mutex_t mutex;
void *fun1(void *arg)
{
char *p = "hello";
pthread_mutex_lock(&mutex);
while(*p)
{
putc(*p,stderr);
p++;
sleep(1);
}
pthread_mutex_unlock(&mutex); //用完打印机后
}
void *fun2(void *arg)
{
char *p = "world";
pthread_mutex_lock(&mutex);
while(*p)
{
putc(*p,stderr);
p++;
sleep(1);
}
pthread_mutex_unlock(&mutex);
}
int main(int argc,char *argv[])
{
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//1,创建2条线程分别输出"hello","world"
pthread_t tid[2];
pthread_create(&tid[0],NULL,fun1,NULL);
pthread_create(&tid[1],NULL,fun2,NULL);
//2,等待两条子线程结束
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
return 0;
}
结果正常打印
二、特殊的互斥锁 – 读写锁
问题:在系统中存在成千上万个任务需要对同一个临界资源进行访问,大部分的任务只需要对这个资源进行读取信息(不修改资源内容),如果此时对这个资源进行上锁处理,那么就会导致这个资源只能被某一个任务独占式使用,其他的资源不能访问(陷入阻塞),这样会导致效率降低。
例如:假设博物馆展示一幅画(临界资源),所有去博物馆参观的游客就是系统中的任务,绝大部分是只需要对这个临界资源进行观赏(读取),少部分需要对临界资源进行修改(维修人员),那么这幅画应该可以同时被很多人参观,但是被维修的时候只能被一个人维修。
1、读写锁机制:
读写锁上锁时可以选择读锁或者写锁。
读锁: 读写锁上读锁,其他的任务在对这个读写锁上读锁时不会阻塞,但是上写锁时会阻塞。
写锁:读写锁上写锁时,其他的任务上读锁和写锁都会阻塞。此时写锁相当于互斥锁。
2、相关API:
SYNOPSIS
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); //读写锁销毁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr); //读写锁初始化
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //上读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //上写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解锁
三、线程的同步
1、线程的同步
现在的操作系统大部分都是多用户多任务操作系统,那么在系统中同时存在大量可调度的任务实体(进程)在运行,可能会出现以下情况:
1) 需要同时访问/使用同一个临界变量 (互斥)
2) 多个任务的执行之间存在依赖关系,一个任务运行需要另一个任务运行的结果。(同步)
互斥: 多个任务在使用同一个临界资源时,当一个任务获取到这个资源时,其他所有的任务都不能去访问这个资源,保证这个资源被独占式使用。
同步: 不同的任务按照一个规定的顺序去执行。同步其实是一种高级的互斥。
例如: 进程A : 面粉厂; 进程B:面包厂
在解决互斥问题时,通常使用互斥锁,读写锁。
2、死锁问题
死锁:系统中多个任务在执行过程中因为资源争夺陷入的一种僵局,陷入这种僵局时,如果没有外力的推动下,谁都无法继续往下执行。
例如:进程A执行需要资源a,b; 进程B的执行也需要a,b; 不管是进程A还是进程B只要执行完就会是否a,b资源。
当前系统中的资源只有一个a,一个b, 此时进程A获取资源a, 进程B获取资源b;
死锁产生的四个必要条件:
1)互斥条件 (一个资源在某一时刻只能被一个任务占有)
2)占有等待 (占据一些资源去请求另外的一些资源)
3)非剥夺条件 (已占据的资源不能被其他任务剥夺)
4)环路等待条件
> 资源是有限的!
> 避免产生死锁:破坏任意一个死锁产生的必要条件,死锁就不会产生。
//=========================================================//
(1)互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
预防死锁
预防死锁的方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,以避免发生死锁。
由于互斥条件是非共享设备所必须的,不仅不能改变,还应该加以保证,因此,主要是破坏产生死锁的后三个条件。
破坏请求与保持条件: 对一个进程在请求资源时,它不能持有不可剥夺资源。
破坏不可剥夺条件: 对一个已经保持了某些不可剥夺资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。
破坏循环等待条件: 对系统所有资源类型进行线性排列,并赋予不同序号。规定每个进程必须按序号递增的顺序请求资源。
避免死锁的方法
一次封锁法:每个进程(事务)将所有要使用的数据全部加锁,否则,就不能继续执行;
顺序封锁法:预先对数据对象规定一个封锁顺序,所有进程(事务)都按这个顺序加锁;
银行家算法:保证进程处于安全进程序列。
//=========================================================//
3、线程间同步功能
信号量 – POSIX信号量
POSIX信号量
无名信号量 : 用于线程间同步
有名信号量 : 用于进程间同步
信号量工作原理:
信号量其实就是资源计数器,用来控制公共资源的访问。
PV原语 : P操作,V操作
P操作: 对信号量的值-1,资源数-1.(如果当前信号量的值为0,p操作会然任务陷入阻塞)
V操作: 对信号量的值+1,资源数+1 (V操作在任何情况下都不会阻塞任务)
例子:
假设你是一家自行车租车店的老板,你有5辆自行车可以拿来出租,顾客可以选择租车,或者是还车,如果当前自行车数量为0,那么顾客来租车时,就会陷入阻塞。
信号量的值: 当前的资源数 5
==> 租车 : P操作 (资源数-1)
==> 还车 : V操作 (资源数+1)
==> 顾客 :使用资源的任务,当一条任务因为申请资源P操作陷入阻塞时,此时必须由另外一个任务进行V操作,使资源量+1,才能解除阻塞。
例子2:
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
sem_t sem; //信号量变量
int Bike_Num = 5;
void *time_display(void *arg)
{
for(int i=0; ; i++)
{
printf("time:%d\n", i);
sleep(1);
}
}
/*线程执行函数*/
void *routine(void *arg)
{
//租车
if(Bike_Num == 0)
printf("当前店里没车,请等一下!\n");
sem_wait(&sem);
Bike_Num--;
printf("[%lu]租车成功!\n", pthread_self()); //打印线程ID
sleep(10);
//还车
sem_post(&sem);
Bike_Num++;
printf("[%lu]还车车成功!\n", pthread_self()); //打印线程ID
}
/*无名信号量使用*/
int main(int argc, char *argv[])
{
sem_init(&sem, 0, 5); //信号量初始值设置为5
//主线程循环获取整数
int n;
pthread_t tid, tid1;
pthread_create(&tid1, NULL, time_display, NULL);
while(1) //主线程循环获取整数,创建线程
{
scanf("%d", &n);
pthread_create(&tid, NULL, routine, NULL);
}
return 0;
}
4、相关API
1) sem_init() //信号量初始化
SYNOPSIS
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -pthread.
==> sem : 信号量变量地址
==> pshared : 0 线程之间的共享
==> value : 信号初始值
返回值:成功返回0,失败返回-1
2)sem_wait() //信号量P操作
SYNOPSIS
#include <semaphore.h>
int sem_wait(sem_t *sem);
==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1
3)sem_post() //信号量V操作
SYNOPSIS
#include <semaphore.h>
int sem_post(sem_t *sem);
==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1
4)sem_destroy() //无名信号量的销毁
SYNOPSIS
#include <semaphore.h>
int sem_destroy(sem_t *sem);
==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1
例子: 使用无名信号量模拟租车还车的案例
主线程循环等待输入数据(scanf()), 每次获取到整数,那就创建一条线程去租车,10s之后还车,如果没有车,那就提示没有车,需要等待,如果有车,那就把车租走。
四、有名信号量
POSIX信号量中,无名信号量用于线程间同步,有名信号量用于进程间同步。
1、相关API:
1)sem_open() //有名信号量初始化
SYNOPSIS
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
==> name : 有名信号量路径名 (有名信号量路径必须在 / )
==> oflag : 有名信号的打开权限 O_CREAT (存在则打开,不存在则创建)
==> mode : 有名信号量的文件权限 0777
==> value : 有名信号量的初始值
返回值: 成功返回有名信号量的地址,失败返回 SEM_FAILED
2) sem_wait(), sem_post() //有名型号量PV操作
3)sem_close() //有名信号量关闭
SYNOPSIS
#include <semaphore.h>
int sem_close(sem_t *sem);
4)有名信号量删除 sem_unlink() --> 保证这个信号量必须被sem_close()了
例子:使用有名信号量规范进程A,进程B的同步关系。
进程A每次从键盘输入一串字符串,进程B就打印一句话”get_string”
分析: 进程A, 进程B如何实现同步
信号量1(输入资源), 信号量2(输出资源)
==> 初始状态下,输入资源为1,输出资源为0; (进程A可以运行,进程B不能运行)
==> 进程A输入数据之后,输入资源-1,输出资源+1
==> 进程B输出数据之后,输出资源-1,输入资源+1
假设设置两个信号量 sem_1 = 1; sem_2=0;
进程A 进程B
While(1) while(1)
{ {
P(sem_1); P(sem_2);
//输入数据 //输出数据
V(sem_2); V(sem_1);
} }
练习:参考有名信号量的示例代码,实现以下功能。
使用POSIX有名信号量代替 IPC信号量,实现两个进程对共享内存的同步使用。
进程A循环往共享内存写入数据,进程B输出共享内存的内容。
2、条件变量
在线程中,条件变量的作用是让线程进入睡眠。当某种情况产生的时候,就给线程发送一个唤醒信号,然线程去工作。
条件变量需要跟互斥锁一起使用。
条件变量这个变量本身是一个临界资源,不同的线程在使用的时候需要互斥使用(使用之前上锁)
案例:
小明是一个学生,小明的父母通过一个银行卡给小明转生活费。
小明的父母判断银行卡中的余额,如果余额大于1500,那小明的父母就去上班,不管这个银行卡了,否则小明的父母给银行卡转钱2000。
小明每个月从银行卡中取出1000元作为生活费,如果卡中余额大于1000,那小明就正常的取出金钱,如果卡中余额数量不够1000元,那就打电话通知父母,父母接到信号就去通过银行卡给小明打钱。
==> 条件变量(临界资源) : 银行卡
==> 小明的父母 : 线程1
==> 小明:线程2
例子3:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t cond; //条件变量
pthread_mutex_t mutex; //互斥锁
int Money = 1800; //银行卡初始值
/*打印时间*/
void *time_play(void *arg)
{
for(int i = 0; ; i++)
{
printf("time:%d\n", i);
sleep(1);
}
}
/*小明的父母*/
void *Parent(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); //上锁
if(Money > 1500)
{
printf("[父母]钱还够,我们睡觉了!\n");
pthread_cond_wait(&cond, &mutex); //线程睡眠 --> 阻塞
}
Money += 2000;
printf("[父母]已经转了2000,余额:%d元\n", Money);
pthread_mutex_unlock(&mutex); //解锁
}
}
void *Son(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); //上锁
if(Money > 1000)
{
Money -= 1000;
printf("[小明]取出1000生活费,剩余%d元\n", Money);
}
else{
printf("[小明]没钱了,快打钱啊\n");
pthread_cond_signal(&cond); //唤醒任意一条睡眠的线程
}
pthread_mutex_unlock(&mutex); //解锁
sleep(3); //一个月
}
}
/*条件变量*/
int main(int argc, char *argv[])
{
//1,初始化条件变量,初始化互斥锁
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
//2,创建2条线程
pthread_t tid[3];
pthread_create(&tid[0], NULL, time_play, NULL);
pthread_create(&tid[1], NULL, Parent, NULL); //小明父母
pthread_create(&tid[2], NULL, Son, NULL); //小明
//3,主线程不退出
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_join(tid[2], NULL);
//4,销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
2,相关API
1)pthread_cond_init() //条件变量初始化
SYNOPSIS
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//条件变量初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//给任意一个被条件变量睡眠的线程发送唤醒信号
int pthread_cond_signal(pthread_cond_t *cond);
//给所有的被条件变量睡眠的线程发送唤醒信号
int pthread_cond_broadcast(pthread_cond_t *cond);
//线程通过条件变量进入睡眠 --> mutex (睡眠的时候会释放锁资源)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
例子4:
进程A
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <signal.h>
#include <stdlib.h>
#define SEM_NAME1 "/sem1"
#define SEM_NAME2 "/sem2"
sem_t *sem_1, *sem_2;
/**********************
函数:sem_quit()
原因:当信号量程序运行后,如果直接CTRL+C关闭程序,下次再运行时
程序无法正常工作,因为上一次代码运行时,信号量进程已经在后台运
行,但没有正常关闭,所以下次运行会出现错误。
通过signal()信号函数来对信号量正常关闭。
SIGINT:为ctrl+c键
**********************/
void Sem_Quit(int arg) //退出处理功能函数
{
//3, 删除有名信号量
sem_close(sem_1);
sem_close(sem_2);
sem_unlink(SEM_NAME1);
sem_unlink(SEM_NAME2);
exit(0);
}
/*输入数据*/
int main(int argc, char *argv[])
{
signal(SIGINT, Sem_Quit);
//1, 获取有名信号量 信号量1,信号量2
sem_1 = sem_open(SEM_NAME1, O_CREAT, 0777, 1);
sem_2 = sem_open(SEM_NAME2, O_CREAT, 0777, 0);
if(sem_1 == SEM_FAILED || sem_2 == SEM_FAILED)
{
perror("sem_open failed");
return -1;
}
//2, 循环输入数据
char buf[32] = {0};
while(1)
{
sem_wait(sem_1); //信号量1 P操作
fgets(buf, sizeof(buf), stdin);
printf("fgets:%s", buf);
sem_post(sem_2); //信号量1 V操作
}
return 0;
}
进程B
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <signal.h>
#include <stdlib.h>
#define SEM_NAME1 "/sem1"
#define SEM_NAME2 "/sem2"
sem_t *sem_1, *sem_2;
void Sem_Quit(int arg) //退出处理功能函数
{
//3, 删除有名信号量
sem_close(sem_1);
sem_close(sem_2);
sem_unlink(SEM_NAME1);
sem_unlink(SEM_NAME2);
exit(0);
}
/*输入数据*/
int main(int argc, char *argv[])
{
signal(SIGINT, Sem_Quit);
//1, 获取有名信号量 信号量1,信号量2
sem_1 = sem_open(SEM_NAME1, O_CREAT, 0777, 1);
sem_2 = sem_open(SEM_NAME2, O_CREAT, 0777, 0);
if(sem_1 == SEM_FAILED || sem_2 == SEM_FAILED)
{
perror("sem_open failed");
return -1;
}
//2, 循环输入数据
while(1)
{
sem_wait(sem_2); //信号量2 P操作
printf("get string!\n");
sem_post(sem_1); //信号量1 V操作
}
return 0;
}