一、数据通信
在把节点类型、节点和边的应用搞清楚后,就自然的引出下一个问题,前后节点间的数据通信方式,是向下一个节点推送还是由下一个节点向上一节点拉拉取呢?这也和消息队列中常见的推拉方式的选取一致。还有,当消息到达节点后,节点如何处理此消息,是存储后转发还是丢弃呢?特别是在实际的应用场景中,这非常有用。本文就对这些问题进行说明。
二、节点通信的方式
在TBB框架中,边负责各个节点间的数据流向和数据通信的方式控制。一般来说,当前节点和后继节点的是以推送的方式将数据从前向后按策略传递。而这种策略有两种形式,即 Single-push和Broadcast-push。也就是单推和广播推送。所谓单推,其实就是只有一对一,即一个前方节点只能将一条消息传送给一个后继节点,而不管其有多少个后继节点。有点阅后即焚的感觉。而广播推送就比较好理解了,前方节点会把消息推送到所有连接到的后继节点(即边的控制流图走向)让其接收。
下面看一个TBB官网的例子:
using namespace oneapi::tbb::flow;
std::atomic<size_t> g_cnt;
struct fn_body1 {
std::atomic<size_t> &body_cnt;
fn_body1(std::atomic<size_t> &b_cnt) : body_cnt(b_cnt) {}
continue_msg operator()( continue_msg /*dont_care*/) {
++g_cnt;
++body_cnt;
return continue_msg();
}
};
void run_example1() { // example for Flow_Graph_Single_Vs_Broadcast.xml
graph g;
std::atomic<size_t> b1; // local counts
std::atomic<size_t> b2; // for each function _node body
std::atomic<size_t> b3; //
function_node<continue_msg> f1(g,serial,fn_body1(b1));
function_node<continue_msg> f2(g,serial,fn_body1(b2));
function_node<continue_msg> f3(g,serial,fn_body1(b3));
buffer_node<continue_msg> buf1(g);
//
// single-push policy
//
g_cnt = b1 = b2 = b3 = 0;
make_edge(buf1,f1);
make_edge(buf1,f2);
make_edge(buf1,f3);
buf1.try_put(continue_msg());
buf1.try_put(continue_msg());
buf1.try_put(continue_msg());
g.wait_for_all();
printf( "after single-push test, g_cnt == %d, b1==%d, b2==%d, b3==%d\n", (int)g_cnt, (int)b1, (int)b2, (int)b3);
remove_edge(buf1,f1);
remove_edge(buf1,f2);
remove_edge(buf1,f3);
//
// broadcast-push policy
//
broadcast_node<continue_msg> bn(g);
g_cnt = b1 = b2 = b3 = 0;
make_edge(bn,f1);
make_edge(bn,f2);
make_edge(bn,f3);
bn.try_put(continue_msg());
bn.try_put(continue_msg());
bn.try_put(continue_msg());
g.wait_for_all();
printf( "after broadcast-push test, g_cnt == %d, b1==%d, b2==%d, b3==%d\n", (int)g_cnt, (int)b1, (int)b2, (int)b3);
}
其输出为:
after single-push test, g_cnt == 3, b1==3, b2==0, b3==0
after broadcast-push test, g_cnt == 9, b1==3, b2==3, b3==3
上面的代码中,通过结果可以与文档说明对应,单推时,只有一个节点得到了消息而广播时,则每广播一次后继的三个节点都会收到消息,所以后继节点会收到九条数据。这里需要注意的是,TBB不支持指定的单推。一般来说只有缓冲(持有和转发的)节点会拥有单推策略,而其它节点一般都有广播推送的策略。
同样,换一种场景,如果后继节点无法接收和处理其前继节点的消息,那么,TBB还支持从推到拉的操作,即消息不再由前继节点推送而是由后继节点拉取消息。它非常适合于如果后继节点很轻松的情况,闲得没事就可以找点儿事儿做。
三、节点间数据任务的处理
在图中,一般节点间是通过边来连接的,边意味着节点间的依赖关系,这也是图的特点。而在边的连接中,又可以根据实际情况来进行推拉的操作。而刚刚又分析过消息的单推和广播推送,而这些情况都取决于节点的类型。特别是节点中对消息管理的方式,也就是对任务中数据的管理方式。一般来说对所有的消息系统都有常见的两种情况:
1、节点缓存消息(数据),然后根据情况转发
2、节点丢弃消息
第一种情况很好理解,它可能需要使用make_edge()来连接需要转发的后继节点,那么节点消息则推送到所有的连接后继节点;同样也可以使用try_get()或try_reserve()来拉取消息,不过,这种情况,在消息被拉取后,则会在存储此消息的节点上将消息删除。
第二种情况其实就是一种删除消息的方式,如果节点确实想把消息删除丢弃,则只需要将其连接到一个无法推送消息的缓冲节点即可。
这个容易理解就不再举例了。
四、总结
在TBB中,可以理解任务分解、处理和并行这种机制开放给了应用开发者,而并行的实现和并行间的同步以及与实际线程和缓冲等数据处理,由TBB框架自行完成。这样,开发者更专注于实际的业务而对底层的并行实现透明处理。它的优势在于抽象隔离了底层并发的复杂性,实现了业务层面的聚焦。不过,这种方式的实现需要有一个前提,需要开发者对并行及其相关的实现有相当的了解,否则在遇到复杂的业务逻辑时,出现各种问题反而更不容易解决。
总之,事物总有其两面性,辩证的看问题吧。