Linux互斥锁,条件变量,POSIX信号量

互斥

概念:任何时刻,都保证一次只有一个执行流进入临界区,访问临界资源。

要保证一次只有一个执行流进入临界区,而且不能被其他线程干扰,要做到就需要有一个锁。在Linux下,把这把锁叫做互斥量

在这里插入图片描述
在进入临界区之前该线程申请这个锁,然后该进程进入临界区访问临界资源,在访问完后再解锁,别的线程再进行重新申请锁。

互斥量的接口
互斥量的函数的头文件都是#include <pthread.h>,gcc/g++编译的时候都要加上-lpthread

初始化互斥量

静态初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);

mutex: 要初始化的互斥量
attr: 设为nullptr即可.
返回值:成功返回0,失败返回错误码


销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex:要销毁的互斥量
返回值:成功返回0,失败返回错误码


互斥量加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

mutex:要加锁的互斥量
返回值:成功返回0,失败返回错误码


互斥量解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex:要解锁的互斥量
返回值:成功返回0,失败返回错误码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
	char *id = (char*)arg;
	while ( 1 ) 
	{
		pthread_mutex_lock(&mutex);在临界区之前互斥量加锁
		if ( ticket > 0 ) 
		{
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);出了临界区互斥量解锁
			} 
		else 
		{
			 pthread_mutex_unlock(&mutex);最后一个票时,因为是加锁的,不执行if下面的语句无法解锁,要在else后面也要解一次锁
			 break;
		}
	}
 }
int main( void )
{
	pthread_t t1, t2, t3, t4;
	pthread_mutex_init(&mutex, NULL);
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	pthread_mutex_destroy(&mutex);
}

效果
在这里插入图片描述

死锁:死锁是多个执行流占着自己不释放的资源,而去申请被其他执行流不会释放的资源而处于一种永久等待的状态
死锁的四个必要条件

1.互斥条件:一个资源一次只能被一个执行流使用
2.请求与保持条件:一个执行流申请其他资源申请不到时,不会释放自己原本的资源
3.不剥夺条件:一个执行流在未结束之前,不能剥夺该执行流所掌握的资源
4.循环等待条件:多个执行流形成首尾相接循环等待资源的关系

避免死锁

1.破坏死锁的四个必要条件的一个或多个
2.资源一次性分配
3.加锁顺序一致
4.银行家算法
5.死锁检测

条件变量

与互斥量不同,条件变量用于自动阻塞一个线程,直到某个条件的满足,会唤醒这个线程。一般条件变量和锁一起配合使用。

条件变量有两个动作:一是一个线程不满足条件而挂起,并释放自带的锁;二是挂起线程受到某种信号被唤醒重新申请锁

条件变量的接口
条件变量的函数的头文件都是#include <pthread.h>,gcc/g++编译的时候都要加上-lpthread

初始化条件变量

静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化

int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);

cond: 要初始化的条件变量
attr: 设为nullptr即可.
返回值:成功返回0,失败返回错误码


销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);int pthread_mutex_destroy(pthread_mutex_t *mutex);

cond:要销毁的条件变量
返回值:成功返回0,失败返回错误码


等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);

cond:要等待的条件变量
mutex:互斥量
如果一个线程带着锁挂起(被阻塞),会自动释放这个锁,避免死锁。这也是该函数内需要传入锁的原因


唤醒等待

int pthread_cond_signal(pthread_cond_t *cond);一次只能唤醒一个在等待队列中的线程
int pthread_cond_broadcast(pthread_cond_t *cond);一次唤醒在等待队列的所有线程

cond:需要唤醒的条件变量
如果一个线程被唤醒,会重新申请锁


基于阻塞队列的单生产者单消费者模型

阻塞队列:设定一个队列是有容量的,当队列已满时,无法往队列中插入数据;队列已空时,无法往队列中读取数据。

生产者消费者模型:生产者的任务是生产数据,消费者的任务是消费数据。

代码:

blockqueue.h

#pragma once
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
class BlockQueue
{
private:
	bool IsFull()
	{
		return q.size()==capacity;
	}
	bool IsEmpty()
	{
		return q.size()==0;
	}
	void ConsumerWait()
	{
		pthread_cond_wait(&c,&lock);
	}
	void ProductorWait()
	{
		pthread_cond_wait(&p,&lock);
	}
	void BlockQueueLock()
	{
		pthread_mutex_lock(&lock);
	}
	void BlockQueueUnlock()
	{
		pthread_mutex_unlock(&lock);
	}
public:
	BlockQueue(int _capacity = 3):capacity(_capacity)
	{
		pthread_mutex_init(&lock,nullptr);
		pthread_cond_init(&c,nullptr);
		pthread_cond_init(&p,nullptr);
	}
	void PushData(int data)
	{
		BlockQueueLock();
		while(IsFull())循环判定,防止signal出错而没有唤醒消费者,进行下面的错误插入
		{
			pthread_cond_signal(&c);当队列已满,给消费者发信号将消费者线程唤醒,进行消费
			ProductorWait();
		}
		q.push(data);
		BlockQueueUnlock();
	}
	void PopData(int &data)
	{
		BlockQueueLock();
		while(IsEmpty())循环判定,防止signal出错而没有唤醒生产者,进行下面的错误读取
		{
			pthread_cond_signal(&p);当队列已空,给生产者发信号将生产者线程唤醒,进行生产
			ConsumerWait();
		}
		data = q.front();
		q.pop();
		BlockQueueUnlock();
	}
	~BlockQueue()
	{
		pthread_mutex_destroy(&lock);
		pthread_cond_destroy(&c);
		pthread_cond_destroy(&p);
	}
private:
	std::queue<int>q;
	int capacity;阻塞队列的容量
	pthread_mutex_t lock;互斥量
	pthread_cond_t c;生产者的条件变量
	pthread_cond_t p;消费者的条件变量
};

blockqueue.cpp

#include "blockqueue.h"
#include <iostream>
using namespace std;
void* productor(void*arg)
{
	BlockQueue*bq = (BlockQueue*)arg;
	srand((unsigned int)time(nullptr));
	while(1)
	{
		int data = rand()%100+1;//产生随机数
		bq->PushData(data);
		cout<<"product a number:"<<data<<endl;
		sleep(1);
	}
}
void* consumer(void*arg)
{
	BlockQueue*bq = (BlockQueue*)arg;
	int data;
	while(1)
	{
		bq->PopData(data);
		cout<<"consume a number:"<<data<<endl;
		sleep(1);
	}
}
int main()
{
	BlockQueue*bq = new BlockQueue;
	pthread_t c,p;
	pthread_create(&p,nullptr,productor,(void*)bq);
	pthread_create(&c,nullptr,consumer,(void*)bq);
	pthread_join(p,nullptr);
	pthread_join(c,nullptr);
	delete bq;
	return 0;
}

运行效果
在这里插入图片描述
阻塞队列的容量为3.生产三个随机数,给消费者发信号,告知消费者可以消费了,然后生产者进行阻塞,消费者消费三个数据给生产者发信号,告知生产者可以生产了,然后消费者进行阻塞。造成循环反复的生产消费。

POSIX信号量

POSIX信号量用于线程间同步,达到无冲突访问共享资源

同步:在保证数据安全的前提上,让线程按照某种特定的顺序访问临界资源,从而不产生饥饿问题。

信号量是一个计数器,本质是描述临界资源数目的计数器。
只要信号量申请了,那么一定在临界区有一份资源是属于申请信号量的程序的.
信号量的常见操作P(- - 预定某种资源,将信号量数目减1),V(++,释放某种资源,将信号量数目加1),而且PV操作都是原子的,即要么没有申请或释放信号量,要么申请了信号量数目–,释放了信号量数目++。

问:全局变量是否能充当信号量?
答:不能,因为对全局变量的++,–操作并非原子的,可能会有多个线程同时访问全局变量造成变量内容的不确定性。

信号量的接口
信号量的函数的头文件都是#include <semaphore.h>,gcc/g++编译的时候都要加上-lpthread

信号量初始化

int sem_init(sem_t *sem, int pshared, unsigned int value);

sem:要初始化的信号量
pshared:进程间设为非0,线程间设为0
value:信号量的初始值
返回值:成功返回0,失败返回-1


信号量销毁

int sem_destroy(sem_t *sem);

sem:要销毁的信号量
返回值:成功返回0,失败返回-1


信号量的等待(预定,P操作)

int sem_wait(sem_t *sem);

sem:要等待的信号量,会将信号量的值减1,表明有程序预定了临界区的某份资源
返回值:返回值:成功返回0,失败返回-1


信号量的增加(释放,V操作)

int sem_post(sem_t *sem);

sem:要增加的信号量,会将信号量的值加1,表明已有程序释放了临街区的资源
返回值:返回值:成功返回0,失败返回-1


基于环形队列的单生产者单消费者模型

环形队列:用数组模拟实现,用模运算实现环形特性

ringqueue.h

#pragma once 
#include <iostream>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
class RingQueue
{
private:
	void P(sem_t&t)
	{
		sem_wait(&t);
	}
	void V(sem_t&t)
	{
		sem_post(&t);
	}
public:
	RingQueue(int _cap = 20):cap(_cap),v(_cap)
	{
		sem_init(&c,0,0);
		sem_init(&p,0,cap);
	}
	void PushData(int& data)
	{
		static int pos_p = 0;设为静态,防止每次插入数据都是从0开始
		P(p);生产一个数据,就将生产者的信号量减1
		v[pos_p] = data;
		V(c);将消费者的信号量加1,表明可以进行消费
		pos_p++;
		pos_p%=cap;
	}
	void PopData(int& data)
	{
		static int pos_c = 0;同样设为静态,防止每次读取数据都是从0开始
		P(c);消费一个数据,就将消费者的信号量减1
		data = v[pos_c];
		V(p);将生产者的信号量加1,表明可以进行生产
		pos_c++;
		pos_c%=cap;

	}
	~RingQueue()
	{
		sem_destroy(&c);
		sem_destroy(&p);
	}
private:
	std::vector<int>v;
	int cap;环形队列的容量
	sem_t c;生产者的信号量
	sem_t p;消费者的信号量
};

ringqueue.cpp

#include "ringqueue.h"
void* productor(void*arg)
{
	RingQueue*rq = (RingQueue*)arg;
	srand((unsigned long)time(nullptr));
	while(true)
	{
		int data = rand()%100+1;
		rq->PushData(data);
		std::cout<<"productor number is:"<<data<<std::endl;
		sleep(1);
	}
}
void* consumer(void*arg)
{
	RingQueue*rq = (RingQueue*)arg;
	while(true)
	{
		int data = 0;
		rq->PopData(data);
		std::cout<<"consumer number is:"<<data<<std::endl;
		sleep(1);
	}
}
int main()
{
	RingQueue*rq = new RingQueue(5);
	pthread_t c,p;
	pthread_create(&p,nullptr,productor,rq);
	pthread_create(&c,nullptr,consumer,rq);
	pthread_join(p,nullptr);
	pthread_join(c,nullptr);
	delete rq;
	return 0;
}

运行效果:
在这里插入图片描述

生产一个数据,消费一个数据,循环往复。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值