videopipe学习之节点数据流转机制探索

videopipe学习之从运行面板开始》分析了VideoPipe如何通过观察者模式实现状态监控。我们看到面板上的数字实时跳动,节点状态通过钩子机制自动报告。但这里还有一个有趣的问题:监控的是状态变化,那状态变化所反映的"数据"本身,又是如何在节点间流转的呢?

1 从attach_to出发------发布订阅模式的登场

1.1 重新审视attach_to

回想1-1-1_sample.cpp中的attach_to调用:

cpp 复制代码
// 1-1-1_sample.cpp中的连接代码
yunet_face_detector_0->attach_to({file_src_0});

// attach_to的实际实现
void vp_node::attach_to(std::vector<std::shared_ptr<vp_node>> pre_nodes) {
    for (auto &pre_node: pre_nodes) {
        pre_node->add_subscriber(this->shared_from_this());  // 关键:向生产者注册
    }
}

attach_to实际上是消费者向生产者注册自己。yunet_face_detector_0对file_src_0说:"我是你的订阅者,有数据请通知我。"

1.2 订阅者列表的管理

add_subscriber做了什么?数据如何从生产者流向消费者?

cpp 复制代码
// vp_meta_publisher.cpp
void vp_meta_publisher::add_subscriber(std::shared_ptr<vp_meta_subscriber> subscriber) {
    std::lock_guard<std::mutex> guard(subscribers_lock);
    
    // 将订阅者加入列表
    subscribers.push_back(subscriber);
}

线程安全:通过subscribers_lock保护订阅者列表。

维护订阅关系:将订阅者指针存入subscribers向量。

建立映射:生产者知道"谁订阅了我的数据"。

1.3 数据如何传递

现在订阅关系建立了,但数据如何传递?这就是发布订阅模式要解决的问题:

2 发布订阅架构------装饰器模式的双重体现

2.1 节点也是多重"装饰"的结果

在分析add_subscriber时,我们发现一个有趣的问题:add_subscriber方法是vp_meta_publisher类的成员函数。但节点类vp_node为什么会有这个方法?

让我们看vp_node的定义:

cpp 复制代码
// 来自vp_node.h的实际继承关系
class vp_node: public vp_meta_publisher,    // ← 关键:继承了发布者接口
                public vp_meta_subscriber,   // ← 也继承了订阅者接口
                public vp_meta_hookable,     // ← 还有监控钩子接口
                public std::enable_shared_from_this<vp_node> {
    // ...
};

vp_meta_publisher接口赋予节点发布者能力。

vp_meta_subscriber接口赋予节点订阅者能力。

vp_meta_hookable接口赋予节点监控报告能力------上篇文章主要探索的核心。

这不正是装饰器模式的典型应用吗?节点通过继承不同的接口,被"装饰"上了不同的能力。

在这些装饰的接口中,vp_meta_publisher和vp_meta_subscriber一起构成了发布订阅模式的实现基础。

2.2 完整的装饰器视图

让我们看看VideoPipe如何通过装饰器模式为节点装配能力:

关键洞察:发布订阅模式在VideoPipe中是通过装饰器模式实现的:

装饰器模式:为节点动态添加能力(发布、订阅、监控)。

发布订阅模式:利用这些装饰的能力实现数据流转。

观察者模式:利用监控装饰实现状态报告。

2.3 发布订阅模式的完整实现

现在让我们看看发布订阅模式如何工作:

在vp_meta_publisher.h中:

cpp 复制代码
class vp_meta_publisher {
public:
    virtual void add_subscriber(std::shared_ptr<vp_meta_subscriber> subscriber) = 0;
    virtual void push_meta(std::shared_ptr<vp_objects::vp_meta> meta) = 0;  // 关键:发布方法
    
protected:
    std::vector<std::shared_ptr<vp_meta_subscriber>> subscribers;  // 订阅者列表
    std::mutex subscribers_lock;  // 线程安全保护
};

在vp_meta_subscriber.h中:

cpp 复制代码
class vp_meta_subscriber {
public:
    virtual void meta_flow(std::shared_ptr<vp_objects::vp_meta> meta) = 0;  // 关键:接收方法
};

数据流转过程:

生产者通过add_subscriber收集订阅者。

生产者通过push_meta发布数据。

订阅者通过meta_flow接收数据。

2.4 完整的故事线

现在我们可以完整理解attach_to建立的连接:`

cpp 复制代码
用户代码:yunet_face_detector_0->attach_to({file_src_0})

实际含义:
1. yunet_face_detector_0(装饰了vp_meta_subscriber)
2. 向file_src_0(装饰了vp_meta_publisher)注册
3. 建立file_src_0.subscribers列表包含yunet_face_detector_0
4. 当file_src_0有数据时,通过push_meta()推送给所有订阅者
5. 订阅者通过meta_flow()接收数据

2.5 节点的双重身份:发布者与订阅者

基于前面的分析,我们发现VideoPipe中的节点有一个关键特征:一个节点可以同时是发布者和订阅者。

这种双重身份体现在:

发布订阅模式与生产者-消费者模式的对应:

发布者 = 生产者

订阅者 = 消费者

中间节点:既是生产者又是消费者

源节点:只是生产者

目标节点:只是消费者

这种设计使得VideoPipe的管道可以灵活组合,每个节点只需要关注自己的处理逻辑,上下游关系通过发布订阅模式自动管理。

3 发布订阅的线程生命周期

理解了节点的双重身份,我们面临一个关键问题:订阅关系建立了,发布者和订阅者的能力也通过装饰器赋予了节点,但数据如何开始流动?

发布者接口(push_meta)和订阅者接口(meta_flow)只是静态的能力定义,需要有执行引擎来让这些接口真正工作。这就是VideoPipe线程模型的作用。

线程是发布订阅模式的执行引擎:

处理线程:执行订阅者能力,消费数据。

分发线程:执行发布者能力,推送数据。

没有线程,接口只是纸上谈兵。

让我们看看这些执行引擎如何创建、运行和销毁。

3.1 线程的初始化创建

从vp_node.h中我们可以看到,vp_node类包含两个线程成员:

cpp 复制代码
// 处理线程
std::thread handle_thread;
// 分发线程
std::thread dispatch_thread;

线程的创建发生在initialized()方法中。从vp_node.cpp可以看到:

cpp 复制代码
void vp_node::initialized() {
    // 在所有资源初始化后启动线程
    this->handle_thread = std::thread(&vp_node::handle_run, this);
    this->dispatch_thread = std::thread(&vp_node::dispatch_run, this);
}

关键点:

线程在initialized()方法中创建。

处理线程执行handle_run方法。

分发线程执行dispatch_run方法。

这两个方法都是vp_node类的成员函数。

至于initialized方法谁来调用,一般是vp_node的子类构造函数里调用它,比如vp_file_src_node的构造函数里调用。

3.2 源节点的特殊处理

从vp_src_node.h中我们看到,源节点重写了handle_run方法:

cpp 复制代码
// 强制在子类中重新实现
virtual void handle_run() override;

在vp_src_node.cpp中,源节点的handle_run方法抛出异常:

cpp 复制代码
void vp_src_node::handle_run() {
    throw vp_excepts::vp_not_implemented_error("必须为源节点重新实现'handle_run'方法!");
}

设计意图:这是防护设计,确保源节点不能直接使用,必须派生子类实现自己的handle_run逻辑,因为源节点是管道的起点,需要主动生成数据而不是等待输入。

3.3 目标节点的特殊处理

从vp_des_node.h中我们看到,目标节点重写了dispatch_run方法:

cpp 复制代码
// 在目标节点中什么都不做
virtual void dispatch_run() override final;

在vp_des_node.cpp中,目标节点的dispatch_run方法为空:

cpp 复制代码
// 在所有目标节点中,分发线程立即终止
void vp_des_node::dispatch_run() {
    // dispatch thread terminates immediately in all des nodes
}

设计原因:目标节点是管道的终点,不需要将数据分发给下游节点,因此分发线程可以立即终止。

3.4 线程的销毁

从vp_node.cpp中,我们看到deinitialized()方法的实现:

cpp 复制代码
void vp_node::deinitialized() {
    // 发送死亡标志
    alive = false;
    {
        std::lock_guard<std::mutex> guard(this->in_queue_lock);
        this->in_queue.push(nullptr);
        this->in_queue_semaphore.signal();
    }
    // 在vp_node中等待线程退出
    if (handle_thread.joinable()) {
        handle_thread.join();
    }
    if (dispatch_thread.joinable()) {
        dispatch_thread.join();
    }
}

线程销毁的关键步骤:

(1)设置alive = false,停止线程循环

(2)向输入队列推送nullptr作为死亡标志

(3)激活输入信号量,唤醒等待的处理线程

(4)使用join()等待线程自然结束

(5)检查线程是否可加入(joinable()),避免重复join()

3.5 源节点的销毁

从vp_src_node.cpp中,我们看到源节点的deinitialized()方法:

cpp 复制代码
void vp_src_node::deinitialized() {
    alive = false;
    gate.open();
    vp_node::deinitialized();
}

源节点特殊处理:

除了调用基类的deinitialized()方法。

还需要打开gate,确保源节点停止工作。

gate是源节点特有的控制机制(从vp_src_node.h可以看到vp_utils::vp_gate gate;)。

3.6 线程与装饰器的关系

回顾第2章的分析,vp_node通过装饰器模式获得能力:

cpp 复制代码
class vp_node: public vp_meta_publisher,    // 发布者能力
                public vp_meta_subscriber,   // 订阅者能力
                public vp_meta_hookable {    // 监控能力

线程是装饰器能力的执行引擎:

处理线程(handle_thread):执行装饰的订阅者能力。

分发线程(dispatch_thread):执行装饰的发布者能力。

没有线程,装饰器接口只是静态能力定义,无法实际工作。

4 线程执行的数据流转

线程创建后,如何体现节点的双重身份?不同类型的节点(源节点、中间节点、目标节点)通过不同的线程行为来体现它们的发布者/订阅者身份:

让我们具体看看这三种节点的线程实现差异。

4.1 处理线程:订阅者的执行引擎

因为不同类节点子类处理线程处理不同,所以分开查看,如下。

4.1.1 源节点处理线程:主动生产数据

基于vp_file_src_node.cpp,源节点的处理线程实现与中间节点完全不同。源节点是管道的起点,需要主动生成数据:

cpp 复制代码
void vp_file_src_node::handle_run() {
    cv::Mat frame;
    int video_width = 0;
    int video_height = 0;
    int fps = 0;
    std::chrono::milliseconds delta;
    int skip = 0;

    while(alive) {
        // 检查是否需要工作
        gate.knock();

        auto last_time = std::chrono::system_clock::now();
        // 尝试打开视频捕获
        if (!file_capture.isOpened()) {
            if(!file_capture.open(this->gst_template, cv::CAP_GSTREAMER)) {
                VP_WARN(vp_utils::string_format("[%s] open file failed, try again...", node_name.c_str()));
                continue;
            }
        }
        // 创建帧元数据
auto out_meta = 
    std::make_shared<vp_objects::vp_frame_meta>(resize_frame, this->frame_index, this->channel_index, video_width, video_height, fps);

if (out_meta != nullptr) {
    this->out_queue.push(out_meta);

    // 如果需要,激活handled钩子
    if (this->meta_handled_hooker) {
        meta_handled_hooker(node_name, out_queue.size(), out_meta);
    }

    // 重要!通知out_queue的消费者
    this->out_queue_semaphore.signal();
}

源节点处理线程的特点:

(1)主动生成数据:不需要等待输入信号量,主动读取视频文件。

(2)控制工作状态:通过gate.knock()检查是否需要继续工作。

(3)流信息报告:视频打开后触发流信息监控钩子。

(4)帧率控制:通过睡眠时间控制输出帧率。

(5)直接放入输出队列:处理结果直接放入out_queue。

4.1.2 中间节点处理线程:标准消费-生产模式

基于vp_node.cpp,中间节点的处理线程遵循标准的消费者-生产者模式:

cpp 复制代码
void vp_node::handle_run() {
    // 缓存用于批处理(如果需要)
    std::vector<std::shared_ptr<vp_objects::vp_frame_meta>> frame_meta_batch_cache;
    while (alive) {
        // 等待生产者,确保in_queue不为空
        this->in_queue_semaphore.wait();

        VP_DEBUG(vp_utils::string_format("[%s] 处理前,in_queue大小==>%d", node_name.c_str(), in_queue.size()));
        auto in_meta = this->in_queue.front();
        
        // 死亡标志
        if (in_meta == nullptr) {
            continue;
        }
        
        // 激活处理钩子
        invoke_meta_handling_hooker(node_name, in_queue.size(), in_meta);
        
        if (out_meta != nullptr && node_type() != p_node_type::DES) 
        {
		    VP_DEBUG(vp_utils::string_format("[%s] 放入out_queue前,out_queue大小==>%d", node_name.c_str(), out_queue.size()));
		    this->out_queue.push(out_meta);
		
		    // 激活已处理钩子
		    invoke_meta_handled_hooker(node_name, out_queue.size(), out_meta);
		
		    // 通知out_queue的消费者
		    this->out_queue_semaphore.signal();
		}

中间节点处理线程的特点:

(1)等待输入:in_queue_semaphore.wait()- 等待上游节点推送数据。

(2)获取数据:in_queue.front()- 从输入队列获取元数据。

(3)触发监控钩子:invoke_meta_handling_hooker()- 装饰的监控能力。

(4)调用处理方法:根据元数据类型调用相应的处理方法。

(5)放入输出队列:处理结果放入输出队列。

(6)触发完成钩子:invoke_meta_handled_hooker()- 装饰的监控能力。

(7)通知分发线程:out_queue_semaphore.signal()。

4.1.3 目标节点处理:终点消费模式

基于vp_screen_des_node.cpp,目标节点是管道的终点,其处理逻辑特殊:

cpp 复制代码
std::shared_ptr<vp_objects::vp_meta> 
    vp_screen_des_node::handle_frame_meta(std::shared_ptr<vp_objects::vp_frame_meta> meta) {
        VP_DEBUG(vp_utils::string_format("[%s] received frame meta, channel_index=>%d, frame_index=>%d", node_name.c_str(), meta->channel_index, meta->frame_index));
        
        cv::Mat resize_frame;
        if (this->display_w_h.width != 0 && this->display_w_h.height != 0) {                 
            cv::resize((osd && !meta->osd_frame.empty()) ? meta->osd_frame : meta->frame, resize_frame, cv::Size(display_w_h.width, display_w_h.height));
        }
        else {
            resize_frame = (osd && !meta->osd_frame.empty()) ? meta->osd_frame : meta->frame;
        }

        if (!screen_writer.isOpened()) {
            assert(screen_writer.open(this->gst_template, cv::CAP_GSTREAMER, 0, meta->fps, {resize_frame.cols, resize_frame.rows}));
        }
        screen_writer.write(resize_frame);

        // 调用基类中定义的一般工作
        return vp_des_node::handle_frame_meta(meta);
}

目标节点处理的特点:

(1)显示结果:处理结果显示在屏幕上。

(2)计算性能:在基类vp_des_node::handle_frame_meta中计算性能指标。

(3)返回nullptr:是管道终点,不返回数据给下游。

(4)分发线程立即终止:不需要推送数据。

4.2 分发线程的统一实现

4.2.1 分发线程的标准工作流程

基于vp_node.cpp,分发线程dispatch_run是所有非目标节点的发布者执行引擎:

cpp 复制代码
void vp_node::dispatch_run() {
    while (alive) {
        // 等待生产者,确保out_queue不为空
        this->out_queue_semaphore.wait();

        VP_DEBUG(vp_utils::string_format("[%s] 分发前,out_queue大小==>%d", node_name.c_str(), out_queue.size()));
        auto out_meta = this->out_queue.front();
        // 死亡标志
        if (out_meta == nullptr) {
            continue;
        }

        // 激活离开钩子
        invoke_meta_leaving_hooker(node_name, out_queue.size(), out_meta);

        // 执行推送操作
        this->push_meta(out_meta);
        this->out_queue.pop();
        VP_DEBUG(vp_utils::string_format("[%s] 分发后,out_queue大小==>%d", node_name.c_str(), out_queue.size()));
    }
}

分发线程的关键流程:

(1)等待输出:out_queue_semaphore.wait()- 等待处理线程完成

(2)获取数据:out_queue.front()- 从输出队列获取元数据

(3)触发离开钩子:invoke_meta_leaving_hooker()- 装饰的监控能力

(4)执行推送:push_meta()- 装饰的发布者能力

(5)清理队列:out_queue.pop()

4.2.2 发布者的核心实现

分发线程通过调用push_meta方法实现数据发布,该方法遍历订阅者列表并调用每个订阅者的meta_flow方法:

cpp 复制代码
void vp_meta_publisher::push_meta(std::shared_ptr<vp_objects::vp_meta> meta) {
    std::lock_guard<std::mutex> guard(subscribers_lock);
    
    // 遍历所有订阅者(通过add_subscriber注册的)
    for (auto& subscriber: subscribers) {
        // 通过订阅者的装饰接口推送
        subscriber->meta_flow(meta);
    }
}

发布订阅的核心:

(1)一对一或多对一的订阅关系。

(2)线程安全的订阅者管理。

(3)遍历订阅者列表,调用每个订阅者的meta_flow方法。

(4)线程安全保护。

4.2.3 目标节点的特殊处理

基于vp_des_node.cpp,目标节点的分发线程立即终止:

cpp 复制代码
// 在所有目标节点中,分发线程立即终止
void vp_des_node::dispatch_run() {
    // dispatch thread terminates immediately in all des nodes
}

设计原因:目标节点是管道的终点,不需要将数据分发给下游节点。

4.4 订阅者的数据接收

从vp_node.cpp中,meta_flow方法的实现展示了订阅者如何接收数据:

cpp 复制代码
void vp_node::meta_flow(std::shared_ptr<vp_objects::vp_meta> meta) {
    if (meta == nullptr) {
        return;
    }

    std::lock_guard<std::mutex> guard(this->in_queue_lock);
    VP_DEBUG(vp_utils::string_format("[%s] meta_flow前,in_queue大小==>%d", node_name.c_str(), in_queue.size()));
    this->in_queue.push(meta);

    // 激活到达钩子(装饰的监控能力)
    invoke_meta_arriving_hooker(node_name, in_queue.size(), meta);
    
    // 通知in_queue的消费者
    this->in_queue_semaphore.signal();
    VP_DEBUG(vp_utils::string_format("[%s] meta_flow后,in_queue大小==>%d", node_name.c_str(), in_queue.size()));
}

订阅者接收流程:

(1)放入队列:in_queue.push(meta)- 将接收的数据放入输入队列。

(2)触发到达钩子:invoke_meta_arriving_hooker()- 装饰的监控能力。

(3)通知处理线程:in_queue_semaphore.signal()- 唤醒等待的处理线程。

4.5 完整的流转链条

结合装饰器模式和线程模型,完整的流转链条如下:

cpp 复制代码
装饰器提供能力接口 → 线程作为执行引擎 → 数据流转
├─ 订阅者能力(vp_meta_subscriber)
│  └─ 处理线程(handle_thread)执行
│     ├─ 通过meta_flow接收数据
│     ├─ 放入输入队列
│     ├─ 调用handle_frame_meta处理
│     └─ 放入输出队列
│
├─ 发布者能力(vp_meta_publisher)
│  └─ 分发线程(dispatch_thread)执行
│     ├─ 从输出队列取出数据
│     ├─ 调用push_meta推送
│     └─ 遍历订阅者列表
│
└─ 监控能力(vp_meta_hookable)
   └─ 四个钩子在关键位置触发
      ├─ 到达钩子:meta_flow中
      ├─ 处理钩子:handle_run中
      ├─ 完成钩子:handle_run中
      └─ 离开钩子:dispatch_run中

5 生产者-消费者模式与线程安全

前面我们提到节点的双重身份:发布者=生产者,订阅者=消费者。在节点内部,这种双重身份通过生产者-消费者模式实现线程间的数据传递。

节点内部的生产者-消费者链条:

cpp 复制代码
上游节点(生产者) → meta_flow() → 本节点in_queue → 处理线程(消费者)
                                     ↓
本节点out_queue ← 处理结果(生产者) ← handle_frame_meta()
                                     ↓
分发线程(消费者) → out_queue → push_meta() → 下游节点(订阅者)

双重角色的同步协调:

当节点作为消费者时:等待上游节点的push_meta()调用。

当节点作为生产者时:通过out_queue_semaphore.signal()通知分发线程。

这种设计确保了数据在节点间的平滑流转。

5.1 生产者-消费者模式

从vp_node.h可以看到队列和信号量的定义:

cpp 复制代码
// 缓存从先前节点接收的输入元数据
std::queue<std::shared_ptr<vp_objects::vp_meta>> in_queue;
// 缓存输出元数据到下一个节点
std::queue<std::shared_ptr<vp_objects::vp_meta>> out_queue;

// 用于in_queue的同步
vp_utils::vp_semaphore in_queue_semaphore;
// 用于out_queue的同步
vp_utils::vp_semaphore out_queue_semaphore;

生产者-消费者模型:

上游节点是生产者:通过meta_flow向in_queue生产数据。

本节点处理线程是消费者:从in_queue消费数据。

本节点处理线程是生产者:向out_queue生产数据。

本节点分发线程是消费者:从out_queue消费数据。

下游节点是消费者:接收通过push_meta推送的数据。

5.2 线程安全的队列管理

从vp_node.cpp可以看到队列操作的线程安全设计:

cpp 复制代码
// meta_flow中的入队操作
void vp_node::meta_flow(std::shared_ptr<vp_objects::vp_meta> meta) {
    std::lock_guard<std::mutex> guard(this->in_queue_lock);
    this->in_queue.push(meta);
    // ...
}

// handle_run中的出队操作
void vp_node::handle_run() {
    // 在while循环内部,但没有额外的锁保护pop操作
    // 因为注释说明:"there is only one thread poping data from the in_queue, we don't use lock here when poping."
    auto in_meta = this->in_queue.front();
    this->in_queue.pop();
    // ...
}

线程安全策略:

入队时加锁:多个上游节点可能同时调用meta_flow。

出队时不加锁:只有处理线程从in_queue弹出数据。

类似地:只有分发线程从out_queue弹出数据。

5.3 信号量同步机制

信号量的作用:

in_queue_semaphore:控制处理线程的等待/唤醒。

out_queue_semaphore:控制分发线程的等待/唤醒。

同步流程:

cpp 复制代码
// 生产者(上游节点)激活信号量
void vp_node::meta_flow(...) {
    this->in_queue.push(meta);
    this->in_queue_semaphore.signal();  // 通知消费者
}

// 消费者(处理线程)等待信号量
void vp_node::handle_run() {
    this->in_queue_semaphore.wait();  // 等待生产者
    // ... 处理数据
    this->out_queue_semaphore.signal();  // 通知下一个消费者
}

5.4 背压处理机制

背压(Backpressure)是流处理系统中的一种流量控制机制。当生产者(上游)生产数据的速度 > 消费者(下游)处理数据的速度时,系统需要处理这种速度不匹配的问题,避免内存溢出(队列无限增长)、系统崩溃和数据丢失。

很显然,videpipe没有背压处理机制。

5.5 线程安全的订阅者管理

从之前的分析可知,发布者的线程安全设计:

cpp 复制代码
class vp_meta_publisher {
protected:
    std::vector<std::shared_ptr<vp_meta_subscriber>> subscribers;
    std::mutex subscribers_lock;  // 保护订阅者列表
    
    void add_subscriber(...) {
        std::lock_guard<std::mutex> guard(subscribers_lock);
        subscribers.push_back(...);
    }
    
    void push_meta(...) {
        std::lock_guard<std::mutex> guard(subscribers_lock);
        for (auto& sub: subscribers) {
            sub->meta_flow(...);
        }
    }
};

线程安全保证:

添加订阅者和遍历订阅者使用同一把锁。

避免在遍历过程中订阅者列表被修改。

确保发布数据时订阅者列表的一致性和完整性。

6 完整的数据流转时序

6.1 从attach_to到数据流的完整过程

6.2 线程模型的完整视图

6.3 数据流转的关键路径

端到端数据流:

cpp 复制代码
file_src_0 (生产者)
    ├─ 读取帧 → handle_run处理 → 放入out_queue
    ├─ dispatch_run分发 → push_meta推送
    └─ 遍历订阅者 → 调用meta_flow
    
yunet_face_detector_0 (消费者)
    ├─ meta_flow接收 → 放入in_queue
    ├─ handle_run处理 → 放入out_queue
    ├─ dispatch_run分发 → push_meta推送
    └─ 遍历订阅者 → 调用meta_flow
    
sface_face_encoder_0 (消费者)
    ├─ meta_flow接收 → 放入in_queue
    ├─ handle_run处理 → 放入out_queue
    └─ ... 继续传递

7 设计模式协同全景

7.1 完整的装饰器层次

通过两篇文章的分析,我们现在可以看到VideoPipe完整的装饰器层次:

7.2 与上一篇的装饰器模式对比

上一篇重点分析了监控装饰器,本篇重点分析数据流转装饰器,但它们都通过多重继承装饰在同一个vp_node上。

7.3 五种设计模式的协同

设计模式的完整协同视图:

VideoPipe通过五种设计模式的精妙协同,构建了完整的流处理系统:

(1)装饰器模式:为节点动态添加能力。

(2)发布订阅模式:管理节点间数据流转。

(3)观察者模式:实现状态监控和报告。

(4)组合模式:构建可视化拓扑结构。

(5)RAII模式:确保资源安全管理。

7.4 设计模式如何支持节点的双重身份

五种设计模式协同工作,完美支持了节点的双重身份:

7.4.1 装饰器模式:为节点动态添加发布者和订阅者能力

中间节点:获得两种能力接口

源节点:主要获得发布者能力

目标节点:主要获得订阅者能力

7.4.2 发布订阅模式:管理节点间的数据流转

发布者推送数据给所有订阅者

支持一对多的通信模式

松耦合的节点连接

7.4.3 线程模型:执行装饰器接口能力

处理线程执行订阅者逻辑

分发线程执行发布者逻辑

不同节点类型有不同线程行为

7.4.4 生产者-消费者模式:节点内部的数据缓冲

输入队列:上游生产者,本节点消费者

输出队列:本节点生产者,下游消费者

信号量控制生产消费速度

7.4.5 观察者模式:监控节点状态变化

监控数据到达、处理、完成、离开

实时反馈节点的双重角色工作状态

7.4.6 模式协同支持的双重身份:

装饰器模式定义"能做什么"(发布/订阅能力)。

发布订阅模式定义"如何交互"(节点间通信)。

线程模型定义"如何执行"(能力执行引擎)。

生产者-消费者模式定义"如何缓冲"(内部数据流)。

观察者模式定义"如何监控"(状态可观测性)。

7.5 模式协同的工作流

相关推荐
憧憬成为java架构高手的小白1 小时前
n8n学习(基于b站秋芝2046)
学习
星幻元宇VR2 小时前
VR大空间:沉浸式数字体验引领新时代科普与教育升级
科技·学习·安全·vr·虚拟现实
zhonghaoxincekj2 小时前
轴距可调式元器件双边无损成形钳
经验分享·科技·深度学习·学习·测试工具·创业创新·制造
爱喝水的鱼丶3 小时前
SAP-ABAP:ABAP Development Tools(ADT)安装配置学习分享教程(四篇连载) 第三篇:ADT常用开发插件与个性化配置教程
数据库·学习·sap·abap
GHL2842710903 小时前
python通过API调用Coze智能体学习
学习·ai
Jackyzhe3 小时前
从零学习Kafka:生产者压缩
分布式·学习·kafka
爱喝水的鱼丶4 小时前
SAP-ABAP:ABAP Development Tools(ADT)安装配置学习分享教程(四篇连载) 第二篇:ADT客户端完整安装与初始配置教程
运维·开发语言·学习·sap·abap
m0_46644103詹湛4 小时前
FPGA时序优化与高速接口实战手册
笔记·学习·fpga开发·硬件架构·verilog
wuxinyan1235 小时前
工业级大模型学习之路011:RAG 零基础入门教程(第七篇):查询优化技术
人工智能·学习·rag