线程安全与重入
线程安全
定义:多个执行流对同一个临界资源进行争抢访问,但不会造成数据二义或逻辑混乱。
临界区:每个线程内部,访问临界资源的代码。
临界资源:多线程执行流共享的资源就叫做临界资源。
实现:同步与互斥
同步:通过同一时间只有一个线程能够访问临界资源来保证操作安全性
互斥 :通过条件判断实现对临界资源访问的时序合理性
- 大部分情况下,线程使用数据的都是局部变量,变量的地址空间在线程栈空间内,这种情况下,变量归属于单个线程,其他线程无法获得这个变量。
- 但有时,很多变量需要在线程间共享,这种变量叫做共享变量,通过数据的共享,完成线程之间的交互。而多个线程并发操作共享变量会带来一些问题。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define MAX_THR 4
int ticket=100;//全局变量
void *thr_start(void *argv){
while(1){
if(ticket>0){
printf("%p get a ticket:%d\n",pthread_self(),ticket);
usleep(1000);
ticket--;
}
else{
printf("%pdon't have ticket\n",pthread_self());
pthread_exit(NULL);
}
}
return NULL;
}
int main(){
pthread_t tid[MAX_THR];
int i;
for(i=0;i<MAX_THR;i++){
int ret=pthread_create(&tid[i],NULL,thr_start,NULL);
if(ret!=0){
perror("pthread create error\n");
return -1;
}
}
for(i=0;i<MAX_THR;i++){
pthread_join(tid[i],NULL);
}
return 0;
}
这个例子中造成的数据二义说明了线程安全的重要性!
可重入
线程互斥
互斥锁
实现原理:
相当于一个只有0/1的计数器,用于标记当前临界资源的访问状态;在对临界资源进行访问之前都需要先加锁访问这个状态计数,若可以进行访问的话则修改计数(这个操作本身是原子操作)。
接口
创建锁:pthread_mutex_t mutex;
初始化锁:pthread_mutex_init ( pthread_mutex_t *mutex , const pthread_mutexattr_t *attr(锁属性,常NULL) );
加锁:pthread_mutex_lock ( pthread_mutex_t *mutex );(加锁后需要在任意地方可能退出线程的地方解锁)
解锁:pthread_mutex_unlock ( pthread_mutex_t *mutex );
加锁、解锁的返回值:成功返回0;失败返回错误号
销毁锁:pthread_mutex_destory ( pthread_mutex_t *mutex );(不要销毁没有解锁的锁)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define MAX_THR 4
int ticket=100;
pthread_mutex_t mutex;
void *thr_start(void *argv){
while(1){
pthread_mutex_lock(&mutex);
if(ticket>0){
printf("%p get a ticket:%d\n",pthread_self(),ticket);
usleep(1000);
ticket--;
pthread_mutex_unlock(&mutex);
}
else{
printf("%pdon't have ticket\n",pthread_self());
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
}
return NULL;
}
int main(){
pthread_t tid[MAX_THR];
pthread_mutex_init(&mutex,NULL);
int i;
for(i=0;i<MAX_THR;i++){
int ret=pthread_create(&tid[i],NULL,thr_start,NULL);
if(ret!=0){
perror("pthread create error\n");
return -1;
}
}
for(i=0;i<MAX_THR;i++){
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
死锁
产生原因
多个线程对所资源进行争抢访问,因为推进顺序不当而导致互相等待,造成程序流程无法推进。
死锁的四个必要条件
- 互斥条件
- 不可剥脱条件
- 请求与保持条件
- 循环等待条件
避免
破坏条件(非阻塞加锁)
银行家算法
线程同步
条件变量
实现原理:
向外提供了一个线程等待(等待队列)(将PCB状态更改为可中断休眠) 以及 唤醒((将PCB状态更改为运行状态)的功能接口。
接口
创建条件变量:pthread_cond_t cond;
初始化条件变量:pthread_cond_init ( pthread_cond_t *cond , const pthread_condattr_t attr(条件变量的属性,常置NULL) );
等待:int pthread_cond_wait( pthread_cond_t *cond , pthread_mutex_t *mutex );
唤醒:int pthread_cond_signal( pthread_cond_t *cond ); (至少唤醒一个等待队列上的等待的线程)
int pthread_cond_broadcast( pthread_cond_t *cond );(广播唤醒所有等待队列上的线程)
销毁条件变量:int pthread_cond_destory( pthread_cond_t *cond );
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define MAX_THR 4
int noodle=0;
pthread_mutex_t mutex;
pthread_cond_t cond_foodie;
pthread_cond_t cond_chef;
void *foodie(void *argv){
while(1){
pthread_mutex_lock(&mutex);
while(noodle==0){
//pthread_mutex_unlock(&mutex);
//pause()
//pthread_mutex_lock(&mutex);//这三步用pthread_cond_wait替换,形成原子操作,避免时间片切换带来的问题
pthread_cond_wait(&cond_foodie,&mutex);
}
printf("%p:the noodles are delicious~\n",pthread_self());
noodle--;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_chef);
}
return NULL;
}
void *chef(void *argv){
while(1){
pthread_mutex_lock(&mutex);
while(noodle==1){
pthread_cond_wait(&cond_chef,&mutex);
}
printf("make a bowl of noodle~\n");
noodle++;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_foodie);
sleep(1);
}
return NULL;
}
int main(){
pthread_t foodietid[MAX_THR],cheftid;
int ret,i;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond_foodie,NULL);
pthread_cond_init(&cond_chef,NULL);
for(i=0;i<MAX_THR;i++){
ret=pthread_create(&foodietid[i],NULL,foodie,NULL);
if(ret!=0){
perror("pthread create error");
}
}
ret=pthread_create(&cheftid,NULL,chef,NULL);
if(ret!=0){
perror("pthread create error");
}
for(i=0;i<MAX_THR;i++){
pthread_join(foodietid[i],NULL);
}
pthread_join(cheftid,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_foodie);
pthread_cond_destroy(&cond_chef);
return 0;
}
注意事项
- 条件变量需要搭配互斥锁一起使用
- 对条件判断要使用while循环语句
- 不同的角色要使用不同的条件变量进行等待与唤醒,不同的角色不能等待在同一个人队列
POSIX信号量
用于实现线程间的同步
本质:是一个计数器,并向用户提供使线程等待与唤醒的接口。
同步与互斥的实现:
- 同步:通过自身计数器进行资源计数,对临界资源访问之前先访问信号量,通过计数判断是否有资源可以访问。计数<=0则表示不能访问,计数-1后开始等待;计数>0则表示可以访问,计数-1后直接访问。其他促使条件满足的线程访问临界资源之前也先访问信号量,若计数>0,访问后计数+1;若计数<0,则唤醒一个的等待队列上的线程并计数+1。
- 互斥:只需要将计数器维护在0/1之间就可以实现互斥。
接口:
//初始化信号量
#include<semaphore>
int sem_init(sem_t *sem , int pshared , unsigned int value);
参数:
pshared:0表示用于线程间共享,非零表示用于进程间共享
value:信号量初始值
//等待信号量,会将信号量的值-1
int sem_wait(sem_t *sem);//条件判断+等待功能,若计数<=0,则阻塞;否则-1后立即返回
//发布信号量,表示资源使用完毕,可以归还资源,将信号量值+1
int sem_t post(sem_t *sem);
//销毁信号量
int sem_destroy(sem_t *sem);
//信号量实现同步
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<pthread.h>
sem_t sem;
void *eat(void *arv){
while(1){
sem_wait(&sem);
printf("**eat a bool of noodle\n");
}
return NULL;
}
void *cook(void *arv){
while(1){
printf("$$cook a bool of noodle\n");
sleep(1);
sem_post(&sem);
}
return NULL;
}
int main(){
int ret,i;
pthread_t tid1,tid2;
sem_init(&sem,0,0);
ret=pthread_create(&tid1,NULL,eat,NULL);
if(ret!=0){
perror("pthead create error");
return -1;
}
ret=pthread_create(&tid2,NULL,cook,NULL);
if(ret!=0){
perror("pthead create error");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
sem_destroy(&sem);
return 0;
}
//信号量实现互斥
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<pthread.h>
sem_t sem;
int t=100;
void *cattle(void *arv){
while(1){
sem_wait(&sem);
if(t>0){
sleep(1);
printf("%pi get a ticket:%d\n",pthread_self(),t--);
sem_post(&sem);
}
else{
sem_post(&sem);
pthread_exit(NULL);
}
}
return NULL;
}
int main(){
int ret;
pthread_t tid1,tid2;
sem_init(&sem,0,1);
ret=pthread_create(&tid1,NULL,cattle,NULL);
if(ret!=0){
perror("pthead create error");
return -1;
}
ret=pthread_create(&tid2,NULL,cattle,NULL);
if(ret!=0){
perror("pthead create error");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
sem_destroy(&sem);
return 0;
}