【Linux】线程的同步与互斥
目录
1. 整体学习思维导图
2. 线程的互斥
2.1 互斥的概念
2.2 见一见数据不一致的情况
2.3 引入锁Mutex(互斥锁/互斥量)
2.3.1 接口认识
2.3.2 Mutex锁的理解
2.3.3 互斥量的封装
3. 线程同步
3.1 条件变量概念
3.2 引入条件变量Cond
3.2.1 接口认识
3.2.2 同步的封装
3.2.3 为什么 pthread_cond_wait 需要互斥量?
4. 生产者/消费者模型
4.1 基于BlockingQueue的生产者消费者模型
4.3 封装实现生产者/消费者模型
1. 整体学习思维导图
2. 线程的互斥
2.1 互斥的概念
我们前面已经在进程通信信号量的时候简单了解过了互斥概念,以下是重要概念的回顾:
-
临界资源:多线程执行流共享的资源就叫做临界资源
-
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
-
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用,保护临界区代码就是变相保护临界资源
-
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
2.2 见一见数据不一致的情况
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>/* 抢票系统 */
int ticket = 1000; /* 票数 */
void *routine(void *args)
{std::string name = static_cast<const char*>(args);while(true){if(ticket > 0){usleep(1000);std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl;ticket--;}else{break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, routine, (void*)"thread-1");pthread_create(&t2, nullptr, routine, (void*)"thread-2");pthread_create(&t3, nullptr, routine, (void*)"thread-3");pthread_create(&t4, nullptr, routine, (void*)"thread-4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
}
我们会发现到最后票已经变成负数了,这就是全局变量资源共享会带来的问题,这个问题具体是怎么发生的呢?
-
为什么数据会不一致?
[1] ticker--
这个操作不是原子的,可能会出现多个线程同时访问执行这条语句,我们将ticket--
转换成底层的汇编:
0XFF00: 载入 ebx ticket
0XFF02:减少 ebx 1
0XFF04: 写回 0X1111 ebx
我们将这三步放到CPU访问内存的操作来看:
结合场景:现在有一个线程A,执行完第二步 ebx:99 pc:0XFF04
时,刚好调度时间片到了,保存上下文数据切换到下一个线程。线程B切换上来将100加载进行--,直到最后ticket--到了1,此时线程B的调度时间片到了,保存上下文数据,ebx:1 pc:0XFF02
。恢复线程A的调度,执行pc执行,ticket又再次被写回到99,这就是数据不一致问题!
从以上我们可以得知,我们认为只有一条汇编语句的就是原子的,多条汇编语句会带来线程切换问题,一旦切换就会导致数据不是原子的!
为什么我们的程序ticket最后值为负数?
ticket作为全局变量,当一些线程执行ticket > 0
逻辑计算之后,正好被切走,正好此时ticket == 1
。然后又来了一个线程进入进行判断ticket--
到0
,然后之前被切走的线程又进来--就导致ticket为负数了!
由此我们可以得出一个结论,对于全局资源的访问我们需要加保护,一次只让一个线程进行访问资源,多线程下可能会出现并发问题,这种问题我们叫做线程安全问题!
2.3 引入锁Mutex(互斥锁/互斥量)
2.3.1 接口认识
锁的创建:
① ⽅法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER② ⽅法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);参数:mutex:要初始化的互斥量attr:nullptr
锁的定义有两种一种是全局性的锁,一种是局部的锁:
-
如果定义的是全局或者静态的锁,可以只使用pthread_mutex_t 锁的名字=PTHREAD_MUTEX_INITIALIZER
-
如果定义的这把锁是动态申请的,比如new或栈上开辟的,必须使用pthread_mutex_init函数来进行初始化。参数1就是你自己定义的锁,参数2是属性,直接设为nullptr即可。
锁的销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:要销毁的互斥量
销毁互斥量需要注意:
-
使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁,因为此时锁是静态或全局的,不需要 destroy,全局的或者静态的变量会随着进程的运行而一直存在,进程结束他也就自动释放了 -
不要销毁⼀个已经加锁的互斥量
-
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量的加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
参数:mutex:要销毁的互斥量
调用 pthread_ lock 时,可能会遇到以下情况:
-
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
-
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么
pthread_lock
调用会陷入阻塞(执行流被挂起),等待互斥量解锁(多线程并发竞争锁访问临界资源)。
代码修改,了解锁之后我们可以通过修改代码加锁保证抢票不会到达负数:
全局锁:
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>/* 抢票系统 */
int ticket = 1000; /* 票数 */
/* 全局锁 */
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
void *routine(void *args)
{std::string name = static_cast<const char*>(args);while(true){/* 加锁 */pthread_mutex_lock(&glock);if(ticket > 0){usleep(1000);std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl;ticket--;/* 解锁 */pthread_mutex_unlock(&glock);}else{/* 解锁 */pthread_mutex_unlock(&glock);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, routine, (void*)"thread-1");pthread_create(&t2, nullptr, routine, (void*)"thread-2");pthread_create(&t3, nullptr, routine, (void*)"thread-3");pthread_create(&t4, nullptr, routine, (void*)"thread-4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
}
局部锁:
/* 抢票系统 */
int ticket = 1000; /* 票数 */
/* 局部锁 */
pthread_mutex_t lock;
void *routine(void *args)
{std::string name = static_cast<const char*>(args);pthread_mutex_init(&lock, nullptr);while(true){/* 加锁 */pthread_mutex_lock(&lock);if(ticket > 0){usleep(1000);std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl;ticket--;/* 解锁 */pthread_mutex_unlock(&lock);}else{/* 解锁 */pthread_mutex_unlock(&lock);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;/* 初始化 */pthread_mutex_init(&lock, nullptr);pthread_create(&t1, nullptr, routine, (void*)"thread-1");pthread_create(&t2, nullptr, routine, (void*)"thread-2");pthread_create(&t3, nullptr, routine, (void*)"thread-3");pthread_create(&t4, nullptr, routine, (void*)"thread-4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);/* 销毁 */pthread_mutex_destroy(&lock);return 0;
}
总结:
-
竞争申请锁,所有线程看到锁,说明锁也是临界资源,因此申请锁的过程必须是原子的!申请锁成功继续向后运行,访问临界区代码,访问临界区资源。失败,阻塞挂起申请的执行流。
-
锁的本质:让执行临界区的代码的执行流由并行变成串行!
-
所以的线程都必须遵守锁的竞争规则,不可以有线程无视锁。
-
加锁之后,临界区依然可以进行线程切换,但是没有解锁之前,其他线程无法访问临界区,只有当持有锁的线程释放锁之后,线程们再次展开对锁的竞争,访问临界区。
2.3.2 Mutex锁的理解
锁有两种实现方式:
1. 硬件实现:
操作系统会收到时钟中断去检查线程的时间片是否被调度完全,如果完了就进行切换操作,我们加锁不就是让一个线程不做切换吗,因此我们可以让操作系统忽略收到的时钟中断即可!(只做了解)
2. 软件实现:
软件实现的过程是通过CPU的%al寄存器,执行swap/change
操作完成加锁的过程。
理解软件实现的加锁过程:
[1] CPU的上下文
我们知道CPU在调度一个线程时,会将其上下文数据加载到寄存器,调度结束时也会保存上下文数据。
[2] 竞争锁
现在内存中一块锁的区域,默认值为1,CPU调度线程竞争锁时,会先将CPU寄存器%al的内容置为0,去对内存中的lock做一个交换!
我们发现锁的申请过程只有一条汇编语句,这与我们前面理解原子概念的猜想是吻合的!因此申请锁是原子的
2.3.3 互斥量的封装
我们这样使用锁是否有点太不方便了,我们可不可以将锁封装成一个对象,进行使用呢?
Mutex.hpp
#pragma once
#include <pthread.h>namespace mutexdouble
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}~Mutex(){pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex;};/* RAII风格 */class MutexGuard{public:MutexGuard(Mutex &lock):_lock(lock) {_lock.Lock();}~MutexGuard() {_lock.Unlock();}private:Mutex &_lock; /* 注意需要是引用 */};
}
测试:
/* 抢票系统 */
int ticket = 1000; /* 票数 */
Mutex lock;
void *routine(void *args)
{std::string name = static_cast<const char *>(args);while (true){{MutexGuard guard(lock);if (ticket > 0){usleep(1000);std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl;ticket--;}else{break;}}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, routine, (void *)"thread-1");pthread_create(&t2, nullptr, routine, (void *)"thread-2");pthread_create(&t3, nullptr, routine, (void *)"thread-3");pthread_create(&t4, nullptr, routine, (void *)"thread-4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
}
-
C++内部也已经封装了mutex互斥对象,头文件为
#include <mutex>
/* RAII 风格 */
std::mutex mtx;
std::lock_guard<std::mutex> guard(mtx);
3. 线程同步
-
前面我们知道互斥是为了解决[
数据不一致/多线程由并行访问临界资源到串行访问
]的问题,但是试想一下多个线程抢夺互斥锁,如果其中一个线程多次连续抢到锁,其他线程一直没法访问临界资源会带来什么问题?线程饥饿
-
那么怎么解决呢?同步的诞生就是问题解决互斥锁带来的问题,有没有一种方式使得线程们可以有着平均拿到锁的机会?有顺序的访问?这就是同步需要解决的!也是同步的作用!
3.1 条件变量概念
我们知道线程在没有竞争到锁之后会被阻塞挂起,我们现在需要让每个线程都有获取锁的机会,第一个问题是怎么唤醒被挂起的线程;第二个问题我们需要让已经使用过锁的线程暂缓一会再获得锁。
-
为了解决第一个问题我们需要引入新的概念:条件变量
场景:
角色:放苹果的人,等待获取苹果的线程队列
中间区域:一个盘子
提示:铃铛
放苹果和取苹果的过程是互斥的需要加上锁,当放苹果的人在盘子放一个苹果,此时他就会摇铃铛告诉队列的线程,队列front的线程取苹果,然后再次排队到线程队列的最后。至此互斥和同步互补解决了多线程访问的数据不一致和线程饥饿问题!因此线程在对应的条件变量下等待,此处的条件变量是铃铛和队列
3.2 引入条件变量Cond
3.2.1 接口认识
1. 初始化和销毁
初始化:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:cond:要初始化的条件变量attr:NULL
销毁:int pthread_cond_destroy(pthread_cond_t *cond) pthread_cond_t cond = PTHREAD_COND_INITIALIZER
条件变量是 pthread_cond_t 的数据类型。它的使用跟前面互斥锁一样,可以定义成局部或者全局的。
如果是全局或者静态的,可以直接使用 PTHREAD_COND_INITIALIZER 初始化。
如果是局部的,就用pthread_cond_init 函数初始化,使用完了就destroy销毁掉
2. 等待条件
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond:条件变量,线程将在这个条件变量上等待。mutex:互斥锁,与条件变量关联的互斥锁作用:让调用线程在条件变量(cond)上等待,同时释放与之关联的互斥锁(mutex),并在被唤醒后重新获取互斥锁。
线程条件不满足时,线程就要等待,要在指定的条件变量上等待
3. 唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
等待完成后,就要进行唤醒。
pthread_cond_signal 表示唤醒一个线程,pthread_cond_broadcast 表示唤醒所有线程。
测试:
/* 条件变量 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *active(void *arg)
{std::string name = static_cast<const char*>(arg);while(true){pthread_mutex_lock(&mutex);// 没有对于资源是否就绪的判定pthread_cond_wait(&cond, &mutex); printf("%s is active!\n", name.c_str());pthread_mutex_unlock(&mutex);}}int main()
{pthread_t tid1, tid2, tid3;pthread_create(&tid1, nullptr, active, (void*)"thread-1");pthread_create(&tid2, nullptr, active, (void*)"thread-2");pthread_create(&tid3, nullptr, active, (void*)"thread-3");sleep(1);printf("Main thread ctrl begin...\n");while(true){printf("main wakeup thread...\n");pthread_cond_signal(&cond); // 一个一个唤醒// pthread_cond_broadcast(&cond); // 全部唤醒sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}
pthread_cond_wait(&cond, &mutex);
为什么等待需要传入锁?你的条件变量不满足,不代表其他线程不满足,你需要归还锁给其他线程竞争使用!
3.2.2 同步的封装
Cond.hpp
封装条件变量唯一需要注意的是要传入一个锁,因此我们需要有一个锁的变量!
#pragma once
#include <pthread.h>namespace CondModule
{class Cond{public:Cond(const Cond&) = delete;const Cond &operator=(const Cond&) = delete;Cond(){pthread_cond_init(&_cond, nullptr);}void Wait(pthread_mutex_t* mutex){int n = pthread_cond_wait(&_cond, mutex);(void)n;}void Signal(){// 唤醒在条件变量下等待的一个线程int n = pthread_cond_signal(&_cond);(void)n;}void Broadcast(){// 唤醒所有在条件变量下等待的线程int n = pthread_cond_broadcast(&_cond);(void)n;}~Cond(){pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}
3.2.3 为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据
核心原因:原子性
pthread_cond_wait
需要互斥量的核心目的是为了保证 “检查条件” 和 “进入等待” 这两个操作的原子性。若没有互斥量,可能因线程竞争导致 信号丢失 或 条件误判,进而引发死锁或逻辑错误。
错误设计示例的问题:
pthread_mutex_lock(&mutex);
while (condition_is_false) {pthread_mutex_unlock(&mutex);pthread_cond_wait(&cond); // 错误点!pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
问题在于:解锁(unlock
)和等待(pthread_cond_wait
)是分离的非原子操作。 在解锁后、进入等待前,可能发生以下事件:
-
其他线程获取到互斥量,修改了条件变量并发送信号(
pthread_cond_signal
)。 -
原线程尚未进入等待状态,导致信号被完全错过。
-
原线程最终在
pthread_cond_wait
处永久阻塞(因为条件已满足但信号已丢失)。
正确设计:pthread_cond_wait
的机制:
pthread_mutex_lock(&mutex);
while (condition_is_false) {pthread_cond_wait(&cond, &mutex); // 原子操作!
}
// 条件满足时执行操作
pthread_mutex_unlock(&mutex);
-
给条件发送信号代码
pthread_mutex_lock(&mutex);
// 设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
-
原子性释放锁并进入等待: 调用
pthread_cond_wait
时,函数会自动释放互斥量,并让线程阻塞在条件变量cond
上。解锁和等待是原子操作,中间不会有其他线程插入修改条件变量。 -
被唤醒后重新获取锁: 当其他线程调用
pthread_cond_signal
或pthread_cond_broadcast
时,原线程被唤醒,自动重新获取互斥量,继续执行后续代码。
为什么必须用互斥量?
-
保护共享数据: 条件的判断(如
condition_is_false
)通常依赖共享数据(例如全局变量)。必须通过互斥量保证线程安全访问这些数据。 -
避免信号丢失: 只有通过
pthread_cond_wait
的原子操作,才能确保在释放锁和进入等待之间不会有其他线程修改条件并发送信号。 -
防止虚假唤醒: 即使没有信号,某些系统实现可能导致线程被唤醒(称为“虚假唤醒”)。通
while(condition_is_false)
循环while
检查条件,可以避免逻辑错误。
4. 生产者/消费者模型
三种要素:
-
生产者p
-
消费者c
-
一个交易场所
"321"原则:
-
三种关系:
-
生产者之间:互斥关系(竞争)
-
消费者之间:互斥关系(竞争)
-
生产者和消费者之间:互斥关系,同步关系
-
-
两种角色
-
生产者和消费者
-
-
一个交易场所
-
以特定结构构成的一块"内存"空间!
-
生产者消费者模型的好处:
-
生产过程和消费过程解耦 --> 生产归工厂管,消费归消费者管,不同批次的线程执行不同的任务。
-
支持忙闲不均。--> 生产者可以生产很快/慢,消费者也可以消费很快/满。
-
提供效率。 --> 主要体现在工厂生产到超市存储,消费者消费之前生成的产品,同时工厂还可以继续生产!生产者和消费者并行!
4.1 基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。
4.3 封装实现生产者/消费者模型
#pragma once
#include "mutex.hpp"
#include "cond.hpp"
#include <queue>/* 基于 cond.hpp mutex.hpp */
#define defaultcap 5namespace BlockQueueModule
{template <typename T>class BlockQueue{private:BlockQueue(const BlockQueue&) = delete;const BlockQueue &operator=(const BlockQueue&) = delete;bool IsFull() { return _q.size() >= _cap; }bool IsEmpty() { return _q.empty(); }public:BlockQueue(): _cap(defaultcap), _p_size(0), _c_size(0){}void Enqueue(const T& data){/* 加锁 */_lock.Lock();while(IsFull()) /* 如果队列为满,生产者需要等待 用while不用if是为了防止伪唤醒 */{_p_size++;std::cout << "生产者,进入休眠了: _p_size: " << _p_size << std::endl;_p_cond.Wait(_lock.GetMutex());_p_size--;}/* 阻塞队列不满,可以入数据 */_q.push(data);if(_c_size > 0){/* 如果存在消费者等待唤醒执行任务 */std::cout << "唤醒消费者..." << std::endl;_c_cond.Signal();}/* 解锁 */_lock.Unlock();}void Pop(T* out){/* 加锁 */_lock.Lock();while(IsEmpty()) /* 如果队列为空,消费者需要等待,用while不用if是为了防止伪唤醒 */{_c_size++;std::cout << "消费者,进入休眠了: _c_size: " << _c_size << std::endl;_c_cond.Wait(_lock.GetMutex());_c_size--;}*out = _q.front();_q.pop();if(_p_size > 0){/* 如果存在生产者等待唤醒执行任务 */std::cout << "唤醒生产者者..." << std::endl;_p_cond.Signal();}/* 解锁 */_lock.Unlock();}~BlockQueue(){}private:MutexModule::Mutex _lock; /* 锁 *//* 两个条件变量 */CondModule::Cond _p_cond; /* 生产者变量 */CondModule::Cond _c_cond; /* 消费者者变量 *//* 阻塞队列 */std::queue<T> _q;int _cap; /* 阻塞队列最大容量 *//* 记录变量 */int _p_size; /* 记录生产者等待数量的变量 */int _c_size; /* 记录消费者等待数量的变量 */};}
测试:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <functional>
#include "blockqueue.hpp"
using namespace BlockQueueModule;#if 0
// V3
typedef std::function<void()> fun_t;
template<typename T>
class Task
{
public:Task(std::vector<T> &task, BlockQueue<T> *bq): _task(task), _bq(bq), _index(0){}int GetIndex(){++_index;_index %= _task.size();return _index;}~Task() {}std::vector<T> _task;BlockQueue<T> *_bq;int _index;
};void* Produce(void *args)
{Task<fun_t>* tk = static_cast<Task<fun_t> *>(args);/* 生产任务 */while(true){sleep(1); // 模拟生产耗时tk->_bq->Enqueue(tk->_task[tk->GetIndex()]);std::cout << "生产一个任务... : " << std::endl;}
}void* Consumer(void *args)
{Task<fun_t>* tk = static_cast<Task<fun_t> *>(args);while(true){sleep(1);/* 消费任务 */fun_t action;tk->_bq->Pop(&action);std::cout << "消费一个任务... : " << std::endl;action();}
}int main()
{fun_t fun1 = [](){ std::cout << "下载任务..." << std::endl; };fun_t fun2 = [](){ std::cout << "日志查看..." << std::endl; };fun_t fun3 = [](){ std::cout << "上传任务..." << std::endl; };std::vector<fun_t> task = {fun1, fun2, fun3};/* 申请一个阻塞队列 */BlockQueue<fun_t> *bq = new BlockQueue<fun_t>();Task<fun_t>* tk = new Task<fun_t>(task, bq);std::vector<pthread_t> p(3);std::vector<pthread_t> c(5);/* create */for(int i = 0; i < p.size(); ++i){pthread_create(&p[i], nullptr, Produce, (void*)tk);}for(int j = 0; j < c.size(); ++j){pthread_create(&c[j], nullptr, Consumer, (void*)tk);}/* join */for(int i = 0; i < p.size(); ++i){pthread_join(p[i], nullptr);}for(int j = 0; j < c.size(); ++j){pthread_join(c[j], nullptr);}return 0;
}
#endif#if 0
// v2
int cnt = 1;
void* Produce(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);/* 生产任务 */while(true){// sleep(1); // 模拟生产耗时bq->Enqueue(cnt++);std::cout << "生产一个任务... : " << cnt << std::endl;}
}void* Consumer(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);while(true){sleep(1);/* 消费任务 */int data;bq->Pop(&data);std::cout << "消费一个任务... : " << data << std::endl;}
}int main()
{/* 申请一个阻塞队列 */BlockQueue<int> *bq = new BlockQueue<int>();/* 单单 */// pthread_t p[1], c[1];// pthread_create(&p[0], nullptr, Produce, (void*)bq);// pthread_create(&c[0], nullptr, Consumer, (void*)bq);// pthread_join(p[0], nullptr);// pthread_join(c[0], nullptr);/* 多多 */std::vector<pthread_t> p(5);std::vector<pthread_t> c(3);for(int i = 0; i < p.size(); ++i){pthread_create(&p[i], nullptr, Produce, (void*)bq);}for(int j = 0; j < c.size(); ++j){pthread_create(&c[j], nullptr, Consumer, (void*)bq);}for(int i = 0; i < p.size(); ++i){pthread_join(p[i], nullptr);}for(int j = 0; j < c.size(); ++j){pthread_join(c[j], nullptr);}delete bq;return 0;
}
#endif#if 0
// V1
bool Ready = false;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 共享互斥锁
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; // 生产者条件变量
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; // 消费者条件变量void* Produce(void* args) {std::string name = static_cast<const char*>(args);while (true) {pthread_mutex_lock(&mutex);// 等待直到产品被消费(Ready == false)while (Ready == true) {pthread_cond_wait(&cond_producer, &mutex);}// 生产std::cout << name << ",开始生产..." << std::endl;Ready = true;// 通知消费者pthread_cond_signal(&cond_consumer);pthread_mutex_unlock(&mutex);sleep(1); // 模拟生产耗时}return nullptr;
}void* Consumer(void* args) {std::string name = static_cast<const char*>(args);while (true) {pthread_mutex_lock(&mutex);// 等待直到产品就绪(Ready == true)while (Ready == false) {pthread_cond_wait(&cond_consumer, &mutex);}// 消费std::cout << name << ",开始消费..." << std::endl;Ready = false;// 通知生产者pthread_cond_signal(&cond_producer);pthread_mutex_unlock(&mutex);}return nullptr;
}int main()
{// 单生产单消费pthread_t p; /* 生产者 */pthread_t c; /* 消费者 */pthread_create(&p, nullptr, Produce, (void*)"thread-p");pthread_create(&c, nullptr, Consumer, (void*)"thread-c");pthread_join(p, nullptr);pthread_join(c, nullptr);return 0;
}
#endif
相关文章:
【Linux】线程的同步与互斥
目录 1. 整体学习思维导图 2. 线程的互斥 2.1 互斥的概念 2.2 见一见数据不一致的情况 2.3 引入锁Mutex(互斥锁/互斥量) 2.3.1 接口认识 2.3.2 Mutex锁的理解 2.3.3 互斥量的封装 3. 线程同步 3.1 条件变量概念 3.2 引入条件变量Cond 3.2.1 接口认识 3.2.2 同步的…...
C++发起Https连接请求
需要下载安装openssl //stdafx.h #pragma once #include<iostream> #include <openssl/ssl.h> #include <openssl/err.h> #include <iostream> #include <string>#pragma comment(lib, "libssl.lib") #pragma comment(lib, "lib…...
Linux 内核链表宏的详细解释
🔧 Linux 内核链表结构概览 Linux 内核中的链表结构定义在头文件 <linux/list.h> 中。核心结构是: struct list_head {struct list_head *next, *prev; }; 它表示一个双向循环链表的节点。链表的所有操作都围绕这个结构体展开。 🧩 …...
[架构之美]Spring Boot集成MyBatis-Plus高效开发(十七)
[架构之美]Spring Boot集成MyBatis-Plus高效开发(十七) 摘要:本文通过图文代码实战,详细讲解Spring Boot整合MyBatis-Plus全流程,涵盖代码生成器、条件构造器、分页插件等核心功能,助你减少90%的SQL编写量…...
游戏引擎学习第270天:生成可行走的点
回顾并为今天的内容定下基调 今天的计划虽然还不完全确定,可能会做一些内存分析,也有可能暂时不做,因为目前并没有特别迫切的需求。最终我们会根据当下的状态随性决定,重点是持续推动项目的进展,无论是 memory 方面还…...
批量统计PDF页数,统计图像属性
软件介绍: 1、支持批量统计PDF、doc\docx、xls\xlsx页数 2、支持统计指定格式文件数量(不填格式就是全部) 3、支持统计JPG、JPEG、PNG图像属性 4、支持统计多页TIF页数、属性 5、支持统计PDF、JPG画幅 统计图像属性 「托马斯的文件助手」…...
QT Creator配置Kit
0、背景:qt5.12.12vs2022 记得先增加vs2017编译器 一、症状: 你是否有以下症状? 1、用qt新建的工程,用qmake,可惜能看见的只有一个pro文件? 2、安装QT Creator后,使用MSVC编译显示no c com…...
[架构之美]IntelliJ IDEA创建Maven项目全流程(十四)
[架构之美]IntelliJ IDEA创建Maven项目全流程(十四) 摘要:本文将通过图文结合的方式,详细讲解如何使用IntelliJ IDEA快速创建Maven项目,涵盖环境配置、项目初始化、依赖管理及常见问题解决方案。适用于Java开发新手及…...
SpringBoot学习(上) , SpringBoot项目的创建(IDEA2024版本)
目录 1. SpringBoot介绍 SpringBoot特点 2. SpringBoot入门 2.1 创建SpringBoot项目 Spring Initialize 第一步: 选择创建项目 第二步: 选择起步依赖 第三步: 查看启动类 2.2 springboot父项目 2.3 测试案例 2.3.1 数据库 2.3.2 生成代码 1. SpringBoot介绍 Spring B…...
《Python星球日记》 第51天:神经网络基础
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、引言:走进神经网络的世界二、神经元与激活函数1. 神经元&#x…...
MindSpore框架学习项目-ResNet药物分类-模型评估
目录 4.模型评估 4.1模型预测 4.1.1加载模型 4.1.2通过传入图片路径进行推理 单张图片推理代码解释 4.2图片推理 4.2.1构造可视化推理结果函数 可视化推理结果函数代码解释 4.2.2进行单张推理 参考内容: 昇思MindSpore | 全场景AI框架 | 昇思MindSpore社区…...
Visual Studio Code 前端项目开发规范合集【推荐插件】
文章目录 前言代码格式化工具(Prettier)1、下载 prettier 相关依赖:2、安装 Vscode 插件(Prettier):3、配置 Prettier(.prettierrc.cjs): 代码规范工具(ESLin…...
uniapp-商城-48-后台 分类数据添加修改弹窗bug
在第47章的操作中,涉及到分类的添加、删除和更新功能,但发现uni-popup组件存在bug。该组件的函数接口错误导致在小程序中出现以下问题:1. 点击修改肉类名称时,回调显示为空,并报错“setVal is not defined”࿰…...
OpenLayers 精确经过三个点的曲线绘制
OpenLayers 精确经过三个点的曲线绘制 根据您的需求,我将提供一个使用 OpenLayers 绘制精确经过三个指定点的曲线解决方案。对于三个点的情况,我们可以使用 二次贝塞尔曲线 或 三次样条插值,确保曲线精确通过所有控制点。 实现方案 下面是…...
uniapp小程序中实现无缝衔接滚动效果
组件滚动通知只能实现简单的滚动效果,不能实现滚动内容中的字进行不同颜色的更改,下面实现一个无缝衔接的滚动动画,可以根据自己的需要进行艺术化的更改需要滚动的内容,也可以自定义更改滚动速度。 <template><view cla…...
【Docker 新手入门指南】第四章:镜像加速
【Docker 新手入门指南】系列文章目录 【Docker 新手入门指南】第一章:前言【Docker 新手入门指南】第二章:架构概述【Docker 新手入门指南】第三章:快速安装【Docker 新手入门指南】第四章:镜像加速 文章目录 🚀【Doc…...
k8s删除pv和pvc后,vg存储没释放分析
原因是pv对应的lvm没删除 pv如下: local-068e2cac-22de-40f3-af90-efd151d043c8 100Gi RWO Retain Released sase-ops/alertmanager-kube-prometheus-stack-alertmanager-db-alertmanager-kube-prometheus-stack-alertmanager-0 …...
Ubuntu 22.04(WSL2)使用 Docker 安装 Zipkin 和 Skywalking
Ubuntu 22.04(WSL2)使用 Docker 安装 Zipkin 和 Skywalking 分布式追踪工具在现代微服务架构中至关重要,它们帮助开发者监控请求在多个服务之间的流动,识别性能瓶颈和潜在错误。本文将指导您在 Ubuntu 22.04(WSL2 环境…...
【DLF】基于语言的多模态情感分析
作者提出的不足 模态平等处理导致冗余与冲突 问题:现有MSA方法对所有模态(语言、视觉、音频)平等处理,忽略模态间贡献差异(如语言为主导模态)。后果:跨模态交互引入冗余信息(如视觉和音频中与情感无关的噪声),甚至模态对间双向信息传递(…...
window 显示驱动开发-线性伸缩空间段
线性伸缩空间段类似于线性内存空间段。 但是,伸缩空间段只是地址空间,不能容纳位。 若要保存位,必须分配系统内存页,并且必须重定向地址空间范围以引用这些页面。 内核模式显示微型端口驱动程序(KMD)必须实…...
[Linux网络_71] NAT技术 | 正反代理 | 网络协议总结 | 五种IO模型
目录 1.NAT技术 NAPT 2.NAT和代理服务器 3.网线通信各层协议总结 补充说明 4.五种 IO 模型 1.什么是IO?什么是高效的IO? 2.有那些IO的方式?这么多的方式,有那些是高效的? 异步 IO 🎣 关键缺陷类比…...
免费5个 AI 文字转语音工具网站!
一个爱代码的设计师在运营,不定时分享干货、学习方法、效率工具和AIGC趋势发展。个人网站:tomda.top 分享几个好用的文字转语音、语音转文字的在线工具,麻烦需要的朋友保存。 01. ChatTTS 中英文智能转换,语音自然流畅,在线免费…...
【入门】数字走向II
描述 输入整数N,输出相应方阵。 输入描述 一个整数N。( 0 < n < 10 ) 输出描述 一个方阵,每个数字的场宽为3。 #include <bits/stdc.h> using namespace std; int main() {int n;cin>>n;for(int in;i>1;i--){for(…...
Linux基础(文件权限和用户管理)
1.文件管理 1.1 文件权限 文件的权限总共有三种:r(可读),w(可写),x(可执行),其中r是read,w是write,x是execute的缩写。 我们…...
【BYD_DM-i技术解析】
关键词:构型、能量流、DM-i 一、发展历史:从DM1到DM5的技术跃迁 比亚迪DM(Dual Mode)技术始于2008年,其发展历程可划分为五代,核心目标始终围绕“油电协同”与“高效节能”展开: DM1…...
React Hooks 精要:从入门到精通的进阶之路
Hooks 是 React 16.8 引入的革命性特性,它让函数组件拥有了类组件的能力。以下是 React Hooks 的详细使用指南。 一、基础 Hooks 1. useState - 状态管理 import { useState } from react;function Counter() {const [count, setCount] = useState(0); // 初始值为0return …...
为什么选择 FastAPI、React 和 MongoDB?
在技术日新月异的今天,全栈开发需要兼顾效率、性能和可扩展性。FastAPI、React 和 MongoDB 这三者的组合,恰好构成了一个覆盖前后端与数据库的技术黄金三角。它们各自解决了开发中的核心痛点,同时以轻量化的设计和强大的生态系统,成为现代 Web 开发的首选方案。以下将从架构…...
01背包类问题
文章目录 [模版]01背包1. 第一问: 背包不一定能装满(1) 状态表示(2) 状态转移方程(3) 初始化(4) 填表顺序(5) 返回值 2. 第二问: 背包恰好装满3. 空间优化 416.分割等和子集1. 状态表示2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 [494. 目标和](https://leetcode.cn/proble…...
重复的子字符串
28. 找出字符串中第一个匹配项的下标 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。 示例 1&#…...
Spark MLlib网页长青
一、实验目的 1.掌握Spark SQL中用户自定义函数的编写。 2. 掌握特征工程的OneHotEncoder、VectorAssembler。 3. 熟悉决策树算法原理,能够使用Spark MLlib库编写程序 4. 掌握二分类问题评估方法 5. 能够使用TrainValidation和crossValidation交叉验证找出最佳模型。 6…...
详解多协议通信控制器
详解多协议通信控制器 在上文中,我们使用Verilog代码实现了完整的多协议通信控制器,只是讲解了具体原理与各个模块的实现代码,但是为什么这么写?这么写有什么用?模块与模块之间又是怎么连接相互作用的?今天我们就来处理这些问题。 为什么不能直接用 FPGA 内部时钟给外设?…...
JavaWeb基础
七、JavaWeb基础 javaWeb:完整技术体系,掌握之后能够实现基于B/S架构的系统 1. C/S和B/S 1.1 C/S(Client/server) C/S:客户端与服务器 本质:本地上有代码(程序在本机上)优点&#…...
localStorage和sessionStorage
localStorage和sessionStorage localStorage是指在用户浏览器中存储数据的方式,允许Web应用程序将少量的数据保存在用户设备上,便于页面之间、关闭浏览器后的数据持久化,他不会随着HTTP请求发送道服务器,减少带宽消耗,…...
c++类【高潮】
类继承 和直接复制源代码修改相比,继承的好处是减少测试。 基类:原始类, 派生类:继承类,基于基类丰富更多内容的类。 继承一般用公有继承,class 派生类名 : public 基类名{……}; 公有继承&…...
C++进阶--AVL树的实现续
文章目录 C进阶--AVL树的实现双旋AVL树的查找AVL树的检验结语 很高兴和搭大家见面,给生活加点impetus,开启今天的比编程之路!! 今天我们来完善AVL树的操作,为后续红黑树奠定基础!! 作者&#x…...
1 2 3 4 5顺序插入,形成一个红黑树
红黑树的特性与优点 红黑树是一种自平衡的二叉搜索树,通过额外的颜色标记和平衡性约束,确保树的高度始终保持在 O(log n)。其核心特性如下: 每个节点要么是红色,要么是黑色。根节点和叶子节点(NIL节点)是…...
Telnetlib三种异常处理方案
1. socket.timeout 异常 触发场景 网络延迟高或设备响应缓慢,导致连接或读取超时。 示例代码 import telnetlib import socketdef telnet_connect_with_timeout(host, port23, timeout2):try:# 设置超时时间(故意设置较短时间模拟超时)tn…...
Linux:进程间通信---消息队列信号量
文章目录 1.消息队列1.1 消息队列的原理1.2 消息队列的系统接口 2. 信号量2.1 信号量的系统调用接口 3. 浅谈进程间通信3.1 IPC在内核中数据结构设计3.2 共享内存的缺点3.3 理解信号量 序:在上一章中,我们引出了命名管道和共享内存的概念,了解…...
暗物质卯引力挂载技术
1、物体质量以及其所受到的引力约束(暗物质压力差) 自然界的所有物体,其本身都是没有质量的。我们所理解的质量,其实是物体球周空间的暗物质对物体的挤压,压力差。 对于宇宙空间中的单个星球而言,它的球周各处压力是相同的,所以,它处于平衡状态,漂浮在宇宙中。 对于星…...
JMeter 中实现 双 WebSocket(双WS)连接
在 JMeter 中实现 双 WebSocket(双WS)连接 的测试场景(例如同时连接两个不同的 WebSocket 服务或同一服务的两个独立会话),可以通过以下步骤配置: 1. 场景需求 两个独立的 WebSocket 连接(例如 …...
卡尔曼滤波算法简介与 Kotlin 实现
一、引言 卡尔曼滤波(Kalman Filter)是一种基于线性系统状态空间模型的最优递归估计算法,由鲁道夫・E・卡尔曼于 1960 年提出。其核心思想是通过融合系统动态模型预测值与传感器观测值,在最小均方误差准则下实现对系统状态的实时…...
【比赛真题解析】混合可乐
这次给大家分享一道比赛题:混合可乐。 洛谷链接:U561549 混合可乐 【题目描述】 Jimmy 最近沉迷于可乐中无法自拔。 为了调配出他心目中最完美的可乐,Jimmy买来了三瓶不同品牌的可乐,然后立马喝掉了一些(他实在是忍不住了),所以 第一瓶可口可乐最大容量为 a 升,剩余 …...
[论文阅读]BadPrompt: Backdoor Attacks on Continuous Prompts
BadPrompt: Backdoor Attacks on Continuous Prompts BadPrompt | Proceedings of the 36th International Conference on Neural Information Processing Systems 36th Conference on Neural Information Processing Systems (NeurIPS 2022) 如图1a,关注的是连续…...
DeepSeek 实现趣味心理测试应用开发教程
一、趣味心理测试应用简介 趣味心理测试是一种通过简单的问题或互动,为用户提供心理特征分析的方式。它通常包含以下功能: 测试题目展示:以问答形式呈现心理测试题。用户行为分析:根据用户的回答或选择,分析心理特征…...
计算机网络八股文--day1
从浏览器输入url到显示主页的过程? 1. 浏览器查询域名的IP地址 2. 浏览器和服务器TCP三次握手 3. 浏览器向服务器发送一个HTTP请求 4. 服务器处理请求,返回HTTP响应 5. 浏览器解析并且渲染页面 6. 断开连接 其中使用到的协议有DNS协议(…...
【计算机视觉】OpenCV实战项目:FunnyMirrors:基于OpenCV的实时哈哈镜效果实现技术解析
FunnyMirrors:基于OpenCV的实时哈哈镜效果实现技术解析 1. 项目概述2. 技术原理2.1 图像变形基础2.2 常见的哈哈镜变形算法2.2.1 凸透镜效果2.2.2 凹透镜效果2.2.3 波浪效果 3. 项目实现细节3.1 核心代码结构3.2 主要功能实现3.2.1 图像采集3.2.2 变形映射生成3.2.3…...
量子机器学习:下一代AI的算力革命与算法范式迁移——从量子神经网络到混合量子-经典架构的产业落地
一、引言:当AI遇见量子力学 2025年,全球量子计算市场规模突破200亿美元,而量子机器学习(QML)正以370%的年复合增长率(数据来源:Gartner 2024)成为最受关注的技术融合领域。传统深度…...
【数据结构】——栈
一、栈的概念和结构 栈其实就是一种特殊的顺序表,其只允许在一端进出,就是栈的数据的插入和删除只能在一端进行,进行数据的插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的元素遵循先进后出LIFO(Last InFirst O…...
Octave 绘图快速入门指南
目录 1. 基本的 2D 绘图 2. 自定义图形样式 3. 绘制散点图 4. 绘制柱状图 5. 绘制直方图 6. 3D 绘图 6.6.1 3D 曲面图 6.6.2 3D 散点图 7. 绘制极坐标 8. 多子图绘制 总结 Octave 是一个类似于 MATLAB 的开源数学软件,广泛用于数值计算和数据分析。它提供…...
RabbitMQ深入学习
继续上一节的学习,上一节学习了RabbitMQ的基本内容,本节学习RabbitMQ的高级特性。 RocketMQ的高级特性学习见这篇博客 目录 1.消息可靠性1.1生产者消息确认1.2消息持久化1.3消费者消息确认1.4消费失败重试机制1.5消息可靠性保证总结 2.什么是死信交换机…...