在流水线执行中使用并行性

时间:2017-05-24 22:16:51

标签: c++ multithreading openmp tbb tbb-flow-graph

我正在尝试开发一个管道,首先读取和处理数据,操作一次,以不同方式操作,然后显示。我有一个设计,其中数据IO提供给第一个操纵器读取的缓冲区。随后,第一操纵器写入另一个缓冲器,该缓冲器在可能的情况下由第二操纵器读取。最后,第二个操纵器的输出被写入显示缓冲区,由可视化工具读取并使用OpenGL显示。

在我看来,这是一个相当简单的并行问题,其中每个任务都有自己的线程,并且它们通过数据缓冲区进行通信。但是,我遇到的所有针对线程程序的教程似乎都暗示多线程是由某些中间件(如OpenMP)决定如何划分工作负载的。

我是开发多线程应用程序的新手,所以这可能是一个愚蠢的问题,但我所描述的是可行的,可以用OpenMP等中间件来完成吗?我意识到明显的答案是"尝试一下,"我想,但是这些教程并没有说明如何*尝试它。

3 个答案:

答案 0 :(得分:1)

OpenMP更适合容易跨越多核(SIMD)的算法。其他情况是可能的,但在您的情况下,我认为直接使用线程将更好地工作,并且将更容易编码和维护。

我将我的答案分为两部分:一般解决方案没有 OpenMP,以及一些使用OpenMP的具体更改。

如评论中所述,您面临生产者/消费者问题,但两次:一个线程正在填充缓冲区(生成项目),然后必须由第二个线程读取(和修改)(消耗)。你的问题的特殊性在于,第二个线程也是一个生产者(要绘制的图像),第三个线程是负责它的人(可视化器)。

正如您所知,P / C问题是使用缓冲区(可能是循环缓冲区或生成的项目队列)解决的,其中缓冲区的每个元素都标记为生成或使用,并且线程具有独占访问权限添加或从中获取物品时。

让我们在下面的示例程序中使用队列方法解决您的问题。

  • 生产的商品将存储在队列中。队列的前面包含最旧的元素,必须首先消耗的元素。
  • 有两个队列:一个用于由第一个操纵器生成的数据(并由第二个操纵器消耗),另一个用于由第二个操纵器生成的数据(并且将由另一个线程可视化)。
  • 生产阶段很简单:获得对相应队列的独占访问权限,并在最后插入元素。
  • 消耗类似但必须等待队列至少有一个元素(不是空的)。
  • 我已添加了一些 sleeps 来模拟其他操作。
  • 停止条件仅供参考。

注意:为了简单起见,我假设您可以访问C ++ 11编译器。使用其他API的实现相对类似。

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <list>

using namespace std::chrono_literals;

std::mutex g_data_produced_by_m1_mutex;
std::list<int> g_data_produced_by_m1;

std::mutex g_data_produced_by_m2_mutex;
std::list<int> g_data_produced_by_m2;

std::atomic<bool> stop = false;

void manipulator1_kernel()
{
  while (!stop) {
    // Producer 1: generate data
    {
      std::lock_guard<std::mutex> lock(g_data_produced_by_m1_mutex);
      g_data_produced_by_m1.push_back(rand());
    }
    std::this_thread::sleep_for(100ms);
  }
}

void manipulator2_kernel()
{
  int data;

  while (!stop) {
    // Consumer 1
    while (!stop) { // wait until there is an item to be consumed
      {
        std::lock_guard<std::mutex> lock(g_data_produced_by_m1_mutex);
        if (!g_data_produced_by_m1.empty()) { // is there data to be consumed?
          data = g_data_produced_by_m1.front(); // consume
          g_data_produced_by_m1.pop_front();
          break;
        }
      }
      std::this_thread::sleep_for(100ms);
    }

    // Producer 2: modify and send to the visualizer
    {
      std::lock_guard<std::mutex> lock(g_data_produced_by_m2_mutex);
      g_data_produced_by_m2.push_back(5 * data);
    }

    std::this_thread::sleep_for(100ms);
  }
}

void visualizer_kernel()
{
  int data;

  while (!stop) {
    // Consumer 2
    while (!stop) { // wait until there is an item to be visualized
      {
        std::lock_guard<std::mutex> lock(g_data_produced_by_m2_mutex);
        if (!g_data_produced_by_m2.empty()) {
          data = g_data_produced_by_m2.front();
          g_data_produced_by_m2.pop_front();
          break;
        }
      }
      std::this_thread::sleep_for(100ms);
    }

    std::cout << data << std::endl; // render to display
    std::this_thread::sleep_for(100ms);

    if (data % 8 == 0) stop = true; // some stop condition for the example
  }
}

int main()
{
  std::thread manipulator1(manipulator1_kernel);
  std::thread manipulator2(manipulator2_kernel);
  std::thread visualizer(visualizer_kernel);

  visualizer.join();
  manipulator2.join();
  manipulator1.join();

  return 0;
}

如果您仍想使用OpenMP,可能最接近的是tasks(因为我认为是OpenMP 3.0)。我还没有使用过它们,但是上面的程序可以重写如下:

int main()
{
  #pragma omp parallel
  {
    #pragma omp task
    manipulator1_kernel();
    #pragma omp task
    manipulator2_kernel();
    #pragma omp task
    visualizer_kernel();

    #pragma omp taskwait
  }    

  return 0;
}

其余代码也可以更改为使用OpenMP功能,但我认为这可以回答您的问题。

这种方法的主要问题是您必须为OpenMP parallel中的任务创建代码块,这很容易使应用程序逻辑和结构的其余部分复杂化。

答案 1 :(得分:1)

要解决此特定问题,英特尔®线程构建模块库包含特殊结构。 Intel® TBB是跨平台库,有助于多线程编程。 我们可以在四个不同的任务提供商处查看您的应用程序中涉及的实体。一类任务是输入任务 - 提供输入数据的任务,第一个操作例程提供的另一类任务,等等。

因此,用户唯一需要做的就是为这些任务提供正文。库中有几个API用于指定要处理的实体以及如何并行执行。其他所有(这里我指的是线程创建,任务执行之间的同步,工作平衡等)都是由库完成的。

我想到的最简单的解决方案是使用parallel_pipeline函数。这是原型:

#include "tbb/pipeline.h"
using namespace tbb;

int main() {
    parallel_pipeline(/*specify max number of bodies executed in parallel, e.g.*/16,
        make_filter<void, input_data_type>(
            filter::serial_in_order, // read data sequentially
            [](flow_control& fc) -> input_data_type {
                if ( /*check some stop condition: EOF, etc.*/ ) {
                    fc.stop();
                    return input_data_type(); // return dummy value
                }
                auto input_data = read_data();
                return input_data;
            }
        ) &
        make_filter<input_data_type, manipulator1_output_type>(
            filter::parallel, // process data in parallel by the first manipulator
            [](input_data_type elem) -> manipulator1_output_type {
                auto processed_elem = manipulator1::process(elem);
                return processed_elem;
            }
        ) &
        make_filter<manipulator1_output_type, manipulator2_output_type>(
            filter::parallel, // process data in parallel by the second manipulator
            [](manipulator1_output_type elem) -> manipulator2_output_type {
                auto processed_elem = manipulator2::process(elem);
                return processed_elem;
            }
        ) &
        make_filter<manipulator2_output_type, void>(
            filter::serial_in_order, // visualize frame by frame
            [](manipulator2_output_type elem) {
                visualize(elem);
            }
        )
    );
    return 0;
}

如果实现了必要的功能(read_data,visualize)。这里input_data_typemanipulator1_output_type等是在管道阶段之间传递的类型,并且操纵器的process函数对传递的参数进行必要的计算。

BTW,为了避免使用锁和其他同步原语,您可以使用库中的concurrent_bounded_queue,并通过可能不同的线程(例如专用于IO操作)将输入数据放入此队列,就像{{1}一样简单},然后通过concurrent_bounded_queue_instance.push(elem)阅读。请注意,弹出项目是此处的阻止操作。 input_data_type elem; concurrent_bounded_queue_instance.pop(elem)提供了非阻塞concurrent_queue替代方案。

另一种可能性是使用tbb::flow_graph及其节点来组织相同的流水线方案。请查看描述dependencydata流程图的两个示例。您可能需要使用sequencer_node来正确排序项目执行(如有必要)。

有必要阅读标有标记的SO问题,以了解其他人如何使用此库。

答案 2 :(得分:0)

你有没有实现单线程版本?异形?

它们是至关重要的步骤,没有它们您可以获得高度并行设计的最佳实现,只是为了意识到瓶颈是缓冲区的I / O和/或线程同步和/或错误共享和/或缓存未命中或类似问题。

我首先尝试一个简单的线程池,其中包含按顺序完成所有步骤的任务。然后在分析它是如何工作的,什么是CPU消耗等。我将尝试更复杂的工具总是将它们的性能与第一个简单版本进行比较