本文名词解释:
条件变量:条件变量是线程同步的一种机制,它允许线程等待某个特定条件的发生。线程可以在条件变量上等待,直到其他线程通知条件已经满足。条件变量的使用通常与一个互斥锁结合在一起,以防止多个线程同时修改条件或共享资源,从而导致竞争条件
互斥锁:互斥锁(Mutex)是一种用于多线程同步的机制,确保同一时刻只有一个线程可以访问共享资源或执行特定代码块。互斥锁的主要目的是防止多个线程同时访问共享资源,从而导致数据竞争和不一致性
临界区:临界区指的是一个访问共用资源(如共用设备或共用存储器)的程序片段,这些共用资源无法同时被多个线程访问。当有线程进入临界区时,其他线程必须等待,以确保共享资源是被互斥地获得和使用
1.死锁
1.死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源,而处于的一种永久等待状态
2.死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个执行流使用--前提
2.请求与保持:条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放--原则
3.不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺--原则
4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系--重要条件
3.解决死锁(破坏四个必要条件的任意一个)
1.加锁顺序一致:我们在申请锁的时候,A线程先申请A锁,在申请B锁,而B线程先申请B锁,在申请A锁,所以两个线程天然申请锁的顺序就是环状的。我们可以尽量不让线程出现这个环路情况,我们让两个线程申请锁的顺序保持一致,就可以破坏循环等待问题。两个线程都是先申请A锁在申请B锁
2.避免锁未释放的场景:接口:pthread_mutex_trylock
,失败了就会返回退出,释放锁
3.资源一次性分配:资源一次性分配,比如说你有一万行代码,有五处要申请锁,你可以最开始一次就给线程分配好,而不要把五处申请打散到代码各个区域里所导致加锁场景非常复杂
4.避免死锁算法(基本用不上):死锁检测算法和银行家算法
2.同步
1.同步!同步问题是保证数据安全的情况下,让我们的线程访问资源具有一定的顺序性
2.保证线程安全同步了,为什么还要设置锁?要注意前言和后果。排队是结果。例如突然新来了一个线程,被锁挡在了门外,才开始到后面排队的。分配均衡的可以使用纯互斥,同步是解决分配不均衡问题
3.快速提出解决方案--条件变量
1.锁和铃铛(条件变量--布尔类型)都是一个结构体,OS 先描述再组织
2.条件变量必须依赖于锁的使用(条件就是被锁了,所以才加入等待队列)
4.条件变量
1.当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它 什么也做不了
2.例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个 节点添加到队列中。这种情况就需要用到条件变量
5.条件变量函数cond
条件变量就相当于是铃铛,和锁的设置非常的相似
初始化 – pthread_cond_init()
静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态分配
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数分析:
1.cond:需要初始化的条件变量
2.attr:初始化条件变量的属性,一般设置为 nullptr
返回值:成功返回 0
,失败返回错误码
销毁– pthread_cond_destroy()
int pthread_cond_destroy(pthread_cond_t *cond);
参数分析:
1.cond: 需要销毁的条件变量
返回值:成功返回 0
,失败返回错误码
注意:使用 PTHREAD_COND_INITIALIZER
初始化的条件变量不需要销毁
等待条件变量 – pthread_cond_wait()
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数分析:
1.cond:需要等待的条件变量
2.mutex:当前线程所处临界区对应的互斥锁
为啥要传一个锁变量,pthread_cond_wait让线程等待的时候,会自动释放锁,将其加入等待队列中,不用管临界资源的状态情况
返回值:成功返回0,失败返回错误码
wait必须在加锁和解锁之间进行
唤醒所有进程 – pthread_cond_broadcast():唤醒等待队列中的全部线程
int pthread_cond_broadcast(pthread_cond_t *cond);
参数分析:cond需要等待的条件变量
返回值:成功返回0,失败返回错误码
唤醒首个线程 – pthread_cond_signal:唤醒等待队列中的首个进程
int pthread_cond_signal(pthread_cond_t *cond);
参数分析:cond需要等待的条件变量
返回值:成功返回0,失败返回错误码
6.条件变量的使用
对于线程的管理: 先所有都锁上,再依次唤醒,就实现了每个线程进去执行一次,退出,下一个执行
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
std::cout << "pthread: " << number << " , cnt: " << cnt++ << std::endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
for(uint64_t i = 0; i < 5; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, Count, (void*)i);
}
while(true)
{
sleep(1);
pthread_cond_broadcast(&cond);
std::cout << "signal one thread..." << std::endl;
}
return 0;
}
如何知道要让一个线程去等待了?临界区有资源,此时的操作,直接离开说明两线程是互斥状态,去排队说明两线程是同步状态
如何知道临界区有资源?在加锁后自行判断,常见的方法是加一个全局bool变量来追踪
所以等待的过程,一定要在加锁和解锁之间, pthread_cond_wait让线程等待的时候,会自动释放锁,将其加入到等待队列中
总之,等待条件满足的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁,条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的
3.cp模型:生产者消费者模型
存在超市的原因:效率高,中转站,大号缓存解决了忙闲不均,使时生产者知道有多少存储空间,使消费者知道有多少商品,让生产和消费有一定程度的解耦
在计算机中,生产者指线程,消费者也指线程,超市指特定结构的内存空间->共享资源->存在并发问题,将商品理解为数据,执行流在做通信
如何高效通信
1.互斥是一个保证安全的关系
2.研究超市的并发:生产者与生产者(竞争互斥,只允许一个),消费者与消费者(互斥),生产者与消费者(互斥--安全,同步--一定的顺序性)
321原则:三种关系,两种角色(生产和消费),一个内存结构(特定结构的内存空间)
例如解耦 add 和 main ,实现高并发
4.基于等待队列的生产者消费者模型
在多线程编程中,阻塞队列是一种常用的数据结构,用于实现生产者和消费者模型。与普通队列相比,阻塞队列具有以下特点:当队列为空时:从队列获取元素的操作将会被阻塞,直到队列中有新元素被放入,当队列满时:往队列里存放元素的操作也会被阻塞,直到队列中有元素被取出,其他情况:其余时间就是边生产边消费,同时进行
Makefile
注意还要包含pthread库并使用c++11
head1.hpp
#include <iostream>
#include <pthread.h>
#include <queue>
template <class T>
class BlockQueue {
static const int defaultNum = 20;
public:
BlockQueue(int maxcap = defaultNum) : maxcap_(maxcap) {
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&c_cond_, nullptr);
pthread_cond_init(&p_cond_, nullptr);
}
~BlockQueue() {
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&c_cond_);
pthread_cond_destroy(&p_cond_);
}
void push(const T &in) {
pthread_mutex_lock(&mutex_);
while (q_.size() == maxcap_) {
pthread_cond_wait(&p_cond_, &mutex_);
}
q_.push(in);
pthread_cond_signal(&c_cond_);
pthread_mutex_unlock(&mutex_);
}
T pop() {
pthread_mutex_lock(&mutex_);
while (q_.size() == 0) {
pthread_cond_wait(&c_cond_, &mutex_);
}
T out = q_.front();
q_.pop();
pthread_cond_signal(&p_cond_);
pthread_mutex_unlock(&mutex_);
return out;
}
private:
std::queue<T> q_;
int maxcap_; // 极限容量
pthread_mutex_t mutex_;
pthread_cond_t c_cond_; // 消费者条件变量
pthread_cond_t p_cond_; // 生产者条件变量
};
代码解析:
阻塞队列:首先是队列宽度20,私有成员,一个队列,容量,mutex_互斥锁,保护队列的访问,c_cond_消费者条件变量,p_cond_生产者条件变量,构造函数:初始化最大容量,互斥是条件变量
push逻辑:首先锁定互斥锁,确保没有其他线程正在访问队列,然后检查队列是否已经满了,使用while循环而不是if可以防止线程被虚假唤醒,如果队列已满,则等待生产者条件变量(解锁互斥锁,将调用线程置于等待队列中,直到一个线程对生产者条件变量使用signal或broad_cast,当线程被唤醒时,该函数会在重新获取互斥锁之前返回),如果不满,则将in加入队列中,再触发消费者条件变量(队列现在不为空,可以买了),表示有东西已经生产完成,再解锁互斥锁,表示可以调用其他部分
pop逻辑:首先锁定互斥锁,检查队列是否为空,为空就等待消费者条件变量,然后删除队头元素,触发生产者条件变量(队列现在是未满状态,可以生产了),解锁互斥锁
zuse1.cc
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <queue>
#include <mutex>
#include <condition_variable>
// Task 类定义
class Task {
public:
Task(int a, int b, char op) : data1(a), data2(b), oper(op), result(0), exitcode(0) {}
std::string GetTask() const {
return std::to_string(data1) + " " + std::string(1, oper) + " " + std::to_string(data2) + "=?";
}
std::string GetResult() const {
std::string r = std::to_string(data1) + " " + std::string(1, oper) + " " + std::to_string(data2) + "=" + std::to_string(result);
if (exitcode != 0) {
r += "[Error: " + std::to_string(exitcode) + "]";
}
return r;
}
void run() {
switch (oper) {
case '+':
result = data1 + data2;
break;
case '-':
result = data1 - data2;
break;
case '*':
result = data1 * data2;
break;
case '/':
if (data2 == 0) {
exitcode = 1; // Division by zero
} else {
result = data1 / data2;
}
break;
case '%':
if (data2 == 0) {
exitcode = 2; // Modulo by zero
} else {
result = data1 % data2;
}
break;
default:
exitcode = 3; // Unknown operator
break;
}
}
private:
int data1, data2;
char oper;
int result;
int exitcode; // Error code, e.g., division by zero
};
// 阻塞队列实现
template<typename T>
class BlockQueue {
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
public:
void push(const T& value) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(value);
cv_.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty(); });
T value = queue_.front();
queue_.pop();
return value;
}
};
// 生产者线程函数
void* Producer(void* args) {
BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);
while (true) {
int data1 = rand() % 100 + 1;
int data2 = rand() % 100 + 1;
char op = "+-*/%"[rand() % 5];
Task t(data1, data2, op);
bq->push(t);
std::cout << "Produced task: " << t.GetTask() << " by thread id: " << pthread_self() << std::endl;
sleep(rand() % 3 + 1);
}
return nullptr;
}
// 消费者线程函数
void* Consumer(void* args) {
BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);
while (true) {
Task t = bq->pop();
t.run();
std::cout << "Processed task: " << t.GetTask() << " Result: " << t.GetResult() << " by thread id: " << pthread_self() << std::endl;
sleep(1);
}
return nullptr;
}
int main() {
srand(time(nullptr));
BlockQueue<Task> bq;
pthread_t consumers[3], producers[5];
for (int i = 0; i < 3; i++) {
pthread_create(&consumers[i], nullptr, Consumer, &bq);
}
for (int i = 0; i < 5; i++) {
pthread_create(&producers[i], nullptr, Producer, &bq);
}
// 注意:在这个示例中,我们没有等待线程结束,因为它们是无限循环的。
// 在实际应用中,您可能需要一种机制来优雅地停止这些线程(例如,使用全局变量作为标志)。
// 为了示例的完整性,这里我们简单地让主线程睡眠一段时间,然后退出(不推荐在实际应用中使用)。
sleep(60); // 让主线程睡眠60秒,以便观察生产者和消费者的输出。
// 注意:由于线程是无限循环的,下面的代码在实际情况下是无效的。
// 在实际应用中,您需要在停止线程后再进行这些操作。
// pthread_exit(NULL); // 这行代码不会停止其他线程,仅退出主线程。
// delete &bq; // 这是错误的,不能删除一个局部对象的地址。
// 而且,由于 bq 是局部对象,它会在 main 函数结束时自动销毁,
// 但由于线程还在运行并可能访问 bq,这会导致未定义行为。
// 由于示例的特殊性(无限循环线程),我们在这里不调用 pthread_join。
// 在实际应用中,您应该确保所有线程都已正确停止并清理资源。
// 由于线程是无限循环的,并且我们没有提供停止它们的机制,
// 因此这个程序将永远运行下去。在实际应用中,您需要设计一种停止线程的方法。
return 0; // 注意:由于线程未正确停止和清理,这个返回值在实际情况下可能不是安全的。
}
代码解析:
task类即任务类,执行四则运算的代码
生产者线程函数producer,不断生成task对象到阻塞队列,打印消息并休眠
消费者线程函数和生产者的大差不差,只不过添加变为删除,从创建task对象变为运行task类的run函数
main函数,首先初始化随机数生成器,创建阻塞队列,启动3个生产者和5个消费者进程,再无限循环进行观察
生产者消费者模型的高效点:生产和消费之后线程并行执行,同时生产消费