为什么需要线程同步?
问题1:回到抢票逻辑中,如果一个线程频繁申请到资源(持有锁),临界资源一直满足这一个线程,会导致其他线程长时间得不到资源(饥饿问题),如果只有互斥,那么线程谁运行就取决于调度器,随机性很强导致其他线程得不到调度
问题2:频繁检测资源有无就绪,频繁申请锁,释放锁,相当于申请锁检测票数是否大于0,而如果当前并没有放票,则线程就是在做无用功,白白浪费系统资源
这两种行为均没有错(没有线程安全问题),但是不合理
解决方案:因此需要线程同步解决这个问题——访问临界资源的合理性的问题
线程同步方案:刚刚申请到锁的人,释放锁之后不可以立刻申请锁,必须去队列尾部排队,所有线程申请资源都要排队,让我们访问临界资源具有一定的顺序性, 这就是线程同步
解决问题1:改进抢票逻辑就是在抢票的过程中,除了同步互斥策略还要加上同步策略,同步策略在抢票逻辑中体现为一个线程申请到锁资源,抢到了一张票,释放锁资源后,不要立刻申请锁了,而是去队列尾部排队
解决问题2:如果申请锁资源检测票数是否大于0,发现临界资源不满足,线程就不要频繁申请了,而是进行等待,等待条件满足,有人来通知你,你可以去申请资源
同步的意义:让执行具有一定的顺序性
线程同步方案Ⅰ 条件变量
条件变量的意义
申请票,票数是否满足条件,检测临界资源是否满足条件的本质也是在访问临界资源,因此对临界资源的访问也是要再加锁和解锁之间的,在临界区进行对临界资源进行判断和申请
当条件不满足时,引入条件变量,他可以做到
临界资源不就绪,线程不要频繁检测,而是释放锁(由条件变量让你释放锁,你不用手动释放),进行等待,等别人检测到资源就绪了
条件就绪,通知线程,让他来进行资源的申请和访问
条件变量的接口
初始化
int pthread_cond_init ( pthread_cond_t *restrict cond , const pthread_condattr_t *restrict attr); 参数: cond:要初始化的条件变量 ,在栈上定义的 attr:NULL
pthread_cond_t cond =PTHREAD_COND_INITIALIZER 全局or静态
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待
在临界区中检测条件变量不满足,就开始等待,在特定条件变量下等(第一个参数),保证数据安全,互斥要使用条件变量必须有一把锁(第二个参数)
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: cond:要在这个条件变量上等待 mutex:互斥量
让特定线程在条件变量下等,默认该线程在执行的时候,wait代码被执行,当前线程立即被阻塞
pcb状态由r——>s
当前线程放到某些队列中等待,该队列由pcond提供
只有当特定条件满足了,把你的状态改为r,放入运行队列
唤醒
条件满足,会有人给我发通知,说明条件就绪,将我唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);
//将所有在等待资源就绪的线程全部唤醒
int pthread_cond_signal(pthread_cond_t *cond); //通过该条件变量唤醒指定线程,说明条件已经就绪,你可以后续操作,比如申请锁了
mutex:加锁/解锁
cond: 等待/唤醒
我们可以设计用主线程控制其他线程的代码,signal函数不关心哪些线程在条件变量下排队,只关心在把条件变量由无效设为有效时,从在条件变量下等的线程中挑一个线程唤醒他(队列,有序唤醒),执行任务,执行完之后如果还想执行,释放完锁之后再去排队
主线程控制其他线程执行代码,当主线程想要让其他线程退出,可以修改设置好的全局变量的状态,并且要注意需要将其他线程唤醒一次,让线程执行后续退出的逻辑0
编写代码,创建五个线程,各自执行自己的函数,给线程传入的数据是线程名,线程各自要执行的函数,线程需要的锁和条件变量,如果不给线程传入回调函数的参数,我们也可以设计让线程去入口函数内部根据自己的线程名拿到自己要执行的函数
代码样例
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#define TNUM 4
typedef void (*func_t)(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
class ThreadData
{
public:
ThreadData(const std::string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
:name_(name), func_(func), pmtx_(pmtx), pcond_(pcond)
{}
public:
std::string name_;
func_t func_;
pthread_mutex_t *pmtx_;
pthread_cond_t *pcond_;
};
void func1(const std::string &name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
// wait一定要在加锁和解锁之间进行wait!
// v2:
pthread_mutex_lock(pmtx);
// if(临界资源是否就绪-- 否) pthread_cond_wait
pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞
std::cout << name << " running -- 播放" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func2(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞
if(!quit) std::cout << name << " running -- 下载" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func3(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞
std::cout << name << " running -- 刷新" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func4(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞
std::cout << name << " running -- 扫码用户信息" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void *Entry(void *args)
{
ThreadData *td = (ThreadData*)args; //td在每一个线程自己私有的栈空间中保存
td->func_(td->name_, td->pmtx_, td->pcond_); // 它是一个函数,调用完成就要返回!
delete td;
return nullptr;
}
int main()
{
pthread_mutex_t mtx;//定义互斥锁
pthread_cond_t cond;//定义条件变量
pthread_mutex_init(&mtx, nullptr);//初始化互斥锁
pthread_cond_init(&cond, nullptr);//初始化条件变量
pthread_t tids[TNUM];//4个线程
func_t funcs[TNUM] = {func1, func2, func3, func4};//线程要执行的函数
for (int i = 0; i < TNUM; i++)
{
std::string name = "Thread ";
name += std::to_string(i+1);
ThreadData *td = new ThreadData(name, funcs[i], &mtx, &cond);
pthread_create(tids + i, nullptr, Entry, (void*)td);
}
sleep(5);
// ctrl new thread
int cnt = 10;
while(cnt)
{
std::cout << "resume thread run code ...." << cnt-- << std::endl;
pthread_cond_signal(&cond);
// pthread_cond_broadcast(&cond);
sleep(1);
}
std::cout << "ctrl done" << std::endl;
quit = true;
pthread_cond_broadcast(&cond);
for(int i = 0; i < TNUM; i++)
{
pthread_join(tids[i], nullptr);
std::cout << "thread: " << tids[i] << "quit" << std::endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
代码规范
等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
生产消费模型
生产消费模型介绍
条件满足时,唤醒指定线程,如何得知条件是否满足?
生产消费模型:供货商把商品提供给超市,超市是集中交易场所,消费者从超市购买商品
超市是商品的缓冲区,把生产者生产的数据存放在超市,合适的时候用户取走
两种角色:生产者,消费者
一个交易场所:超市(缓冲区)共享资源,为了保证生产和消费的安全性,需要研究三者的关系
生产者和生产者是竞争互斥关系(因为竞争所以需要互斥)
消费者和消费者是竞争互斥关系
生产者和消费者是互斥,同步关系(编写代码时要让生产和消费的过程具有一定顺序性(互斥和同步))
互斥体现在:一个线程在写数据,另一个不要拿数据,保证写入的原子性
同步体现在:生产完再消费(通知),消费完再生产(通知)
编写代码时要维护好这(321原则):三种关系,两种角色,一个交易场所
工程师思维模式下的生产消费模型:
生产者和消费者这两种角色由线程承担,线程角色化,交易场所就是某种数据结构对应的缓冲区(阻塞队列/环形队列......)商品就是数据,如何通知?生产完的行为生产者最清楚,生产者通知消费者来消费
消费完的行为消费者最清楚,消费者通知生产者来生产
我们要编写的多生产多消费模型需要维护三种关系,单生产和单消费只需要维护好生产者和消费者的互斥和同步
条件变量中的参数锁的意义:在wait时重新申请和释放锁
多生产多消费的意义:一旦消费者取得数据就可以处理数据,生产数据也需要花费很多时间,数据一般时从网络中来,如果任务不难处理不难获得,那么多生产多消费便没有意义
基于阻塞队列的生产消费模型类似于管道的原理,但是管道在内部做好了互斥与同步的工作
RAII方式的加锁
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t *mtx):pmtx_(mtx)
{}
void lock()
{
std::cout << "要进行加锁" << std::endl;
pthread_mutex_lock(pmtx_);
}
void unlock()
{
std::cout << "要进行解锁" << std::endl;
pthread_mutex_unlock(pmtx_);
}
~Mutex()
{}
private:
pthread_mutex_t *pmtx_;
};
// RAII风格的加锁方式
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx):mtx_(mtx)
{
mtx_.lock();
}
~lockGuard()
{
mtx_.unlock();
}
private:
Mutex mtx_;
};
利用了lockGuard创建的时候自动调用构造函数,初始化mtx_,mtx_是一个自定义类型,会去调用他的构造函数,利用你传进来的那把锁的地址,并且他还会调用lock进行加锁,相当于创建lockGuard就枷锁了,退出了lockGuard的作用域会自动调用他的析构函数,进行解锁
编写阻塞队列
成员变量
std::queue<T> bq_; // 阻塞队列
int capacity_; // 容量上限
pthread_mutex_t mtx_; // 通过互斥锁保证队列安全
pthread_cond_t Empty_; // 用它来表示bq 是否空的条件
pthread_cond_t Full_; // 用它来表示bq 是否满的条件
阻塞队列初始化
BlockQueue(int capacity = gDefaultCap) : capacity_(capacity)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&Empty_, nullptr);
pthread_cond_init(&Full_, nullptr);
}
3. 阻塞队列析构(处理他的资源:锁和条件变量都需要destory)
~BlockQueue()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&Empty_);
pthread_cond_destroy(&Full_);
}
生产者往队列投入数据
void push(const T &in) // 生产者,in为输入型参数
{
// pthread_mutex_lock(&mtx_);
// //1. 先检测当前的临界资源是否能够满足访问条件
// // pthread_cond_wait: 我们竟然是在临界区中!我是持有锁的!如果我去等待了,锁该怎么办呢?
// // pthread_cond_wait第二个参数是一个锁,当成功调用wait之后,传入的锁,会被自动释放!
// // 当我被唤醒时,我从哪里醒来呢??从哪里阻塞挂起,就从哪里唤醒, 被唤醒的时候,我们还是在临界区被唤醒的啊
// // 当我们被唤醒的时候,pthread_cond_wait,会自动帮助我们线程获取锁
// // pthread_cond_wait: 但是只要是一个函数,就可能调用失败
// // pthread_cond_wait: 可能存在 伪唤醒 的情况
// while(isQueueFull()) pthread_cond_wait(&Full_, &mtx_);
// //2. 访问临界资源,100%确定,资源是就绪的!
// bq_.push(in);
// // if(bq_.size() >= capacity_/2) pthread_cond_signal(&Empty_);
// pthread_cond_signal(&Empty_);
// pthread_mutex_unlock(&mtx_);
lockGuard lockgrard(&mtx_); // 自动调用构造函数
while (isQueueFull())
pthread_cond_wait(&Full_, &mtx_);
// 2. 访问临界资源,100%确定,资源是就绪的!
bq_.push(in);
pthread_cond_signal(&Empty_);
} // 自动调用lockgrard 析构函数
消费者消费数据
void pop(T *out)//输出型参数
{
lockGuard lockguard(&mtx_);
// pthread_mutex_lock(&mtx_);
while (isQueueEmpty())
pthread_cond_wait(&Empty_, &mtx_);
*out = bq_.front();
bq_.pop();
pthread_cond_signal(&Full_);
// pthread_mutex_unlock(&mtx_);
}
编写生产者消费者要执行的代码
主逻辑
构建缓冲区交易场所,定义类型,初始化生产消费线程,线程join,delete交易场所
BlockQueue<Task> *bqueue = new BlockQueue<Task>();//构建阻塞队列充当缓冲区
pthread_t c[2],p[2];
pthread_create(c, nullptr, consumer, bqueue);//生产者,消费者各自执行各自的代码,他们看到的同一份资源(消费场所阻塞队列)通过参数传进来
pthread_create(c + 1, nullptr, consumer, bqueue);
pthread_create(p, nullptr, productor, bqueue);
pthread_create(p + 1, nullptr, productor, bqueue);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
delete bqueue;
编写生产线程
int myAdd(int x, int y)
{
return x + y;
}
void* productor(void *args)
{
BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
// int
// int a = 1;
while(true)
{
// 制作任务 -- 不一定是从生产者来的
int x = rand()%10 + 1;
usleep(rand()%1000);
int y = rand()%5 + 1;
// int x, y;
// std::cout << "Please Enter x: ";
// std::cin >> x;
// std::cout << "Please Enter y: ";
// std::cin >> y;
Task t(x, y, myAdd);
// 生产任务
bqueue->push(t);
// 输出消息
std::cout <<pthread_self() <<" productor: "<< t.x_ << "+" << t.y_ << "=?" << std::endl;
sleep(1);
}
return nullptr;
}
3. 编写消费线程
int myAdd(int x, int y)
{
return x + y;
}
void* consumer(void *args)
{
BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
while(true)
{
// 获取任务
Task t;
bqueue->pop(&t);
// 完成任务
std::cout << pthread_self() <<" consumer: "<< t.x_ << "+" << t.y_ << "=" << t() << std::endl;
// sleep(1);
}
return nullptr;
}