hello~ 很高兴见到大家! 这次带来的是Linux系统中关于线程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
一、同步
1.1 同步概念介绍
- 线程饥饿:指某个线程长期无法获取所需资源,始终得不到执行机会,一直被搁置无法运行,这种现象就是线程饥饿问题。
- 线程同步:让多个线程按照约定的顺序有序执行、依次获取所需资源,实现线程间的协调配合,避免并发时出现执行混乱、线程饥饿等问题。
1.2 抽象与概念理解

- 我们可以将线程访问临界资源打个比方:上图中的小人就相当于一个个线程,它们的目标都是访问被互斥锁保护的临界资源。由于临界资源同一时刻只允许一个线程进入,所有线程都要去抢夺进入的 "钥匙",抢到钥匙的线程才能顺利访问临界资源。线程在执行临界区代码的过程中如果被操作系统切换调度,这把 "钥匙" 也会被它一同带走,其他线程依旧无法进入。
- 我们把临界资源看作一间单人自习室,设想这样一种场景:抢到钥匙的线程只在里面停留很短时间,归还钥匙后又凭借就近的优势立刻再次抢到锁,反复独占访问权。如此一来,其他线程始终没有机会获取钥匙,也就一直无法访问临界资源,进而出现线程饥饿问题;同时这种无序争抢的方式也会让整体执行效率变低,大量线程空等资源,无法充分发挥并发执行的优势。
- 想要解决这类饥饿问题,只需要让所有线程按顺序排队等候即可。线程访问完临界资源归还钥匙后,就排到队伍末尾,让排在前面的线程依次获取钥匙、访问临界资源。这种让线程按照既定顺序有序获取资源、执行任务的机制,就是线程同步。
1.3 生产者消费者模型(CP模型)
1. 模型介绍

- 这是日常生活中很常见的场景:消费者会去超市购买商品,生产者则负责往超市里补货。消费者通常不会直接找生产者采购,毕竟单个消费者的需求量很小,生产者为单次少量需求专门生产商品,显然并不划算。
- 在这个模型中,存在生产者、消费者两种角色,彼此之间会形成三种核心关系:消费者与消费者之间是竞争关系,生产者与生产者之间同样是竞争关系,这层关系比较直观,就不过多解释。生产者与消费者之间则是同步 + 互斥的关系,同步体现在二者的执行顺序上:必须先由生产者生产出商品,消费者才能进行消费;当商品堆满超市、库存接近饱和时,生产者就要暂停生产,等待消费者消费;当超市里的商品被消费一空时,消费者则需要等待,等待生产者补货。互斥则是为了保障数据安全:不能在生产者摆放、清点商品的过程中,就让消费者同时取货,否则会导致库存统计混乱。比如生产者刚放完商品准备清点数量,消费者却提前拿走了商品,最终库存数据就会出错且无法核对。从线程层面来讲,这种多角色对同一资源的竞争关系,本质上就是互斥关系。
总结:
- 一个场所:超市,对应程序中的内存缓冲区,一般由队列、数组等数据结构实现。
- 两种角色:生产者与消费者线程。
- 三种关系 :生产者与生产者之间是互斥,消费者与消费者之间是互斥,生产者与消费者之间是互斥+同步。不过有的特殊情况
可以简记为 123 或 321 原则。
2. 模型优点
- 解耦:生产者和消费者彼此不会直接打交道,只通过超市这个中间缓冲区通信。一方的逻辑修改不会影响另一方。比如生产者从一次生产 2 个商品改成一次生产 3 个,完全不影响消费者,消费者依旧正常从超市拿自己需要的商品即可。
- 支持并发,提高效率 :生产者线程和消费者线程大部分时间可以同时运行、互不干扰。互斥只限制在操作缓冲区货架这一小段临界区代码,而生产者线程和消费者线程要执行自己的任务都是并发执行的;同步也只在两种情况触发:货架满了生产者等、货架空了消费者等。除此之外,生产和消费可以并发执行,CPU 利用率更高。
- 支持忙闲不均(削峰填谷):当生产者比较忙、生产速度很快时,多余的商品可以先放在超市缓冲区里;如果消费者消费得慢,也不会立刻影响生产者。反过来,如果消费者消费很快、生产者暂时跟不上,超市里的库存也能让消费者继续处理。缓冲区可以平衡生产和消费的速度差异,让系统运行更平稳,不会因为一方快一方慢而频繁阻塞或空转。
1.4 条件变量
1. 介绍

- 我们先来模拟一个场景:同学甲和同学乙蒙着眼玩一个放苹果的游戏,由同学乙往盘子里放苹果,同学甲从盘子里拿苹果。我们模拟一下游戏情况:同学乙在放完苹果之后,并不知道同学甲什么时候会来拿苹果,可他又想继续放下一个,所以他只能在准备放苹果时先检查盘子里有没有苹果 ------ 有苹果就不能放,没苹果才能放。而同学甲也不知道同学乙什么时候会放苹果,他只能在想拿苹果时检查盘子里有没有苹果 ------ 有苹果就拿,没有就只能空手而归。这里双方都存在一个问题:同学乙不知道同学甲何时会拿苹果,为了能尽快放下一个,他只能不断重复检查盘子;同学甲也不知道同学乙何时会放苹果,为了拿到苹果,也只能不停去查看盘子。并且盘子同一时间只能由一个同学操作(互斥)。如果同学乙抢占了先机,一直霸占着检查盘子的机会,同学甲甚至根本没有机会去查看、拿取苹果。
- 为了解决这种无效轮询、甚至饥饿 的问题,我们就引入了铃铛 + 等待队列的机制:同学乙检查盘子时,如果发现苹果还在,就不再反复检查盘子,而是主动进入等待队列里休息;等同学甲拿走苹果后,摇一下铃铛把同学乙唤醒,让他回来继续放苹果。这样就能避免无效循环和资源抢占,效率会高很多。这是1 个铃铛 + 1 个等待队列的机制。而在完整的生产者消费者模型里,我们还会用2 个铃铛 + 2 个等待队列来更完善地解决问题:一个负责唤醒生产者,一个负责唤醒消费者。这个 铃铛 + 等待队列 的组合就叫做条件变量,它是实现线程同步的重要工具。
- 条件变量 :条件变量就是 Linux 中专门实现线程等待 - 唤醒的同步机制,完美解决线程盲目循环检查(忙等)、饥饿、执行顺序混乱的问题。
2. 接口介绍
初始化和销毁

-
条件变量的初始化分为静态初始化与动态初始化两种方式。静态初始化适用于具有静态存储期的变量(如全局变量或 static 局部变量),直接使用宏 PTHREAD_COND_INITIALIZER 赋值即可;动态初始化则通过调用 pthread_cond_init() 函数在运行时完成。静态初始化的条件变量由系统自动管理,生命周期通常随进程结束自动回收,无需手动销毁;而动态初始化的条件变量在使用完毕后,必须显式调用 pthread_cond_destroy() 进行清理,以释放内核同步资源。
-
它们的使用跟互斥量的接口很像,这里就不多做介绍了。
唤醒和等待


- 其中 pthread_cond_broadcast 会唤醒等待队列中的所有线程,而 pthread_cond_signal 只会唤醒等待队列中的任意一个线程。
- pthread_cond_wait 会让线程进入条件变量的等待队列并阻塞休眠,至于该接口为什么需要传入一把互斥锁,我们会在后续内容中详细讲解。
1.5 阻塞队列
1. 介绍

- 在多线程编程中,阻塞队列是实现生产者消费者模型极为常用的数据结构。生产者线程负责向阻塞队列中投放任务,消费者线程则从阻塞队列中获取并处理任务。阻塞队列内部会通过互斥锁保证并发访问安全,并依靠条件变量实现线程的等待与唤醒机制:当阻塞队列为空时,消费者线程无法拿到任务,会自动进入等待队列休眠;反之,如果阻塞队列任务已满、没有多余空间,生产者线程就无法继续插入任务,也会进入对应的等待队列等待,直到队列出现空闲位置。
2. 代码实现
cpp
void Put(const T& in)
{
pthread_mutex_lock(&_lock);
//判断条件访问了临界区
while (_q.size() == _cap)
{
pthread_cond_wait(&_pcond, &_lock);
}
_q.push(in);
if (_q.size() >= _blockqueue_high_water)
pthread_cond_signal(&_tcond);
pthread_mutex_unlock(&_lock);
}
-
为什么要进行判断?因为阻塞队列的核心特性就是队列满时禁止生产者继续插入数据,在判断队列已满后,当前生产者线程不能再向阻塞队列中塞入数据,而是应该进入对应的等待队列中休眠,直到队列有空闲位置。
-
线程为什么要在临界区内部进行等待?因为判断队列是否满的条件访问了临界资源,为了保证临界资源的安全、避免竞态条件,条件判断和等待操作必须放在互斥锁保护的临界区内,保证执行的原子性。
-
线程进入等待队列为什么要传一把锁?因为线程在调用 pthread_cond_wait 前已经持有了互斥锁,如果不释放锁,其他线程将无法获取锁操作临界资源,进而导致死锁;pthread_cond_wait 会自动释放传入的锁,随后让线程进入等待状态;当线程被唤醒后,会自动重新竞争并持有这把锁,然后从等待位置继续执行代码,保证临界区操作的原子性与安全性。
-
判断为什么用 while 而不是 if?因为存在伪唤醒和多线程竞争问题,会导致线程被唤醒后队列状态依然不满足执行条件;if 只会判断一次,无法保证线程安全;while 会在线程被唤醒后重新检查条件,直到条件真正满足才会继续执行,是线程安全的唯一写法。
- 伪唤醒:操作系统可能在没有任何线程唤醒的情况下,自动唤醒等待的线程。OS 的问题。
- 多线程竞争:多线程竞争:当线程被唤醒后,在它真正执行操作前,可能已有其他生产者线程抢先完成了入队,再次将队列填满;此时这个刚被唤醒的线程,必须重新检查队列状态,若已满就需要再次等待。
cpp
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
const int defaultcap = 5;
template<class T>
class BlockQueue
{
public:
BlockQueue(int cap = defaultcap) : _cap(cap)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_tcond, nullptr);
_blockqueue_low_water = cap * 1 / 3;
_blockqueue_high_water = cap * 2 / 3;
}
void Put(const T& in)
{
pthread_mutex_lock(&_lock);
//判断条件访问了临界区
while (_q.size() == _cap)
{
pthread_cond_wait(&_pcond, &_lock);
}
_q.push(in);
if (_q.size() >= _blockqueue_high_water)
pthread_cond_signal(&_tcond);
pthread_mutex_unlock(&_lock);
}
void Take(T* out)
{
pthread_mutex_lock(&_lock);
while (_q.empty())
{
pthread_cond_wait(&_tcond, &_lock);
}
*out = _q.front();
_q.pop();
if (_q.size() <= _blockqueue_low_water)
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_lock);
}
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_tcond);
}
private:
std::queue<T> _q;
int _cap;
pthread_mutex_t _lock;
pthread_cond_t _pcond, _tcond;
//1. 水位线
int _blockqueue_low_water;
int _blockqueue_high_water;
};
cpp
//Main.cc
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "BlockQueue.hpp"
int gnumber = 0;
pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;
int Getnumber()
{
pthread_mutex_lock(&_lock);
int num = gnumber++;
pthread_mutex_unlock(&_lock);
return num;
}
void* ProducterRoutine(void* args)
{
int num = Getnumber();
std::string _name = "Thread-pro-" + std::to_string(num);
pthread_setname_np(pthread_self(), _name.c_str());
BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
int data = 1;
while (true)
{
//sleep(1);
bq->Put(data);
std::cout << _name << "生产:" << data++ << std::endl;
}
return nullptr;
}
void* ConsumerRoutine(void* args)
{
int num = Getnumber();
std::string _name = "Thread-con-" + std::to_string(num);
pthread_setname_np(pthread_self(), _name.c_str());
BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
int data;
while (true)
{
sleep(2);
bq->Take(&data);
std::cout << _name << "消费:" << data << std::endl;
}
return nullptr;
}
int main()
{
BlockQueue<int> bq;
//pthread_t c, p;
// pthread_create(&c, nullptr, ConsumerRoutine, &bq);
// pthread_create(&p, nullptr, ProducterRoutine, &bq);
// pthread_join(c, nullptr);
// pthread_join(p, nullptr);
pthread_t c[3], p[2];
pthread_create(c, nullptr, ConsumerRoutine, &bq);
pthread_create(c + 1, nullptr, ConsumerRoutine, &bq);
pthread_create(c + 2, nullptr, ConsumerRoutine, &bq);
pthread_create(p, nullptr, ProducterRoutine, &bq);
pthread_create(p + 1, nullptr, ProducterRoutine, &bq);
pthread_join(*c, nullptr);
pthread_join(*(c + 1), nullptr);
pthread_join(*(c + 2), nullptr);
pthread_join(*p, nullptr);
pthread_join(*(p + 1), nullptr);
return 0;
}
1.6 Cond 封装
cpp
#pragma once
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex& mutex)
{
int n = pthread_cond_wait(&_cond, mutex.Ptr());
(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;
};
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!