概念:线程安区描述的是在线程中对于一个临界资源的操作访问是否安全的。
如何实现线程安区:同步与互斥。
线程互斥:指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性
如何实现:互斥锁。
互斥锁---->本质上是一个只有0/1的计数器,标记临界资源的两种访问状态,在访问之前加锁,这样别的线程再要访问时会先判断发现不满足访问条件则阻塞,访问完资源之后进行解锁。
需要注意的是:互斥锁本身就是一种临界资源,需要多个线程共同访问一个互斥锁才能实现互斥,而互斥锁本身的计数操作应该为原子操作。
互斥锁的实现流程和接口
pthread_mutex_t mutex; //定义一个互斥锁mutex
#include
int pthread_mutex_init(pthread_mutex_t * mutex,
const pthread_mutexattr_t *attr);
参数:mutex为互斥锁,attr传NULL即可。
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_lock();阻塞加锁,锁被别人加了自己则会一直等待直到能加锁。
pthread_mutex_trylock();非阻塞加锁,如果已经被加锁了则会报错。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意事项:
死锁指的是程序流程无法继续运行,卡死的状态。
死锁是如何产生的呢?有四个必要条件:
以上这四个条件加起来就会产生死锁。
那么该如何预防死锁的产生? 只要破坏上面的四个必要条件即可,但是1和2条件是锁自带的属性无法破坏,可以考虑破坏3或4条件。
火车站有1000张票;有四个黄牛党抢票–有票则抢,没票退出(对抢票的过程加锁,同一时间只能有一个黄牛抢到票)
#include
#include
#include
#include
int tickets=1000;
pthread_mutex_t mutex; //定义互斥锁(公共资源),都能访问到
void* scalpers(void *arg ){
//进程入口函数
while(1){
pthread_mutex_lock(&mutex); //抢票前加锁,保证抢票过程中不会有别的黄牛来抢票(原子操作)
if(tickets>0){
usleep(5);
tickets--;
printf("抢到一张票%p,剩余总票数为:%d\n",pthread_self(),tickets);
pthread_mutex_unlock(&mutex);//解锁
}
else{
pthread_mutex_unlock(&mutex); //可能退出的地方都要解锁
printf("票已经无了!\n");
pthread_exit(NULL); //无票则退出线程。
}
usleep(10);
}
return NULL;
}
int main()
{
int ret ;
pthread_t tid[4];
pthread_mutex_init(&mutex,NULL); //初始化互斥锁
for(int i=0;i<4;i++){
//创建四个黄牛进程,进程入口函数(抢票)相同
ret = pthread_create(&tid[i],NULL,scalpers,NULL);
if(ret!=0){
perror("pthread_creat error\n");
return -1;
}
}
for(int i=0;i<4;i++){
//线程等待
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex); //摧毁互斥锁
return 0;
}
通过条件变量可实现进程的同步,条件变量提供了一个pcb等待队列(临界资源,需要加锁保护),和使线程阻塞和唤醒的函数接口。
程序员在程序中进行判断当前进程获取资源是否合理,不合理则调用阻塞接口使线程阻塞(加入pcb等待队列)。其他线程当条件满足之后调用唤醒函数唤醒阻塞的线程。
条件变量一般是和互斥锁一起使用的。
操作流程及接口:
pthread_cond_t cond; //定义一个名为cond的条件变量
法一(记不住,不推荐):
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
法二:调用函数
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数:
restrict cond:定义的条件变量
restrict attr:条件变量的属性,传NULL即可。
法一:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数:
restrict cond:定义的条件变量
restrict mutex:互斥锁
功能:永久阻塞线程,直到被唤醒。
这里的阻塞操作可分解为{解锁,休眠}和被唤醒后加锁这三个操作,这个函数保证了{解锁,休眠}这两个操作为原子操作。举个反例:线程A运行到解锁这里还未休眠被线程B的操作打断,线程B操作了一通之后唤醒线程A,但是线程A不在pcb等待队列,时间片切换到线程A执行下一步操作休眠,休眠之后,不会再有线程B再次唤醒线程A,线程A永久休眠。
法二:
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参数restrict abstime:时间
功能:阻塞线程一段时间,这段时间内若线程还未被唤醒,则报错返回。
法一:
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有被阻塞的线程
法二:
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒**至少一个(也可能多个)**被阻塞的线程
int pthread_cond_destroy(pthread_cond_t *cond);
注意事项:
通过信号量也可实现同步和互斥。
信号量本质:计数器+ pcb等待队列
作用:实现进程或线程间的同步和互斥。
操作:
P操作:计数-1,判读计数是否大于1,不大于则阻塞进程或线程
V操作:计数+1,唤醒一个阻塞的线程或进程。
实现同步:通过计数器对资源进行计数。获取资源前先访问信号量进行P操作,产生资源之后进行V操作。
互斥的实现:访问资源前进行P操作,访问后V操作.
使用流程及接口:
sem_t sem; //定义一个信号量,后面传参用
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:信号量
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值。(有多少资源设置为多少)
#include
int sem_wait(sem_t *sem);
功能:等待信号量,会将信号量的值减1,小于0则阻塞。
int sem_trywait(sem_t *sem);
功能: 非阻塞等待信号量,信号量的值减1。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:限制等待时长的阻塞塞等待信号量,信号量的值减1。
int sem_post(sem_t *sem);
功能:发送信号,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_destroy(sem_t *sem);
这是一种典型的设计模式,应用于大量数据的产生和处理的场景。
分析:生产者与消费者就是两种角色的线程,需要将产生的数据存入到一个缓冲区,且对这块缓冲区的存入和取数据必须为安全的操作–>原子操作(同步和互斥实现),针对这块缓冲区的数据操作应满足先入先出(队列)。故需要设计一个队列,对这个队列的存入和取数据进行加锁保护(互斥),并判断存入和取出数据是否合理(同步),不合理则阻塞并唤醒另一个进程进行操作。
代码:
#include
#include
#include
#include
#include
#define MAX_QUEUE 10 //队列最大容量
class BlockQueue{
public:
BlockQueue(int que_capacity=MAX_QUEUE):_capacity(que_capacity)
{
pthread_mutex_init(&_mutex,NULL);
pthread_cond_init(&_cond_p,NULL);
pthread_cond_init(&_cond_c,NULL);
}
~BlockQueue(){
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_p);
pthread_cond_destroy(&_cond_c);
}
bool push(const int& data){
pthread_mutex_lock(&_mutex); //加锁
if(_queue.size()==_capacity){
pthread_cond_wait(&_cond_p,&_mutex);//队列已满则等待条件满足再放数据到队列
}
_queue.push(data);
printf("productor %p push data: %d queue-size: %d \n",pthread_self(),data,_queue.size());
//这里打印一下线程ID和队列大小,方便查看结果。
pthread_cond_signal(&_cond_c); // 唤醒
pthread_mutex_unlock(&_mutex); //解锁
return true;
}
bool pop(int *data){
pthread_mutex_lock(&_mutex);
if(_queue.empty()){
pthread_cond_wait(&_cond_c,&_mutex);//无数据则等待
}
*data = _queue.front();
_queue.pop();
printf("customer %p pop data: %d queue-size: %d :\n",pthread_self(),*data,_queue.size());
pthread_cond_signal(&_cond_p);
pthread_mutex_unlock(&_mutex);
return true;
}
private:
int _capacity;
std::queue<int> _queue;
pthread_cond_t _cond_p; //生产者条件变量
pthread_cond_t _cond_c; //消费者条件变量
pthread_mutex_t _mutex; //互斥锁
};
void* productor(void* arg){
BlockQueue *q = (BlockQueue*)arg;
int data=1;
while(1){
q->push(data++);
usleep(1000000);
}
return NULL;
}
void* customer(void* arg){
BlockQueue *q = (BlockQueue*)arg;
while(1){
int data=0;//获取pop的数据
q->pop(&data);
usleep(1000000);
}
return NULL;
}
int main ()
{
BlockQueue q;
int ret ;
pthread_t ptid[4],ctid[4];
for(int i=0;i<4;i++){
//这里创建了四个priductor线程
ret=pthread_create(&ptid[i],NULL,productor,&q);
if(ret!=0){
perror("pthread_create error\n");
return -1;
}
}
for(int i=0;i<4;i++){
//创建四个costomer线程
ret=pthread_create(&ctid[i],NULL,customer,&q);
if(ret!=0){
perror("pthread_create error\n");
return -1;
}
}
for(int i=0;i<4;i++){
pthread_join(ctid[i],NULL);
pthread_join(ptid[i],NULL);
}
return 0;
}
分析:
队列使用环形队列(线程安区的)。
还应该使用两个信号量:
一个是对空闲节点计数---->生产者放数据。
对于生产者来说空闲节点是资源。
操作流程:
另一个是对数据节点进行计数-> 消费者取数据。
对于消费者来说数据是资源。
操作流程:
这是应该定义的成员变量:
Class RingQueue
{
int _capacity;//容量
int _step_read;//读指针
int _step_write;//写指针
std::vector=tor<int> _arry;
sem_t _sem_data;//数据节点计数
sem_t _sem_idle;//空闲节点计数
sem_t _sem_lock;//用于实现互斥
}
代码实现:
#include
#include
#include
#include
#include
#include
#define MAX_QUEUE 5
class RingQueue
{
public:
RingQueue(int cap = MAX_QUEUE):
_capacity(cap) , _step_read(0),_step_write(0),_arry(cap)
{
sem_init(&_sem_data,0,0);
sem_init(&_sem_idle,0, cap);
sem_init(&_sem_lock,0 ,1);//用于实现互斥,初始信号量为1
}
~RingQueue(){
sem_destroy(&_sem_data);
sem_destroy(&_sem_lock);
sem_destroy(&_sem_idle);
}
bool Push(const int &data){
sem_wait(&_sem_idle); //对空闲节点P操作,空闲节点-1, 小于0则阻塞
sem_wait(&_sem_lock);//加锁
//放数据
_arry[_step_write] =data;
_step_write=(_step_write+1)% _capacity;//写指针加一取模
sem_post(&_sem_lock); //放完数据解锁,唤醒一个消费者
sem_post(&_sem_data); //对数据节点V操作,数据节点+1
return true;
}
bool Pop(int *buf){
sem_wait(&_sem_data); //数据节点 -1
sem_wait(&_sem_lock); //加锁
*buf= _arry[_step_read];//取数据
_step_read =(_step_read+1)%_capacity;//读指针+1取模
sem_post(&_sem_lock);//解锁
sem_post(&_sem_idle);//空闲节点+1
return true;
}
private:
int _capacity;//容量
int _step_read;//读指针
int _step_write;//写指针
std::vector<int> _arry;
sem_t _sem_data;//数据节点计数
sem_t _sem_idle;//空闲节点计数
sem_t _sem_lock;//用于实现互斥
};
void* productor(void *arg)
{
RingQueue*q =(RingQueue*)arg;
int data =0;
while(1){
q->Push(data);
printf("push data : %d \n",data++);
sleep(1);
}
return NULL;
}
void* customer(void *arg)
{
RingQueue*q =(RingQueue*)arg;
int data =0;
while(1){
q->Pop(&data);
printf("pop data : %d \n", data);
sleep(1);
}
return NULL;
}
int main ()
{
RingQueue q ;
int ret;
pthread_t tid_p[4],tid_c[4];
for(int i=0;i<4;++i){
ret = pthread_create(&tid_p[i],NULL,productor,&q);
if(ret !=0){
perror("thread error\n");
}
}
for(int i=0;i<4;++i){
ret = pthread_create(&tid_c[i],NULL,customer,&q);
if(ret !=0){
perror("thread error\n");
}
}
for(int i=0;i<4;++i){
pthread_join(tid_c[i],NULL);
pthread_join(tid_p[i],NULL);
}
return 0;
}
如图结果可能会出现连续pop或者push数据,原因是线程入口函数void* productor(void arg)和void customer(void *arg)函数里的和Pop或者push操作和之后的打印操作并非原子操作,可能出现push数据之后还没打印就切换到另一个线程执行pop操作了,下一次push操作时就会多打印一次push data。
通过两种不同的生产者与消费者模型的实现可以看出条件变量和互斥锁的区别:
为保证并发情况下线程对临界资源的合理安区的访问,除了互斥锁还有很多其他的锁:悲观锁(PCC),乐观锁(CAS锁),可重入锁和不可重入锁,读写锁等;下一篇将介绍这几种锁和实现。