💻文章目录
📄前言
多线程编程互斥机制虽然能保护共享资源的安全,但同时也带来了"线程饥饿问题",而且也无法控制线程的执行顺序,为了解决这种情况,就要用到同步机制。
线程同步
概念
线程同步指的多线程环境中,为了让不同线程按照一定的顺序来执行任务,它和互斥都是为了解决多线程中的并发问题而所存在的机制,要学习同步机制,也得对互斥机制有所了解。
-
线程互斥与同步的关系:
-
线程互斥:是为了保证多个线程不会同时访问临界区(共享资源)的机制,但线程的可能会同时竞争锁,无法保证线程执行顺序,
-
线程同步:在保证线程安全的前提下,让线程按照特定顺序访问资源,从而避免线程饥饿问题。
-
-
线程同步的意义:
- 解决竞态条件: 竞态条件指的是程序因为线程执行先后问题而发生异常,同步机制就用于控制线程顺序。
条件变量
条件变量 是多线程编程中最常见的一种线程同步机制 ,虽然它与互斥量一起使用 ,但它的作用不是锁住线程,而是让线程等待(休眠),直到其他线程发出信号,然后唤醒等待的线程。
函数介绍:
- 初始化条件变量
cpp
pthread_cond_t // 条件变量的类型(POSIX)
// 函数初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
// 静态初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 参数:
- cond:指向被初始化的条件变量
- attr:指向条件变量的属性对象的指针。可设为NULL
- 等待条件变量
cpp
// 需要与mutex锁一起使用。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- 参数:
- cond: 指向需要等待的条件变量的指针,
- mutex: 指向互斥锁的指针
- 工作原理: 当线程执行到这条指令时,线程将会休眠,并自动将锁释放 。直到其他线程发出唤醒命令,然后互斥量就会恢复原样。
- 唤醒线程(条件变量)
cpp
// 唤醒单个线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒多个线程
int pthread_cond_broadcast(pthread_cond_t *cond);
- 参数:
- cond: 指向条件变量的指针
- 摧毁条件变量
cpp
int pthread_cond_destroy(pthread_cond_t *cond);
- 参数:
- cond: 指向条件变量的指针
函数的使用:
cpp
#include <pthread.h>
#include <unistd.h>
#include <iostream>
#include <string>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool flag = false;
int data = 0;
void* producer(void* arg)
{
// pthread_detach(pthread_self());
std::cout << "producer is running:" << std::endl;
while(data < 100)
{
pthread_mutex_lock(&mtx);
while(flag) // while循环可防止多个线程同时唤醒时引起虚假唤醒
pthread_cond_wait(&cond, &mtx);
data++; //生产资源
std::cout << "producer: " << data << std::endl;
flag = !flag;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
}
return NULL;
}
void* consumer(void* arg)
{
// pthread_detach(pthread_self());
std::cout << "consumer is running:" << std::endl;
while(data < 100)
{
pthread_mutex_lock(&mtx);
while(!flag) // flag 和 cond 使用确保了producer先运行
pthread_cond_wait(&cond, &mtx);
//使用资源
std::cout << "consumer: " << data << std::endl;
flag = !flag;
pthread_cond_signal(&cond); //唤醒一个线程
pthread_mutex_unlock(&mtx);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
try
{
pthread_create(&tid1, NULL, producer, NULL);
pthread_create(&tid2, NULL, consumer, NULL);
}
catch (std::system_error& e)
{
std::cerr << e.what() << std::endl;
return -1;
}
pthread_join(tid1, NULL); // 回收线程资源
pthread_join(tid2, NULL);
return 0;
}
生产者-消费者模型
概念
生产者-消费者着模型是多线程编程中用于解决同步问题的一种模式,程序通过分离数据(任务)的产生和消费过程,从而提高程序的性能与可扩展性。
-
生产者与消费者的关系
- 生产者与生产者: 互斥 ------ 多个生产者不能同时向缓冲区添加元素。如果一个生产者正在执行添加操作,其他生产者必须等待,直到缓冲区可用。这是为了避免数据冲突和保证数据一致性。
- 生产者与消费者: 互斥 && 同步 ------ 生产者和消费者需要协调工作的节奏。
- 消费者与消费者: 互斥 ------ 多个消费者不能同时从缓冲区取出元素。
-
生产消费模型优点:
- 提高并发性
- 支持生成消费速度不匀
- 提高代码的可维护性
实现生产者-消费者模型
生产消费模型拥有多种实现方式,有使用基于唤醒队列(信号量)、也有基于阻塞队列(条件变量)的。本文将介绍基于阻塞队列的生产者-消费者模型。
阻塞队列
cpp
template <typename T>
class blockqueue
{
public:
blockqueue(int capacity = 5) //初始化变量
:_capacity(capacity), _p_waterline(_capacity - 2), _c_waterline(2)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_p_cond, nullptr);
pthread_cond_init(&_c_cond, nullptr);
}
bool empty() const //判断是否为空
{
return _que.empty();
}
bool full() const
{
return _que.size() == _capacity;
}
void push(const T &data) //往资源池投放资源。
{
LockGuard lock(&_mtx); //锁住线程
while (full()) //使用while循环阻止虚假唤醒
{
pthread_cond_wait(&_p_cond, &_mtx);
}
_que.push(data);
// 资源数量到底水量线
if(_que.size() >= _c_waterline) pthread_cond_signal(&_c_cond);
}
T pop() //取走资源。
{
LockGuard lock(&_mtx);
while (empty())
pthread_cond_wait(&_c_cond, &_mtx);
auto& data = _que.front();
_que.pop();
if(_que.size() <= _c_waterline) pthread_cond_signal(&_p_cond);
return data;
}
T front()
{
return _que.front();
}
~blockqueue() // 销毁资源。
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
}
private:
int _capacity; //队列容量
int _p_waterline; //生产者水位
int _c_waterline; //消费者水位
std::queue<T> _que; //队列
pthread_mutex_t _mtx; //互斥锁
pthread_cond_t _p_cond; //生产者的条件变量
pthread_cond_t _c_cond; //消费者的条件变量
};
POSIX信号量
概念
POSIX信号量是用于多进程或多线程程序协同共享资源访问的一种同步机制,
工作原理: 信号量是基于等待队列的的结构,当一个线程对值为零的信号量进行 wait 操作时,将会被阻塞,然后被放入等待队列中,直到另一个线程发出 post 信号。
函数介绍
- 初始化信号量
cpp
// 头文件: <semaphore.h>
// sem_t : 信号量结构
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:
- sem: 执行信号量结构指针
- pshared: 如果为非零值,信号量在多个进程间共享;为零则在创建它的进程的各个线程之间共享。
- value: 信号量的初始值。
- 销毁信号量
cpp
int sem_destroy(sem_t *sem);
- 等待信号量
cpp
int sem_wait(sem_t *sem);
- 发布信号量
cpp
int sem_post(sem_t *sem);
函数的使用
cpp
sem_t sem; // 定义信号量
// 线程函数1,将等待信号量
void* thread_func_wait(void* arg) {
printf("Thread 1: Waiting for the semaphore...\n");
sem_wait(&sem); // 等待信号量
printf("Thread 1: Received the semaphore signal.\n");
return NULL;
}
// 线程函数2,将释放信号量
void* thread_func_post(void* arg) {
printf("Thread 2: Sleeping for 2 seconds before releasing the semaphore...\n");
sleep(2); // 休眠2秒
printf("Thread 2: Releasing the semaphore...\n");
sem_post(&sem); // 释放信号量
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化信号量,初始值为0
sem_init(&sem, 0, 0);
// 创建线程
pthread_create(&t1, NULL, thread_func_wait, NULL);
pthread_create(&t2, NULL, thread_func_post, NULL);
// 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
其实还有基于信号量的唤醒队列存在,如果感兴趣的话,可以自己动手实现一下。
📓总结
多线程编程中的同步和互斥是为了解决并发访问共享资源造成的竞态条件问题。互斥锁是用于保护资源,而同步是为了控制资源访问的顺序。条件变量和信号量都是实现线程同步的重要工具,它们通过不同的方式控制线程对共享资源的访问。掌握这些基础知识,对于编写高效安全的多线程程序至关重要。