4. 线程同步机制(下):Event事件对象、Condition条件变量、Semaphore信号量、Barrier栅栏
好,咱们接着聊线程同步。上一章我们把互斥锁和读写锁讲透了,这一章我带你看看剩下的四个同步工具:Event、Condition、Semaphore、Barrier。说实话,这几个家伙在工控场景里用得特别多,尤其是多任务协调的时候。
我个人习惯把同步机制分成两类:一类是「保护数据」的,比如互斥锁;另一类是「协调流程」的,就是今天要讲的这些。你想想看,工控机里经常有这种情况:一个线程等着另一个线程干完某件事才能继续,或者多个线程要同时到达某个点才能一起往下走。这时候用锁?不合适。得用事件、条件变量这些东西。
4.1 Event事件对象:最简单的信号通知
Event,说白了就是一个「开关」。一个线程等着,另一个线程一拉开关,等着的那位就醒了。我在项目中遇到过用Event做急停信号处理的场景——一个线程专门监控急停按钮,按下后设置Event,其他所有工作线程看到Event被设置,立刻停止当前动作。
核心概念:
- signaled(有信号):事件被触发,等待的线程可以继续
- non-signaled(无信号):事件未触发,线程阻塞等待
- 手动重置 vs 自动重置:手动重置需要手动恢复无信号状态,自动重置触发后自动恢复
看个C++11的简单例子:
#include <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>
// 用条件变量模拟Event(C++11没有原生Event)
class Event {
public:
Event(bool manualReset = false)
: manualReset_(manualReset), signaled_(false) {}
void Set() {
std::lock_guard<std::mutex> lock(mutex_);
signaled_ = true;
if (manualReset_) {
cv_.notify_all();
} else {
cv_.notify_one();
}
}
void Reset() {
std::lock_guard<std::mutex> lock(mutex_);
signaled_ = false;
}
void Wait() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{ return signaled_; });
if (!manualReset_) {
signaled_ = false; // 自动重置
}
}
private:
bool manualReset_;
bool signaled_;
std::mutex mutex_;
std::condition_variable cv_;
};
// 使用示例
Event g_emergencyStop(false); // 自动重置事件
void workerThread() {
while (true) {
// 检查急停信号
if (g_emergencyStop.Wait()) {
// 执行急停逻辑
break;
}
// 正常工作...
}
}
我的经验:工控场景里,Event最适合做「一次性通知」。比如启动完成信号、故障触发信号。别拿它做频繁的同步,那玩意儿效率不高。
4.2 Condition条件变量:更灵活的等待机制
条件变量比Event复杂一点,但功能也更强。它允许线程等待某个条件成立,而不是单纯等一个信号。我曾经在一个多轴运动控制项目里用条件变量做「位置到达通知」——每个轴到位后通知主线程,主线程再决定下一步动作。
条件变量必须和互斥锁配合使用,这是关键。为什么?因为你要检查条件是否满足,这个检查过程必须是原子的,不能被其他线程打断。
#include <thread>
#include <condition_variable>
#include <mutex>
#include <queue>
std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(mtx);
dataQueue.push(i);
std::cout << "生产: " << i << std::endl;
}
cv.notify_one(); // 通知消费者
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件:队列不为空
cv.wait(lock, []{ return !dataQueue.empty(); });
int data = dataQueue.front();
dataQueue.pop();
lock.unlock(); // 提前解锁,提高并发
std::cout << "消费: " << data << std::endl;
if (data == 9) break;
}
}
注意:条件变量的wait()一定要用带谓词的重载版本!我见过太多人直接用wait(lock),结果发生虚假唤醒(spurious wakeup)导致程序崩溃。说白了,就算被唤醒了,也要再检查一次条件是否真的满足。
4.3 Semaphore信号量:控制并发数量
信号量,你可以把它想象成一个「令牌池」。线程要干活,先得拿一个令牌,干完活再还回去。令牌没了,后来的线程就得等着。这在工控里太常见了——比如控制同时访问某个硬件资源的线程数量。
我记得有个项目,一台工控机要控制8个伺服驱动器,但通信总线一次只能处理4个请求。我就用信号量限制并发数,效果很好。
| 信号量类型 | 特点 | 工控应用场景 |
|---|---|---|
| 计数信号量 | 资源计数,可多次获取 | 限制并发访问硬件设备 |
| 二进制信号量 | 只有0和1,类似互斥锁 | 简单互斥,但无所有权概念 |
C++20终于有了原生信号量,但很多工控项目还在用C++11/14。没关系,我们可以自己封装一个:
#include <mutex>
#include <condition_variable>
class Semaphore {
public:
Semaphore(int count = 0) : count_(count) {}
void acquire() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{ return count_ > 0; });
--count_;
}
void release() {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
cv_.notify_one();
}
private:
int count_;
std::mutex mutex_;
std::condition_variable cv_;
};
// 使用:限制最多4个线程同时访问硬件
Semaphore g_hwAccess(4);
void accessHardware() {
g_hwAccess.acquire();
// 访问硬件...
g_hwAccess.release();
}
避坑指南:我曾经在信号量的release()里忘记加锁,结果多线程环境下计数乱掉了。记住,信号量的内部计数必须受保护,别偷懒。
4.4 Barrier栅栏:多线程会合点
Barrier,也叫栅栏或屏障。它的作用就是让一组线程都到达某个点之后,再一起往下走。工控里做「同步启动」的时候特别有用——比如4个轴都要准备好,才能开始执行联动轨迹。
C++20提供了std::barrier,但如果你用老版本,可以自己实现一个简单的:
#include <mutex>
#include <condition_variable>
class Barrier {
public:
explicit Barrier(int count)
: threshold_(count), count_(count), generation_(0) {}
void wait() {
std::unique_lock<std::mutex> lock(mutex_);
int gen = generation_;
if (--count_ == 0) {
// 最后一个到达的线程,重置并唤醒所有人
++generation_;
count_ = threshold_;
cv_.notify_all();
} else {
// 等待直到generation改变
cv_.wait(lock, [this, gen]{ return gen != generation_; });
}
}
private:
int threshold_;
int count_;
int generation_;
std::mutex mutex_;
std::condition_variable cv_;
};
// 使用:4个线程同步启动
Barrier g_startBarrier(4);
void axisControl(int axisId) {
// 准备阶段...
std::cout << "轴" << axisId << "准备就绪" << std::endl;
g_startBarrier.wait(); // 等待所有轴准备好
// 所有轴同时开始运动
std::cout << "轴" << axisId << "开始运动" << std::endl;
}
关键点:Barrier的实现要注意「generation」的概念。如果线程到达后直接重置计数,可能会让下一轮的线程提前通过。用generation来区分轮次,这是标准做法。
4.5 四种同步机制对比
好了,四种工具都讲完了。我最后给你做个对比,方便你选型:
| 同步机制 | 核心用途 | 适用场景 | 注意事项 |
|---|---|---|---|
| Event | 信号通知 | 一次性事件、急停、启动信号 | 注意手动/自动重置的选择 |
| Condition | 条件等待 | 生产者-消费者、状态变化通知 | 必须配合互斥锁,用带谓词的wait |
| Semaphore | 资源计数 | 限制并发数、资源池管理 | 注意计数保护,防止溢出 |
| Barrier | 多线程同步点 | 同步启动、分阶段任务 | 注意generation管理,防止ABA问题 |
嗯,这一章内容不少。你消化一下,下一章我们讲线程池和任务队列,那又是另一个大话题了。