Files
workflow/docs/about-conditional.md
xiehan 5fbe73f5aa Add template type on WFTaskFactory::send_by_name()/signal_by_name(). (#1677)
* Add template type on WFTaskFactory::send_by_name()/signal_by_name().

* Remove unused parameter name.
2025-01-03 22:49:04 +08:00

111 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 条件任务与观察者模式
有的时候,我们需要让任务在某个条件下才被执行。条件任务(WFConditional)就是用于解决这种问题。
条件任务是一种任务包装器,可以包装任何的任务并取代原任务。通过对条件任务发送信号来触发被包装任务的执行。
# 条件任务的创建
在[WFTaskFactory.h](/src/factory/WFTaskFactory.h)里,可以看到条件任务的创建接口。
~~~cpp
class WFTaskFactory
{
public:
static WFConditional *create_conditional(SubTask *task);
static WFConditional *create_conditional(SubTask *task, void **msgbuf);
};
~~~
可以看到,我们通过工厂的create_conditional接口创建条件任务。
其中,task为被包装的任务。msgbuf是用于接收消息的缓冲区,如果无需关注消息的具体内容,msgbuf可以缺省。
WFConditional的主要接口:
~~~cpp
class WFConditional : public WFGenericTask
{
public:
virtual void signal(void *msg);
...
};
~~~
WFConditional是一种任务,所以,它满足普通workflow任务的一切属性。特别的接口只有signal,用于发送信号。
# 示例
以下示例,通过timer和conditional,实现一个延迟1秒执行的计算任务。
~~~cpp
int main()
{
WFGoTask *task = WFTaskFactory::create_go_task("test", [](){ printf("Done\n"); });
WFConditional *cond = WFTaskFactory::create_conditional(task);
WFTimerTask *timer = WFTaskFactory::create_timer_task(1, 0, [cond](void *){
cond->signal(NULL);
});
timer->start();
cond->start();
getchar();
}
~~~
这个示例里,在定时器的回调里向cond发送信号,让被包装的go task可以被执行。
注意,无论cond->signal()与cond->start()哪一个先被调用,程序都完全正确。
# 观察者模式
我们看到,如果直接对cond发送信息,需要发送者直接持有cond的指针,这在一些情况下并不是很方便。
于是,我们引入了观察者模式,也就是命名的条件任务。通过向某个名称发送信号,同时唤醒所有在这个名称下的条件任务。
命名条件任务的创建与唤醒:
~~~cpp
class WFTaskFactory
{
public:
static WFConditional *create_conditional(const std::string& cond_name, SubTask *task);
static WFConditional *create_conditional(const std::string& cond_name, SubTask *task, void **msgbuf);
static int signal_by_name(const std::string& cond_name, void *msg);
static int signal_by_name(const std::string& cond_name, void *msg, size_t max);
template<typename T>
static int signal_by_name(const std::string& cond_name, T *const msg[], size_t max);
};
~~~
我们看到,与普通条件任务唯一区别是,命名条件任务创建时,需要传入一个cond_name。
而signal_by_name()接口,默认将msg发送到所有在这个名称上等待的条件任务,将它们全部唤醒。
也可以通过max参数指定唤醒的最大任务数。此时,msg还可以是一个指针数组,可给不同的条件任务发送不同的消息。
任何一个signal_by_name的重载函数,其返回值都是表示实际唤醒的条件任务个数。
这就相当于实现了观察者模式。
# 示例
还是上面的延迟计算示例,我们增加到两个计算任务并用观察者模式来实现。用"slot1"作为条件任务名。
~~~cpp
int main()
{
WFGoTask *task1 = WFTaskFactory::create_go_task("test", [](){ printf("test1 done\n"); });
WFGoTask *task2 = WFTaskFactory::create_go_task("test", [](){ printf("test2 done\n"); });
WFConditional *cond1 = WFTaskFactory::create_conditional("slot1", task1);
WFConditional *cond2 = WFTaskFactory::create_conditional("slot1", task2);
WFTimerTask *timer = WFTaskFactory::create_timer_task(1, 0, [](void *){
WFTaskFactory::signal_by_name("slot1", NULL);
});
timer->start();
cond1->start();
cond2->start();
getchar();
}
~~~
我们看到,在这个示例里,timer在回调中通过signal_by_name方法,同时唤醒了slot1下两个计算任务。
# 使用条件任务注意事项
Workflow里的任何任务,如果创建之后不想运行,都可以通过dismiss接口直接释放。
对于条件任务,如果要被dismiss(或者在某个被cancel的series里),必须保证这个条件任务没有被signal过。
以下代码的行为无定义:
~~~cpp
int main()
{
WFEmptyTask *task = WFTaskFactory::create_empty_task();
WFConditional *cond = WFTaskFactory::create_conditional("slot1", task);
WFTimerTask *timer = WFTaskFactory::create_timer_task(0, 0, [](void *) {
WFTaskFactory::signal_by_name("slot1");
});
timer->start();
cond->dismiss(); // 取消任务
getchar();
}
~~~
显然,如果timer的callback里已经执行或正在执行了signal_by_name,cond被signal,再dismiss()是一种错误行为。
这种情况一般也只会出现在命名条件任务里。所以,dismiss一个命名条件任务,需要特别的小心。