线程安全:
因为进程中的线程共享了进程的虚拟地址空间,因此线程间通信将变得更加简单,但是缺点也随之而来:缺乏数据的访问控制容易造成数据混乱(因为大家都在争抢访问公共资源)。我们把能够造成数据混乱的情况总结了两个比较经典的模型,他们都是描述了多个进程/线程之间在数据访问时所应该保持的关系,不至于造成数据混乱和逻辑混乱。
//这是一个火车站黄牛买票的例子
//每一个黄牛都是一个线程,在这个例子中,有一个总票数ticket,一个黄牛抢到票,ticket-1
//直至ticket票数为0
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
int ticket=100;
void* route(void* arg)
{
int tid=(int)arg;
while(1)
{
if(ticket>0)
{
usleep(1000);//当这个线程休眠时,可能会有其他线程进入该代码段
printf("%dbuy tickets:%d\n",tid,ticket);
ticket--;
}
else
{
printf("no tickets\n");
pthread_exit(NULL);//线程退出
}
}
return NULL;
}
int main()
{
pthread_t tid1,tid2,tid3,tid4;
//创建线程
pthread_create(&tid1,NULL,route,(void *)1);
pthread_create(&tid2,NULL,route,(void *)2);
pthread_create(&tid3,NULL,route,(void *)3);
pthread_create(&tid4,NULL,route,(void *)4);
//线程释放资源
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
return 0;
}
从运行结果看有的黄牛还买到-1张票,显然不对,那是为什么呢?
1.if 语句判断条件为真后,代码可以并发的切换到其他进程;
2.usleep这个模拟漫长业务的过程,在这个漫长的业务过程总,可能会有多个线程进入该代码段;
3.ticket–不是原子操作。
要解决以上问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区后,不允许有其他线程进入该临界区;
2.如果多个线程同时要求执行临界区代码,并且临界区没有线程在执行,那么只能允许一个线程进入临界区;
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入该临界区。
要做到这三点,本质上是一把锁。linux上把这把锁称为互斥量。
如何实现线程间的互斥?
互斥锁:
定义一个互斥锁(即定义变量)
pthread_mutex_t 是类型;
初始化互斥锁
互斥锁初始化有两种方式:
a: 定义时直接赋值初始化,最后不需要手动释放
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //PTHREAD_MUTEX_INITIALIZER是宏
b: 函数接口初始化,最后需要手动释放(需要在线程创建之前)
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
功能:互斥锁初始化
mutex :互斥锁变量
attr: 互斥锁的属性 ,NULL;
返回值:成功 :0 失败 :错误码
对临界操作进行加锁/解锁
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:阻塞加锁,如果获取不到锁阻塞,等待锁被解开(计数器变为1)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:非阻塞加锁,如果获取不到则立即报错返回EBUSY
int pthread_mutex_timedlock (pthread_mutux_t *mutex,struct timespec *t)
功能:限时阻塞加锁,如果获取不到锁则等待指定时间,在这段时间内如果一直获取不到,则报错返回,否则加锁
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
在加锁后任意有可能退出的地方都要进行解锁,否则会导致其他线程阻塞卡死
释放(销毁)互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
那么由互斥锁可以将代码改为:
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
int ticket=100;
pthread_mutex_t mutex; //定义互斥锁,线程都能访问到
void *y_cow(void *arg)
{
int tid=(int )arg;
while(1)
{
//加锁
// int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock(&mutex);
if(ticket>0)
{
usleep(100);
printf("y_cow:%d get a ticket:%d\n",tid,ticket);
ticket--;
}
else
{
// int pthread_mutex_unlock(pthread_mutex_t *mutex);
printf("no tickets\n");
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);//需要在这里解锁,否则循环后由于没有解锁而阻塞等待
}
return NULL;
}
int main()
{
// int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_t tid[4];
int i=0;
int ret=-1;
for(i=0;i<4;i++)
{
ret=pthread_create(&tid[i],NULL,y_cow,(void*)i);
if(ret!=0)
{
perror("pthread_create error");
return -1;
}
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
return 0;
}
对互斥锁进行操作时,有加锁一定要有解锁,并且必须在任意一个有可能会退出的地方都要进行解锁操作,否则会造成其他线程的锁死。
死锁情况:因为一直获取不到资源而造成的锁死情况
死锁产生的必要条件:必须具备条件才能满足
1.互斥条件:我获取了你就不能获取
2.不可剥夺条件:我拿到的锁,别人不能释放我的锁,只能我释放
3.请求与保持条件 :拿了锁1又去获取锁2,没有获取到锁2不释放锁1
4.环路等待条件 :a拿了锁 1去请求锁2(没有获取锁2不释放锁1),b拿了锁2去请求锁1(没有获取锁1不释放2),a和b都在等待
预防产生死锁:破坏死锁产生的必要条件
避免产生死锁:银行家算法:
定义两个状态:安全状态
非安全状态
锁编号:大家都按这个编号来获取锁,以及释放
如何实现线程间的同步?
条件变量、posix信号量
条件变量:
1.定义初始化变量
条件变量初始化两种方式
a: 定义时直接赋值初始化,最后不需要手动释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
PTHREAD_COND_INITIALIZER是宏,
b: 函数接口初始化,最后需要手动释放(需要在线程创建之前)
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:cond 要初始化的条件变量;
attr: NULL;
2.销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
3.等待:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:cond 要在这个条件变量上等待
mutex: 互斥量,条件变量和互斥锁搭配使用
4.唤醒等待:通知线程有资源
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
唤醒第一个等待在条件变量上线程
功能:睡眠等到唤醒
为什么pthread_ cond_ wait 需要互斥量?
因为等待需要被唤醒,而被唤醒的前提条件是条件满足
1.条件等待是线程间同步的一种手段,如果只有⼀个线程,条件不满足,⼀直等下去都不会满⾜,所以必须要有⼀一个线程通过某些操作,改变共享变量,使原先不满⾜足的条件变得满⾜足,并且友好的通 知等待在条件变量上的线程。
2.条件不会⽆无缘⽆无故的突然变得满⾜足了,必然会牵扯到共享数据的变化。所以⼀一定要⽤用互斥锁来保 护。没有互斥锁就⽆无法安全的获取和修改共享数据。
3.按照上⾯面的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变量 上,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false)
{
pthread_mutex_unlock(&mutex); //解锁之后,等待之前,条件可能已经满⾜足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
条件变量使用规范:
等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
posix标准信号量
posix标准信号量:既可以实现同步,也可以实现互斥
posix既可以用于进程间同步与互斥 ,也可以用于线程间同步与互斥。
信号量:具有一个等到队列的计数器
同步实现:
消费者:没有资源则等待
生产者:生产出资源则通知等待等待队列的等待者
下面为信号量实现线程间同步与互斥:
1.同步:等待与通知
信号量的初始化
信号量的操作(等待与通知)
信号量的释放
信号量的初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -lrt or -pthread.
sem :信号量变量
pshared: 0代表线程间,非0代表进程间
value: 信号量的初始计数
信号量等待:
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:阻塞等待,没有资源一直等待有资源;
int sem_trywait(sem_t *sem);
功能:非阻塞等待,没有资源则报错返回,否则获取资源
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:限时等待,没有资源则等待指定时间,这段时间内有资源则获取,超时没有资源报错返回。
Link with -lrt or -pthread.
信号量通知:
#include <semaphore.h>
int sem_post(sem_t *sem);
Link with -lrt or -pthread.
//信号量实现线程的同步
//
include<stdio.h>
#include<pthread.h>
#include<error.h>
#include<semaphore.h>
sem_t sem; //全局信号量
void* thr_producer(void* arg)
{
while(1)
{
sleep(1);
printf("product one\n");
sem_post(&sem);
}
return NULL;
}
void* thr_consumer(void *arg)
{
while(1)
{
sem_wait(&sem); //没有资源则等待,判断是信号量内部自己判断
//不需要手动修改计数器,是自己内部修改资源计数,这个内部资源计 //数就是条件,而条件变量修改的是外部条件,需要用户自己修改
printf("buy one\n");
}
return NULL;
}
int main()
{
pthread_t tid1,tid2;
// int sem_init(sem_t *sem, int pshared, unsigned int value);
// pshared : 0代表线程间,非0代表进程间;value代表信号量初值
sem_init(&sem,0,0);//在创建线程前
pthread_create(&tid1,NULL,thr_producer,NULL); //没有判断是否创建成功
pthread_create(&tid2,NULL,thr_consumer,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);//等待线程退出
sem_destroy(&sem); //销毁信号量
return 0; //如果没有pthread_join,return后进程退出,那么所有线程也就会退出,
}
信号量在线程间的互斥
计数器最大是1,也就代表只有一个线程可以获取到信号量,保证了同一时间只有一个线程能操作;
操作完毕后,对计数器+1,这时候信号量资源计数就可以获取,然后又进入新一轮资源争抢。
//信号量实现线程间互斥
#include<stdio.h>
#include<error.h>
#include<pthread.h>
#include<semaphore.h>
sem_t sem;
int tickets=100;
void* thr_buy(void *arg)
{
int i=(int)arg;
while(1)
{
usleep(1000);
sem_wait(&sem);//等待
if(tickets>0)
{
printf("%d buy tickets:%d\n",i,tickets);
tickets--;
}
sem_post(&sem);//通知
}
}
int main()
{
pthread_t tid1,tid2,tid3;
sem_init(&sem,0,1);
pthread_create(&tid1,NULL,thr_buy,(void *)1);
pthread_create(&tid2,NULL,thr_buy,(void *)2);
pthread_create(&tid3,NULL,thr_buy,(void *)3);
pthread_join(tid1,NULL);
sem_destroy(&sem);
return 0;
}
读写者模型:
少量写
大量读
写的时候不能读,读的时候不能写,一个人写的时候其他人不能写,一个人读的时候其他人也可以读;
读和写互斥(写优先:因为读的时候不能写,如果读的多的话,就可能一直没法写,因此写优先指:一旦要开始写,会等待当前正在读的人读完,但是如果有新的想读则不可以读,直至写完);写和写互斥;读和读没有关系。写独占,读共享,写锁优先级高。
如何实现读写者安全数据访问:读写锁。
读写锁初始化和销毁:
#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_wrlock(pthread_rwlock_t *rwlock);
加读锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
解锁: int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//1.读写锁的初始化
//2.读写锁的操作(加读锁,加写锁,解锁)
//3.读写锁的释放
//特性:写独占,读共享,写优先级高
#include<stdio.h>
#include<error.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
pthread_rwlock_t rwlock;
int ticket=100;
void *thr_write(void *arg)
{
while(1)
{
//加写锁
// int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock(&rwlock); //写独占
// int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
if(ticket>0)
//pthread_rwlock_rdlock(&rwlock); //读共享
if(ticket>0)
{
usleep(1000);
ticket--;
printf("ticket:%d\n",ticket);
}
pthread_rwlock_unlock(&rwlock);
}
}
int main()
{
pthread_t wtid[4],rtid[4];
int ret,i;
//int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
//const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_init(&rwlock,NULL);
for(i=0;i<4;i++)
{
ret=pthread_create(&wtid[i],NULL,thr_write,NULL);
if(ret!=0)
{
printf("pthread_cteate error\n");
return -1;
}
}
pthread_join(wtid[0],NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
下面案例将用读锁和写锁:验证多个线程写和多个线程读 的特性。
//5个读线程,3个写线程
//
#include<stdio.h>
#include<pthread.h>
#include<error.h>
int counter;
pthread_rwlock_t rwlock;
void* thr_write(void *arg)
{
int i=(int)arg;
int t;
while(1)
{
usleep(1000);
pthread_rwlock_wrlock(&rwlock); //写锁
t=counter;
printf("write:%d:%#X:counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);
pthread_rwlock_unlock(&rwlock); //写完之后就解锁
usleep(4000);
}
}
void* thr_read(void *arg)
{
int i=(int)arg;
while(1)
{
pthread_rwlock_rdlock(&rwlock); //读锁
printf("read:%d:%#X:count=%d\n",i,pthread_self,counter);
pthread_rwlock_unlock(&rwlock);
usleep(900);
}
}
int main()
{
pthread_t tid[8];
int i=0;
pthread_rwlock_init(&rwlock,NULL); //初始化读写锁
for(i=0;i<3;i++)
{
pthread_create(&tid[i],NULL,thr_write,(void*)i);
}
for(i=3;i<8;i++)
{
pthread_create(&tid[i],NULL,thr_read,(void*)i);
}
for(i=0;i<8;i++)
{
pthread_join(tid[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
上面案例验证只有当一个线程写完后,另一个线程才可以写,并且多个线程可以一起读,而且是写优先。
自旋锁
自旋锁一直轮询判断,这种做法十分 消耗cpu资源,因此适用于确定等待时间花费比较少,很快就能获取到锁的情况。
互斥锁是一直挂起等待。