目录
一. 线程同步的概念及功能
二. 线程同步的实现方法
2.1 条件变量相关函数
2.2 线程同步demo代码
三. 生成与消费者模型
3.1 生产与消费者模型的概念
3.2 生产与消费者模型实现代码
四. 总结
为了了解线程同步的概念及实现的功能,要先明确线程互斥的缺点。
如伪代码1.1所示的情况,在加锁和解锁之中,需要对临界资源是否满足条件进行判断,如果临界资源条件满足,才会执行有效的操作,临界资源条件长时间无法得到满足,那么就会频繁执行 加锁 -> 检测 -> 解锁的操作,在不断加锁、检测、解锁的过程中,消耗的大量的计算机资源,但是并没有做实际的工作,造成了严重的资源浪费。并且,在线程互斥的条件下,如不加以控制,很可能会存在一个线程频繁申请到锁访问临界资源的情况,这样就造成了其它线程的饥饿问题。
代码1.1:线程互斥不断加锁解锁浪费线程资源问题
void *ThreadRoutine(void *args)
{
while(true)
{
// 只有在检测到临界资源count > 0时才进行有效工作
// 如果临界资源不满足条件,那么就不断重复 上锁 -> 检测临界资源 -> 解锁 的操作
pthread_mutex_lock(&mtx); // 上锁
if(临界资源条件满足)
{
// 。。。
// 这里为有效代码
}
pthread_mutex_unlock(&mtx);
}
}
总结,单纯的线程互斥存在的缺陷有:
为了解决线程互斥的上述缺陷,线程同步被引入了进来。线程同步的功能,就是为了解决多线程在互斥的条件下,访问临界资源的合理性问题。
线程同步的概念:让多线程按照一定的顺序,访问临界资源。
通过设置条件变量的方法,可以实现线程的同步。
创建条件变量pthread_cond_t:
条件变量的初始化:
pthread_cond_init 函数 -- 初始化条件变量
函数原型:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr)
函数参数:
- cond -- 指向被初始化的条件变量的指针 (被初始化条件变量的地址) 。
- attr -- 初始化属性,一般采用默认属性,在使用的时候传nullptr。
返回值:函数调用成功返回0,失败返回非0错误码。
条件变量的销毁:
pthread_cond_destroy 函数 -- 局部变量销毁
函数原型:int pthread_cond_destroy(pthread_cond_t *cond);
函数参数:cond -- 被销毁的条件变量的地址。
返回值:成功返回0,失败返回返回非0错误码。
条件变量的等待:
pthread_cond_wait 函数 -- 等待条件变量
函数原型:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数参数:
- cond -- 设置等待的条件变量的地址。
- mutex -- 等到条件变量的线程当前持有的互斥锁的地址。
返回值:成功返回0,失败返回非0错误码。
演示代码2.1为 pthread_cond_wait 如何使用的规范版代码。我们希望,如果临界资源条件不满足,当前线程就要被阻塞,直到临界资源条件满足后再由其它线程唤醒。pthread_cond_wait要在加锁和解锁之间执行,且判断临界资源条件是否满足应当通过while循环来判断而不是if条件判断。
代码2.1:pthread_cond_wait 的规范使用方法
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局互斥锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 全局条件变量
void *ThreadRoutine(void *args)
{
while(true)
{
pthread_mutex_lock(&mtx);
// 这里一定要用while轮巡检测,而不可以用if
while(临界资源条件不满足)
{
// 设置条件变量阻塞等待
// 直到临界资源就绪被唤醒
pthread_cond_wait(&cond, &mutex);
}
// 对临界资源进行操作的有效代码位于此处
// ... ...
pthread_mutex_unlock(&mtx);
}
}
使用while轮询检测判断临界资源条件是否满足而不采用if判断的原因如下:
线程执行流因pthread_cond_wait被阻塞的时候,是拿锁阻塞的,那么,其他线程又为什么可以访问临界资源了呢?这里就要涉及到pthread_cond_wait的第二个参数了,第二个参数传的是互斥锁的地址。
当线程在pthread_cond_wait调用的位置被阻塞时,它会释放它所持有的第二个参数指向的互斥锁。同理,当该线程再次被唤醒时,又会去竞争互斥锁,以保证其获得访问临界资源的权限。如果线程被唤醒,但是互斥锁被其它线程占有,这种情况有可能存在,但不会出现问题,因为此时该线程会继续阻塞等待锁,拿到锁之后才会继续执行。
唤醒条件变量的等待:
pthread_cond_signal -- 唤醒一个等待指定条件变量的线程
函数原型:int pthread_cond_signal(pthread_cond_t *cond)
函数参数:cond -- 被唤醒的条件变量的地址。
返回值:成功返回0,失败返回非0错误码。
pthread_cond_broadcast函数 -- 唤醒全部等待某个条件变量的线程
函数原型:int pthread_cond_broadcast(pthread_cond_t *cond)
函数参数:cond -- 被唤醒的条件变量的地址。
返回值:成功返回0,失败返回非0错误码。
代码2.2创建了4个子线程,通过条件变量,设置阻塞等待,让这四个线程按照先后顺序依次运行。具体的实现方法为:
编译程序,运行代码,观察图2.1所示的运行结果,我们发现,4个线程按照先后顺序被调用了。
代码2.2:通过条件变量让多个线程依次执行
#include
#include
#include
#include
#include
#define g_PTHREAD_NUM 4
struct ThreadData
{
public:
// 构造函数
ThreadData(const std::string& name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
: _name(name), _pmtx(pmtx), _pcond(pcond)
{ }
std::string _name; // 线程名
pthread_mutex_t *_pmtx; // 互斥锁地址
pthread_cond_t *_pcond;
};
void *ThreadRoutine(void *args)
{
ThreadData* pth = (ThreadData*)args;
while(true)
{
// 加锁
pthread_mutex_lock(pth->_pmtx);
// 设置等待条件变量
pthread_cond_wait(pth->_pcond, pth->_pmtx);
// 执行线程核心代码(输出线程名称)
std::cout << "Thread Running ..., " << pth->_name << std::endl;
// 解锁
pthread_mutex_unlock(pth->_pmtx);
}
delete pth;
return nullptr;
}
int main()
{
// 创建并初始化互斥锁
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);
// 创建并初始化条件变量
pthread_cond_t cond;
pthread_cond_init(&cond, nullptr);
pthread_t tid[g_PTHREAD_NUM];
// 创建子线程
for(int i = 0; i < g_PTHREAD_NUM; ++i)
{
std::string name = "Thread ";
name += std::to_string(i + 1);
ThreadData *pth = new ThreadData(name, &mtx, &cond);
int n = pthread_create(tid + i, nullptr, ThreadRoutine, (void*)pth);
assert(n == 0);
}
// 主线程轮询唤醒子线程
while(true)
{
pthread_cond_signal(&cond);
sleep(1);
}
// 主线程等待子线程退出
for(int i = 0; i < g_PTHREAD_NUM; ++i)
{
int n = pthread_join(tid[i], nullptr);
assert(n == 0);
}
return 0;
}
如图3.1为生产与消费者模型的示意图,在该模型中,有如下的要素:
总结:生产者消费者模型具备 3种关系、2个角色、1个交易场所。
用软件工程师的思维来理解生产与消费者模型:
实现生产者与消费者之间的同步关系,就需要依靠条件变量来实现线程同步,实现消费者与消费者、生产者与生产者之间的互斥关系,就需要依靠互斥锁来实现线程互斥。
生产者与消费者模型,具有以下的优势:
如图3.2所示,采用阻塞队列的方式来实现生产者与消费者之间的线程同步,假设阻塞队列最多容纳5个数据,如果阻塞队列中数据已满,那么生产者就应当等待消费者读取数据,而如果阻塞队列中数据为空,那么消费者就应当等待生产者向阻塞队列中写数据。并且,同一时刻只允许有一个生产者向阻塞队列中写数据、一个消费者从阻塞队列中读数据,这样就模拟出来了生产者与生产者、消费者与消费者之间的互斥关系。
代码3.1和3.2一同构成了生产者与消费者的代码,在阻塞队列class type BlockQueue中,给出了写数据函数push、删除数据函数pop、判断阻塞队列是否已满函数IsFull以及判空函数IsEmpty,在main函数中为生产者和消费者个创建两个线程,定义线程函数为consume何product,实现同步式的读取数据和写入数据。
代码3.1:阻塞队列实现的头文件BlockQueue.hpp
#include
#include
#include
// 使用全局变量定义阻塞队列的默认容量
const int g_DEF_SIZE = 5;
template
class BlockQueue
{
public:
// 构造函数
BlockQueue(int capacity = g_DEF_SIZE)
: _capacity(capacity)
{
// 对互斥锁和条件变量初始化
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_full, nullptr);
pthread_cond_init(&_empty, nullptr);
}
// 判断阻塞队列是否已满的函数
bool IsFull()
{
return _capacity == _bq.size();
}
// 判断阻塞队列是否为空的函数
bool IsEmpty()
{
return _bq.empty();
}
// 写数据函数(由生产者调用)
void push(const T& val)
{
pthread_mutex_lock(&_mtx); // 加锁
// 判断阻塞队列是否已满,满了就设置条件变量进行等待
// while是为了避免函数未成功执行以及伪唤醒问题
while(IsFull())
{
pthread_cond_wait(&_full, &_mtx);
}
// 向阻塞队列中写数据
_bq.push(val);
pthread_cond_signal(&_empty); // 唤醒消费者线程读取数据
pthread_mutex_unlock(&_mtx); // 解锁
}
// 读数据函数(由消费者调用)
// 将数据读到pval所指向的地址中去
void pop(T *pval)
{
pthread_mutex_lock(&_mtx); // 加锁
// 判断阻塞队列是否为空,如为空,设置对_empty条件变量的阻塞等待
while(IsEmpty())
{
pthread_cond_wait(&_empty, &_mtx);
}
// 读取并删除队头数据
*pval = _bq.front();
_bq.pop();
pthread_cond_signal(&_full); // 唤醒生产者线程
pthread_mutex_unlock(&_mtx); // 解锁
}
private:
std::queue _bq; // 阻塞队列
int _capacity; // 阻塞队列容量
pthread_mutex_t _mtx; // 互斥锁地址
pthread_cond_t _full; // 用于标识阻塞队列已满的条件变量
pthread_cond_t _empty; // 用于标识阻塞队列为空的条件变量
};
代码3.2:ConProd.cc -- 生成消费者模型源文件代码
#include
#include
#include
#include "BlockQueue.hpp"
void *consume(void *args)
{
BlockQueue *pbq = (BlockQueue*)args;
int val = 0;
while(true)
{
pbq->pop(&val);
std::cout << "消费者获取了一个数据: " << val << std::endl;
sleep(1);
}
return nullptr;
}
void *product(void *args)
{
BlockQueue *pbq = (BlockQueue*)args;
int a = 0;
while(true)
{
pbq->push(a);
std::cout << "生产者向阻塞队列中写入一个数据:" << a++ << std::endl;
}
return nullptr;
}
int main()
{
pthread_t c[2], p[2]; // 消费者与生产者线程id
// 创建互斥锁
pthread_mutex_t mtx;
// 创建条件变量
pthread_cond_t isFull;
pthread_cond_t isEmpty;
BlockQueue *pbq = new BlockQueue();
// 创建两个生产者线程
pthread_create(c, nullptr, consume, (void*)pbq);
pthread_create(c + 1, nullptr, consume, (void*)pbq);
// 创建两个消费者线程
pthread_create(p, nullptr, product, (void*)pbq);
pthread_create(p + 1, nullptr, product, (void*)pbq);
// 主线程阻塞等待子线程退出
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
delete pbq;
return 0;
}