并行编程实战——TBB中的节点通信

一、数据通信

在把节点类型、节点和边的应用搞清楚后,就自然的引出下一个问题,前后节点间的数据通信方式,是向下一个节点推送还是由下一个节点向上一节点拉拉取呢?这也和消息队列中常见的推拉方式的选取一致。还有,当消息到达节点后,节点如何处理此消息,是存储后转发还是丢弃呢?特别是在实际的应用场景中,这非常有用。本文就对这些问题进行说明。

二、节点通信的方式

在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框架自行完成。这样,开发者更专注于实际的业务而对底层的并行实现透明处理。它的优势在于抽象隔离了底层并发的复杂性,实现了业务层面的聚焦。不过,这种方式的实现需要有一个前提,需要开发者对并行及其相关的实现有相当的了解,否则在遇到复杂的业务逻辑时,出现各种问题反而更不容易解决。
总之,事物总有其两面性,辩证的看问题吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值