在c ++中,在线程之间共享数据容器的最佳方法是什么

时间:2018-05-25 13:44:09

标签: c++ multithreading c++11

我有一个应用程序有几个处理级别,如:

InputStream->Pre-Processing->Computation->OutputStream

这些实体中的每一个都在单独的线程中运行。 所以在我的代码中我有一般的线程,它拥有

std::vector<ImageRead> m_readImages;

然后它将此成员变量传递给每个线程:

InputStream input{&m_readImages};
std::thread threadStream{&InputStream::start, &InputStream};
PreProcess pre{&m_readImages};
std::thread preStream{&PreProcess::start, &PreProcess};
...

这些类中的每一个都拥有指向此数据的指针成员:

std::vector<ImageRead>* m_ptrReadImages;

我还定义了一个全局互斥锁,我在对该共享容器的每次读/写操作时锁定和解锁。 困扰我的是这种机制非常模糊,有时我会混淆数据是否被另一个线程使用。

那么在这些线程之间共享这个容器的更直接的方法是什么?

6 个答案:

答案 0 :(得分:7)

您描述的过程&#34;输入 - &gt;预处理 - &gt;计算 - &gt;输出&#34;按顺序设计:每个步骤都依赖于前一个步骤,因此以这种特定方式并行化并不是有益的,因为每个线程只需等待另一个线程完成。试着找出哪个步骤需要花费大部分时间并将其并行化。或者尝试设置多个并行处理流水线,这些流水线按顺序在独立的单个数据集上运行。通常的方法是使用一个处理队列,在一组线程中分配任务。

答案 1 :(得分:1)

在我看来,您的阅读和预处理可以独立于容器完成。

天真地,我会将其构建为扇出,然后扇入任务网络。

首先,make dispatch task (一个任务是一个给予线程实际操作的工作单元),它将创建输入和预处理任务。

使用期货作为子任务的手段,以便将指针传回指向完全加载的图像。

创建第二个任务, std :: vector builder任务,只需在期货上调用join即可在完成后获得结果并将其添加到{{1} } array。

我建议您以这种方式构建事物,因为我怀疑您正在进行的任何IO和预处理将比在向量中设置值花费更长的时间。使用任务而不是直接使用线程可以调整工作的并行部分。

我希望不要从具体元素中抽象出来。这是一种模式,我发现在可用硬件饱和之间可以很好地平衡,减少捶打/锁定争用,并且将来可以理解 - 稍后再调试它。

答案 2 :(得分:1)

我会使用3个单独的队列,ready_for_preprocessing由InputStream提供并由预处理消耗,ready_for_computation由预处理提供并由计算消耗,ready_for_output它由Computation提供并由OutputStream使用。

您希望每个队列都在一个类中,该类具有访问互斥(用于控制实际添加和删除队列中的项目)和一个&#34;图像可用&#34;信号量(表示项目可用)以及实际队列。这将允许每个线程的多个实例。像这样:

class imageQueue
{
    std::deque<ImageRead> m_readImages;
    std::mutex            m_changeQueue;
    Semaphore             m_imagesAvailable;

    public:
    bool addImage( ImageRead );
    ImageRead getNextImage();

}

addImage()获取m_changeQueue互斥锁,将图像添加到m_readImages,然后发出信号m_imagesAvailable;

getNextImage()等待m_imagesAvailable。当它发出信号时,它需要m_changeQueue,从列表中删除下一个图像,然后返回它。

比照http://en.cppreference.com/w/cpp/thread

答案 3 :(得分:1)

忽略&#34;问题:如果每个操作都在一个单独的线程中运行,那么您想要处理的对象似乎从一个线程移动到另一个线程。实际上,它们一次只由一个线程唯一拥有(没有线程需要访问来自其他线程的任何数据)。有一种方法可以在C ++中表达:std::unique_ptr

然后每个步骤仅适用于其拥有的图像。您所要做的就是找到一种线程安全的方法,通过流程步骤逐个移动图像的所有权,这意味着关键部分只在任务之间的边界。由于你有多个这些,抽象它是合理的:

class ProcessBoundary
{
public:
  void setImage(std::unique_ptr<ImageRead> newImage)
  {
    while (running)
    {
      {
        std::lock_guard<m_mutex> guard;
        if (m_imageToTransfer == nullptr)
        {
          // Image has been transferred to next step, so we can place this one here.
          m_imageToTransfer = std::move(m_newImage);
          return;
        }
      }
      std::this_thread::yield();
    }
  }

  std::unique_ptr<ImageRead> getImage()
  {
    while (running)
    {
      {
        std::lock_guard<m_mutex> guard;
        if (m_imageToTransfer != nullptr)
        {
          // Image has been transferred to next step, so we can place this one here.
          return std::move(m_imageToTransfer);
        }
      }
      std::this_thread::yield();
    }
  }

  void stop()
  {
    running = false;
  }

private:
  std::mutex m_mutex;
  std::unique_ptr<ImageRead> m_imageToTransfer;
  std::atomic<bool> running; // Set to true in constructor
};

然后,流程步骤将要求使用getImage()的图像,一旦该函数返回,它们就会唯一拥有该图像。他们处理它并将其传递给下一个setImage的{​​{1}}。

您可以使用条件变量或在此类中添加队列来改进此功能,以便线程可以返回处理下一个图像。但是,如果某些步骤比其他步骤更快,那么最终它们必然会被较慢的步骤停滞。

答案 4 :(得分:1)

这是一个设计模式问题。我建议阅读并发设计模式,看看是否有任何可以帮助你的东西。

如果您想在以下顺序过程中添加并发性。

InputStream->Pre-Processing->Computation->OutputStream

然后我建议使用活动对象设计模式。这样,每个进程都不会被上一步阻塞,并且可以同时运行。实现起来也非常简单(这是一个实现: http://www.drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/225700095

关于每个共享DTO的线程的问题。这可以通过DTO上的包装器轻松解决。包装器将包含写入和读取功能。 write函数块使用mutext,read返回const数据。

但是,我认为你的问题在于设计。如果流程是您所描述的顺序,那么为什么每个流程共享数据?一旦当前的数据完成,数据应该传递到下一个进程。换句话说,每个过程都应该解耦。

答案 5 :(得分:0)

使用互斥锁和锁是正确的。对于C ++ 11,这实际上是在线程之间访问复杂数据的最优雅方式。