1. 生产者-消费者模式
生产者-消费者模式是一种通过线程安全缓冲区解耦生产者和消费者的并发设计模式。在工业视觉检测的语境下:
- 生产者:工业相机采集线程、图像预处理线程、PLC 信号接收线程等数据源。
- 消费者:缺陷检测算法线程、分类器线程、结果上报线程、MES 系统对接线程等数据处理方。
- 缓冲区:图像帧队列、检测任务队列、结果消息队列等线程安全容器。
生产者和消费者不直接通信,而是通过缓冲区间接交互。例如,相机采集线程将原始图像帧放入队列后即可继续采集下一帧,无需等待检测算法完成;检测算法线程从队列中取出图像帧进行处理,处理完毕后将结果放入另一个队列供上报线程消费。这种解耦使得采集帧率不受检测耗时影响,检测算法升级也不影响采集流程。
在工业视觉系统中,缓冲区通常采用有界队列 (如 ArrayBlockingQueue),防止图像数据堆积导致内存溢出;同时配合超时策略,避免产线异常时线程永久阻塞。
2. 模式结构
在工业视觉检测系统中,生产者-消费者模式通常以多级流水线的形式出现。以典型的 AOI(自动光学检测)系统为例,其结构如下:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 相机采集 │───▶│ 图像帧队列 │───▶│ 预处理线程 │
│ (生产者1) │ │ (缓冲区1) │ │ (消费者1) │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ MES上报 │◀───│ 结果消息队列 │◀───│ 缺陷检测 │
│ (消费者3) │ │ (缓冲区3) │ │ (生产者2) │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ 预处理队列 │
│ (缓冲区2) │
└─────────────┘
- 缓冲区1(图像帧队列):存储原始相机帧,容量通常设为 2~4 帧,避免内存爆炸。
- 缓冲区2(预处理队列):存储经过降噪、增强、ROI 裁剪后的图像。
- 缓冲区3(结果消息队列):存储检测结果(OK/NG、缺陷坐标、分类标签等),供 MES 系统消费。
每个环节的线程既是一个消费者(消费上一级数据),又是一个生产者(产出下一级数据),形成多级流水线。
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <opencv2/opencv.hpp>
// 有界缓冲区(线程安全)
template<typename T>
class BoundedBuffer {
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable not_full_;
std::condition_variable not_empty_;
size_t capacity_;
public:
BoundedBuffer(size_t cap) : capacity_(cap) {}
void produce(T item) {
std::unique_lock<std::mutex> lock(mtx_);
not_full_.wait(lock, [this] { return queue_.size() < capacity_; });
queue_.push(std::move(item));
not_empty_.notify_one();
}
T consume() {
std::unique_lock<std::mutex> lock(mtx_);
not_empty_.wait(lock, [this] { return !queue_.empty(); });
T item = std::move(queue_.front());
queue_.pop();
not_full_.notify_one();
return item;
}
size_t size() {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.size();
}
};
// 数据结构
struct Frame {
int camera_id;
long long timestamp;
cv::Mat image;
};
struct InspectionResult {
int frame_id;
bool is_ok;
std::string defect_type;
float confidence;
};
// 相机采集线程(生产者)
void camera_acquisition(BoundedBuffer<Frame>& frame_queue, int camera_id) {
cv::VideoCapture cap(camera_id); // 实际项目中替换为工业相机SDK
while (true) {
Frame frame;
frame.camera_id = camera_id;
frame.timestamp = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
cap >> frame.image; // 采集一帧
if (frame.image.empty()) break;
frame_queue.produce(std::move(frame));
std::cout << "[采集] 相机 " << camera_id << " 采集一帧,队列大小: "
<< frame_queue.size() << std::endl;
}
}
// 缺陷检测线程(消费者)
void defect_inspection(BoundedBuffer<Frame>& frame_queue,
BoundedBuffer<InspectionResult>& result_queue) {
while (true) {
Frame frame = frame_queue.consume();
// 模拟缺陷检测(实际项目中调用深度学习模型或传统视觉算法)
InspectionResult result;
result.frame_id = frame.camera_id;
result.is_ok = (rand() % 10) > 2; // 模拟 80% 良品率
result.defect_type = result.is_ok ? "" : "划痕";
result.confidence = 0.95f;
// 模拟检测耗时(实际场景中可能 50~200ms)
std::this_thread::sleep_for(std::chrono::milliseconds(80));
result_queue.produce(std::move(result));
std::cout << "[检测] 帧来自相机 " << frame.camera_id
<< ",结果: " << (result.is_ok ? "OK" : "NG")
<< ",缺陷: " << result.defect_type << std::endl;
}
}
// 结果上报线程(消费者)
void result_reporting(BoundedBuffer<InspectionResult>& result_queue) {
while (true) {
InspectionResult result = result_queue.consume();
// 模拟上报 MES 系统或触发 PLC 分拣
std::cout << "[上报] 结果: " << (result.is_ok ? "OK" : "NG")
<< ",缺陷: " << result.defect_type
<< ",置信度: " << result.confidence << std::endl;
}
}
int main() {
// 创建缓冲区:图像帧队列容量 4 帧,结果队列容量 100 条
BoundedBuffer<Frame> frame_queue(4);
BoundedBuffer<InspectionResult> result_queue(100);
// 启动 2 个相机采集线程(生产者)
std::thread cam1(camera_acquisition, std::ref(frame_queue), 0);
std::thread cam2(camera_acquisition, std::ref(frame_queue), 1);
// 启动 2 个检测线程(消费者)
std::thread inspector1(defect_inspection, std::ref(frame_queue), std::ref(result_queue));
std::thread inspector2(defect_inspection, std::ref(frame_queue), std::ref(result_queue));
// 启动 1 个上报线程
std::thread reporter(result_reporting, std::ref(result_queue));
cam1.join();
cam2.join();
inspector1.join();
inspector2.join();
reporter.join();
return 0;
}
关键设计要点:
- 有界队列容量:图像帧队列设为 4,防止内存被原始图像撑爆(单帧 10MB × 4 = 40MB 上限)。
- 多生产者多消费者:2 个相机线程 + 2 个检测线程,充分利用多核 CPU 并行处理。
- 三级流水线:采集 → 检测 → 上报,每级之间通过独立缓冲区解耦。
- 实际替换 :
cv::VideoCapture可替换为工业相机 SDK(如 Basler Pylon、HikRobot MVS 等);缺陷检测部分可接入 ONNX Runtime 或 TensorRT 推理引擎。
3. 优缺点
3.1 优点
- 解耦:生产者和消费者不直接依赖,只依赖于缓冲区。修改一方不会影响另一方,提高了系统的可维护性和可扩展性。
- 支持并发:生产者和消费者可以并行执行,提高了系统的整体吞吐量。
- 支持忙闲不均:缓冲区可以平衡生产者和消费者处理速度的差异。当生产者速度快时,数据暂存在缓冲区;当消费者速度快时,可以从缓冲区获取数据,避免了资源浪费或系统崩溃。
- 易于扩展:可以方便地增加或减少生产者和消费者的数量,以适应不同的负载需求。
3.2 缺点
- 增加复杂度:引入了缓冲区作为中间层,增加了系统的设计和实现复杂度。
- 潜在的性能瓶颈:如果缓冲区设计不当(如锁竞争激烈),可能成为系统的性能瓶颈。
- 数据一致性:在多生产者、多消费者场景下,需要确保缓冲区操作的线程安全性,否则可能出现数据竞争或数据不一致的问题。
- 缓冲区管理:需要合理设置缓冲区的大小。太小会导致生产者频繁阻塞,太大则可能浪费内存或导致数据延迟过高。
4. 应用场景
生产者-消费者模式在实际开发中应用非常广泛,以下是一些典型的场景:
- 线程池任务队列:线程池是典型的生产者-消费者模式。提交任务的线程是生产者,线程池中的工作线程是消费者,任务队列就是缓冲区。
- 日志系统:应用程序线程(生产者)将日志消息放入一个内存队列(缓冲区),一个或多个专门的日志写入线程(消费者)从队列中取出日志并写入文件或数据库。这避免了日志 I/O 阻塞主业务逻辑。
- 消息中间件:如 Kafka、RabbitMQ 等消息队列系统,其核心思想就是生产者-消费者模式。消息生产者将消息发送到 Broker(缓冲区),消息消费者从 Broker 订阅并消费消息。
- 管道-过滤器模式:在数据处理流水线中,每个处理步骤可以看作一个消费者(消费前一步的输出)和一个生产者(生产下一步的输入),它们之间通过管道(缓冲区)连接。
- 网络请求处理:Web 服务器接收 HTTP 请求(生产者),将请求放入请求队列(缓冲区),然后由工作线程池(消费者)处理这些请求。