并行编程实战——TBB编程流图的问题

一、应用TBB出现的问题描述

在工程中应用TBB出现了一个问题,起初这个问题未暴露出来,是因为没有图像数量的统计。但在后期增加图像数量的统计后,发现最终落盘的图像比TBB输入的图像少了1~N(N的量级在十以内),一般以几帧居多,但最多偶尔也会到三四十帧。也就是TBB框架中各个节点流过的图像到最终的保存图像少了一部分。

二、解决过程

这个问题一开始并不重要,但到后来越来越重要,所以解决是必须的。思路是首先确定图像是在哪个节点丢失的?是在TBB流动过程中算法丢失的还是在落盘时保存线程丢失的。经过日志分析,发现图像从输入节点到算法节点一直到输出节点前,都是正常的。但进入输出节点后,则少了部分图像帧。而从进入输出节点的图像数量与最终落盘的图像数量帧数是完全相同的。所以可以确定(简化流程),图像的帧丢失是输入节点到输出节点间的帧丢失。
那么什么情况下会导致节点间的帧丢失呢?首先考虑的是,此处的输入节点是在一个功能节点中模拟的输入节点,通过1端口与输出节点通信,会不会是这个原因导致的?可从理论上分析,并查看TBB的文档,不应该有这种情况。如果做为一个框架无法保障节点间数据同步,那么肯定这个框架早就被抛弃了。
然后,开始怀疑是不是传输的速度太快。于是将输入节点的传输速度降低了一半,效果有所改善,甚至有几次都没有丢失的问题了,但是当把传输的数据量增大一个量级后,又出现了丢失帧的情况。所以说,这种分析可能只是触碰到了真正原因的皮毛。既然已经到了这个地步,就查看了一下相关传输的瓶颈使用的时间,结果发现时间占用非常少,这就更奇怪了(问题其实就在这里)。
最后只能分析日志并在代码中打入了一些新调试日志。在日志中发现了一些奇怪的现象。输入节点的日志和输出节点的日志有一个明显的问题,即输入节点发送出第一帧后,输出节点并不动作,当只有第二帧发送完成后,输出节点才开始处理数据并开始启动保存线程。这就是以前测试总是报丢失一帧的原因。
继续加大图像传输的数量,结果在日志中发现了端倪,当到达一个稳定的时间或数量时(二者有正比关系),输入节点和输出节点间,会有一个明显的数量统计异常。比如输入节点仍然按顺序的增长(300~310),但输出节点则停留在300不动作,直到311帧传输过来,才会继续增长。
这时忽然才想起来,在节点的创建时,有一个队列的初始化过程。同时,在生产者和消费者的控制中,一般是使用缓存队列来处理这类问题。那会不会是输入节点的速度太快(上面的占用时间少)导致输出节点处理不过来,在缓存范围内可以快速处理,但当到达一个最大队列长度时,则会有一个丢弃或者其它类似的处理机制,导致图像帧的丢失。这也是为什么在前面把输入速度降低会有一个明显的改善的过程。

三、最终方案

初步定位到了问题,就要找问题的解决方式。一般来说,这种类型的问题,都是通过同步来控制的。学习OS时,使用各种原语机制,其实是类似的。继续查找TBB的官方文档,发现了一个说明:

Always Use wait_for_all()
One of the most common mistakes made in flow graph programming is to forget to call wait_for_all. The function graph::wait_for_all blocks until all tasks spawned by the graph are complete. This is not only useful when you want to wait until the computation is done, but it is necessary to call wait_for_all before destroying the graph, or any of its nodes. For example, the following function will lead to a program failure:

void no_wait_for_all() {
    graph g;
    function_node< int, int > f( g, 1, []( int i ) -> int {
        return spin_for(i);
    } );
    f.try_put(1);


    // program will fail when f and g are destroyed at the
    // end of the scope, since the body of f is not complete
}
In the function above, the graph g and its node f are destroyed at the end of the function’s scope. However, the task spawned to execute f’s body is still in flight. When the task completes, it will look for any successors connected to its node, but by then both the graph and the node have been deleted out from underneath it. Placing a g.wait_for_all() at the end of the function prevents the premature destruction of the graph and node.

If you use a flow graph and see mysterious behavior, check first to see that you have called wait_for_all.

所以尝试对代码进行改造,首先在传送最终完成时增加wait_for_all()函数,但是发现未能解决问题。只好在每次发送后使用这个函数,结果验证不再丢帧。但这样会不会增加处理的时间呢?毕竟同步的代价大家是都知道的。然后只能用前面的测试时间的方法来处理使用这个等待函数和不使用其的时间对比。
经过测试发现结果如下:
1、未使用此等待函数,处理速度大约是200~300微秒。但它会在某个时间段有一个峰值等待,最高大约在100毫秒,注意是毫秒。一般是50毫秒左右。
2、使用此等待函数,处理速度400~500微秒。但它的峰值等待会下降到不超过20毫秒。
3、单独测试此函数使用的时间,大约在50微秒左右。
而具体的环境要求上基本以毫秒要求即可。所以这个时间的处理,有一个量级上的冗余,完全可以接受。

四、总结

有时间应该看看TBB的底层源码。其实可能看到源码,瞬间就明白了原因,可是现在只是上层应用,那么就只能靠经验和文档来处理类似的问题了。框架的运用,其实重点在于明白其内在的运行逻辑和控制流程,再加以代码辅助,文档说明,这是最佳的打手伴侣。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值