线程知识合集:
【Linux】线程概念 && 线程控制(接口详谈)&& 僵尸线程的产生和解决
【Linux】线程安全:互斥 & 同步
【Linux】线程安全:死锁(附加gdb调试分析)
生产者-消费者模型
场景引入
对于吃面-做面模型
来说,其临界资源就是一个全局变量g_bowl = 0 / 1
,也就是该临界资源只有一个;
如果设计一个类似于微信后端的程序:一个用户发送给另一个用户的消息,实际上是先发送至微信后端服务器处理后,再由服务器转发给另一个用户。我们可以将后端大致分为三种线程:接受消息线程,消息处理线程,发送数据线程;
在该场景中,服务器储存用户消息的内存就是临界资源。如果临界资源只有一个,那么为了保证资源安全,一个用户将消息发送给微信服务器,在服务器处理完消息并发送前,服务器的不会接收其他用户发送的消息,这种情况实际上肯定要避免。
为了实现高并发(同时可处理多个用户消息),就要让服务器的临界资源远不止一个,一般就是设立一个队列,队列的大小就是临界资源的个数,队列的每个元素都存储(或指向)了一条用户的消息,且该消息队列必须保证线程安全;
但是一般接受消息、发送数据线程的执行速度远快于消息处理线程,如果一条数据没有被处理完毕,该线程内容需一直在临界资源,而速度更快的接受消息线程因为临界资源空间不足,就会陷入等待。
为了处理消息的速度更快,我们需实现各类型线程解耦(接受消息、处理消息、发送消息互不干扰)和忙闲不均(资源队列作相当于缓冲区,避免双方线程一个阻塞,导致另一个阻塞),设置多个临界资源队列即可。
生产者-消费者模型的结构
通过一个简单的生产者-消费者模型
,了解多临界资源的机制:
就是通过一个容器(这里使用队列)存放临界资源,使生产者和消费者解耦;
解耦:生产者线程和消费者线程不直接通讯。
生产线程生成的数据不用等待消费线程处理后再继续,而是直接放入临界资源队列中;
消费线程读取线程不通过生产线程,而是直接读取临界资源队列即可;
临界资源队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力;
临界资源队列必须保证线程安全:生产线程之间不能同时向资源队列写入;
消费线程之间不能同时向资源队列读取;
生产线程和消费线程之间也不能同时操作资源队列;
多临界资源机制的优点
生产者-消费者模型的实现
123规则:
-
一个线程安全的队列;
-
两种角色的线程:生产者线程、消费者线程;
-
三个规则;
源码:prod_cons.cpp
#include <stdio.h>
#include <unistd.h>
#include <queue>
#include <pthread.h>
#define THREAD_COUNT 2
using namespace std;
// 1.一个线程安全的队列
// 封装一个安全队列的类
class safe_queue
{
public:
//构造
safe_queue(size_t capacity = 1)
:g_data(0)
,_capacity(capacity)
{
//初始化互斥锁和条件变量
pthread_mutex_init(&_q_lock, NULL);
pthread_cond_init(&_prod_cond, NULL);
pthread_cond_init(&_cons_cond, NULL);
}
//析构
~safe_queue()
{
//销毁互斥锁和条件变量
pthread_mutex_destroy(&_q_lock);
pthread_cond_destroy(&_prod_cond);
pthread_cond_destroy(&_cons_cond);
}
//生产线程的push接口
void Push()
{
pthread_mutex_lock(&_q_lock);
while(_q.size() >= _capacity) //当资源队列已满,生产线程等待
{
pthread_cond_wait(&_prod_cond, &_q_lock);
}
_q.push(++g_data);
printf("Product_thread: %p, make %d\n", pthread_self(), g_data);
pthread_cond_signal(&_cons_cond);
pthread_mutex_unlock(&_q_lock);
}
//消费线程的pop接口
void Pop()
{
pthread_mutex_lock(&_q_lock);
while(_q.size() <= 0) //当资源队列为空,消费线程等待
{
pthread_cond_wait(&_cons_cond, &_q_lock);
}
printf("Consume_thread: %p, get %d\n", pthread_self(), _q.front());
_q.pop();
pthread_cond_signal(&_prod_cond);
pthread_mutex_unlock(&_q_lock);
}
private:
queue<int> _q; //队列(临界资源)
int g_data; //队列资源数序号
size_t _capacity; //预设队列大小
pthread_mutex_t _q_lock; //队列的互斥锁
pthread_cond_t _prod_cond; //生产线程的条件变量
pthread_cond_t _cons_cond; //消费线程的条件变量
};
// 生产线程入口函数
void* prod_start(void* arg)
{
safe_queue* Que = (safe_queue*)arg;
while(1)
{
Que->Push();
}
}
// 消费线程入口函数
void* cons_start(void* arg)
{
safe_queue* Que = (safe_queue*)arg;
while(1)
{
Que->Pop();
}
}
int main()
{
// 初始化一个资源队列(线程安全)
size_t n = 1;
safe_queue* Que = new safe_queue(n);
// 创建线程
int i = 0;
pthread_t prod_t[THREAD_COUNT];
pthread_t cons_t[THREAD_COUNT];
for(; i < THREAD_COUNT; ++i)
{
int ret = pthread_create(&prod_t[i], NULL, prod_start, (void*)Que);
if(ret < 0)
{
perror("pthread_create_prod");
return 0;
}
ret = pthread_create(&cons_t[i], NULL, cons_start, (void*)Que);
if(ret < 0)
{
perror("pthread_create_prod");
return 0;
}
}
// 线程等待
for(i = 0; i < THREAD_COUNT; ++i)
{
pthread_join(prod_t[i], NULL);
pthread_join(cons_t[i], NULL);
}
// 销毁队列
delete Que;
return 0;
}
信号量
信号量是什么
若想保证线程同步,之前提到可以采用条件变量来控制线程进入PCB等待序列,但使用条件变量麻烦之处在于,需要程序员自己判断条件是否满足。
而信号量,相当于封装了上述的第2步和第4步,使用信号量,程序员无需判断线程是否满足条件;
- 条件变量的本质是一个PCB等待队列,而信号量的本质就是一个资源计数器+PCB等待队列;
- 使用信号量,需在初始化时添加该线程的可使用资源数;
- 每个信号量都有P(等待接口)、V(释放接口)两种操作;
P操作相当于上述步骤2,该操作会先将计数器资源-1(非原子操作)
-1后,若资源计数器值<0,说明此时无可用资源,会将该线程放入PCB等待序列阻塞等待;
若资源计数器值>=0,说明有资源可用,直接执行后续代码即可;
V操作相当于上述步骤4,该操作会先将计数器资源+1
+1后,若资源计数器值<=0,说明该信号量对应的PCB等待队列有线程等待,那么唤醒PCB等待队列的线程;
若资源计数器值>0,说明没有线程在等待,不用执行任何操作;
信号量的接口
信号量的注意事项
-
信号量可用于线程或进程;
信号量必须实现不同线程(或进程)访问到同一个,否则无意义,因此:
在线程中使用,信号量必须开辟在当前进程的全局数据区(即设为全局变量),以便所有线程访问;
在进程中使用,信号量必须开辟在共享内存中(进程通信); -
等待接口
sem_wait
是P操作,释放接口sem_post
是V操作; -
sem_wait
等待接口不会对互斥锁做任何操作,因此在使用时,必须若先加锁,再执行信号量P操作;
-
信号量不止可以实现同步功能,也可以实现互斥功能;
互斥锁本质就是一个临界资源只能同时被一个线程所访问,那么让信号量的资源量变为1,所有线程要访问该临界资源时,必须先进行信号量P操作,若资源占用,则进入PCB等待队列,这个流程和互斥锁一模一样;
信号量实现生产者-消费者模型
- 通过环形队列存放生产者生成的资源数据;
- 初始,生产者可使用资源数:环形队列的大小;消费者可使用资源数:0;
源码:sem.cpp
// 通过信号量实现“生产者-消费者模型”
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#define CAPACITY 1 //资源队列大小
#define THREADCOUNT 2 //每种线程数量
using namespace std;
/* 1.定义一个线程安全的队列
* 队列:环形队列(数组模拟)
* 线程安全:
* 同步:信号量实现
* 互斥:信号量实现
* */
class SafeQueue
{
public:
// 构造
SafeQueue()
: _capacity(CAPACITY)
, _vec(CAPACITY)
{
// 初始化信号量
sem_init(&sem_mutex, 0, 1); //互斥信号量(初始资源数为1)
sem_init(&sem_cons_cond, 0, 0); //消费者信号量(初始资源数为0)
sem_init(&sem_prod_cond, 0, CAPACITY); //生产者信号量(初始资源为队列长度)
_pos_write = 0;
_pos_read = 0;
}
// 析构
~SafeQueue()
{
// 销毁信号量
sem_destroy(&sem_mutex);
sem_destroy(&sem_cons_cond);
sem_destroy(&sem_prod_cond);
}
// 消费者线程的POP接口
void Pop()
{
sem_wait(&sem_cons_cond);
sem_wait(&sem_mutex);
int cur = _vec[_pos_read];
_pos_read = (_pos_read + 1)%_capacity; //更新_pos_read
printf("Consume:%p get:%d\n", pthread_self(), cur);
sem_post(&sem_prod_cond);
sem_post(&sem_mutex);
}
// 生产者线程的PUSH接口
void Push(int data)
{
sem_wait(&sem_prod_cond);
sem_wait(&sem_mutex);
printf("Product:%p make:%d\n", pthread_self(), data);
_vec[_pos_write] = data;
_pos_write = (_pos_write + 1)%_capacity;
sem_post(&sem_cons_cond);
sem_post(&sem_mutex);
}
private:
size_t _capacity; //数组容量
vector<int> _vec; //环形队列数组
sem_t sem_mutex; //队列资源互斥信号量
sem_t sem_cons_cond; //消费者线程的同步信号量
sem_t sem_prod_cond; //生产者线程的同步信号量
int _pos_write; //队列末尾,插入元素位置
int _pos_read; //队列头部,读取元素位置
};
// 消费者线程入口函数
void* cons_strat(void* arg)
{
SafeQueue* sq = (SafeQueue*)arg;
while(1)
{
sq->Pop();
sleep(2);
}
}
//生产者向循环队列存放的数据
int g_data = 0;
//设置静态互斥锁,不同生产者对g_data必须互斥访问
pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
// 生产者线程入口函数
void* prod_strat(void* arg)
{
SafeQueue* sq = (SafeQueue*)arg;
while(1)
{
pthread_mutex_lock(&g_lock);
sq->Push(g_data++);
pthread_mutex_unlock(&g_lock);
sleep(1);
}
}
// 创建生产者-消费者两种线程
int main(){
SafeQueue* sq = new SafeQueue();
pthread_t cons[THREADCOUNT], prod[THREADCOUNT];
for(int i = 0; i < THREADCOUNT; i++){
int ret = pthread_create(&cons[i], NULL, cons_strat, (void*)sq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
ret = pthread_create(&prod[i], NULL, prod_strat, (void*)sq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
}
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_join(cons[i], NULL);
pthread_join(prod[i], NULL);
}
delete sq;
return 0;
}