如何在2019年的OpenCV中正确使用多线程?

时间:2019-01-31 17:55:21

标签: c++ multithreading performance opencv vectorization

背景:

我阅读了一些有关OpenCV中多线程的文章和帖子:

  • 一方面,您可以构建具有TBB或OpenMP支持的OpenCV,它们在内部并行化OpenCV的功能。
  • 另一方面,您可以自己创建多个线程并并行调用函数以在应用程序级别实现多线程。

但是我无法获得一致的答案,哪种多线程方法是正确的方法。

关于TBB,2012年的answer有5票支持:

  

With WITH_TBB = ON OpenCV尝试将多个线程用于某些功能。问题在于,目前只有漂亮的功能与TBB一起使用(可能是一打)。因此,几乎看不到任何加速。 OpenCV的哲学是应用程序应该是多线程的,而不是OpenCV函数。[...]

关于应用程序级别的多线程,来自comment的主持人的answers.opencv.org

  

请避免将自己的多线程与opencv一起使用。许多函数显然不是线程安全的。   而是使用TBB或openmp支持来重建opencv库。

但是另外有3个投票的answer声明:

  

库本身是线程安全的,因为您可以同时对库进行多次调用,但是数据并不总是线程安全的。

问题描述:

因此,我认为在应用程序级别使用(多)线程至少可以。但是当我长时间运行程序时,遇到了奇怪的性能问题。

在研究了这些性能问题之后,我创建了这个最小,完整和可验证的示例代码:

#include "opencv2\opencv.hpp"
#include <vector>
#include <chrono>
#include <thread>

using namespace cv;
using namespace std;
using namespace std::chrono;

void blurSlowdown(void*) {
    Mat m1(360, 640, CV_8UC3);
    Mat m2(360, 640, CV_8UC3);
    medianBlur(m1, m2, 3);
}

int main()
{
    for (;;) {
        high_resolution_clock::time_point start = high_resolution_clock::now();

        for (int k = 0; k < 100; k++) {
            thread t(blurSlowdown, nullptr);
            t.join(); //INTENTIONALLY PUT HERE READ PROBLEM DESCRIPTION
        }

        high_resolution_clock::time_point end = high_resolution_clock::now();
        cout << duration_cast<microseconds>(end - start).count() << endl;
    }
}

实际行为:

如果程序长时间运行,则打印的时间跨度为

cout << duration_cast<microseconds>(end - start).count() << endl;

越来越大。

程序运行大约10分钟后,打印的时间跨度增加了一倍,这在正常波动下无法解释。

预期的行为:

我希望程序的行为是时间跨度保持恒定,即使它们可能比直接调用函数还要长。

注意:

直接调用该函数时:

[...]
for (int k = 0; k < 100; k++) {
    blurSlowdown(nullptr);
}
[...]

打印的时间跨度保持不变。

不调用cv函数时:

void blurSlowdown(void*) {
    Mat m1(360, 640, CV_8UC3);
    Mat m2(360, 640, CV_8UC3);
    //medianBlur(m1, m2, 3);
}

打印的时间跨度也保持恒定。因此,将线程与OpenCV函数结合使用时肯定有问题。

  • 我知道上面的代码无法实现实际的多线程,因此在调用blurSlowdown()函数的同时只有一个线程处于活动状态。
  • 我知道创建线程并随后清理它们并不是免费的,并且比直接调用该函数要慢。
  • 不是 关于代码通常运行缓慢。 问题是打印的时间跨度随着时间的推移越来越长
  • 该问题与medianBlur()函数无关,因为它也发生在其他函数上,例如erode()blur()
  • 该问题在Mac上用clang ++复制,请参阅@Mark Setchell的评论
  • 使用调试库而不是发行版时,该问题得到了放大

我的测试环境:

  • Windows 10 64位
  • MSVC编译器
  • OpenCV 3.4.2官方二进制文件

我的问题:

  • 在OpenCV的应用程序级别上可以使用(多)线程吗?
  • 如果是,为什么我的程序会随着时间在 Growing 上方打印时间跨度?
  • 如果没有,为什么使用OpenCV,然后considered thread safe,请解释如何解释statement from Kirill Kornyakov
  • 2019年的TBB / OpenMP是否受到广泛支持?
  • 如果是,那么什么能提供更好的性能,应用程序级别的多线程(如果允许)或TBB / OpenMP?

1 个答案:

答案 0 :(得分:1)

首先,感谢您清楚这个问题。

问:是否可以在OpenCV的应用程序级别上使用(多)线程?

A:是的,在OpenCV的应用程序级别上使用多线程是完全可以的,除非并且直到您正在使用可以利用多线程的功能(例如模糊,色彩空间改变),否则您可以拆分将图像分为多个部分,并在整个部分中应用全局功能,然后重新组合以提供最终输出。

在某些函数中,例如Hough,pca_analysis在将它们应用于分割的图像部分然后进行重新组合时无法给出正确的结果,因此在应用程序级别对这些函数应用多线程可能无法给出正确的结果,因此不应该这样做。

正如πάνταῥεῖ提到的那样,多线程的实现不会给您带来优势,因为您是在for循环本身中加入线程的。我建议您使用Promise和Future对象(如果您想使用示例,请在注释中告诉我,我将分享该片段。

下面的答案进行了大量研究,感谢您提出问题,它确实有助于我在多线程知识中添加信息:)

问:,如果是,为什么我的程序会在一段时间内在GROWING上方显示时间跨度?

A:经过大量研究,我发现创建和销毁线程会占用大量CPU和内存资源。当我们初始化线程时(在您的代码中此行:thread t(blurSlowdown, nullptr);),一个标识符被写入该变量所指向的内存位置,并且该标识符使我们能够引用该线程。现在在您的程序中,您正在以很高的速度创建和销毁线程,这就是发生的事情,有一个分配给程序的线程池,我们的程序可以通过该线程池运行和销毁线程,我将其简短地介绍一下以下说明:

  1. 创建线程时,这将创建一个指向该线程的标识符。
  2. 销毁线程时,该内存被释放

但是

  1. 在不久之后再次创建线程时,第一个线程被销毁,该新线程的标识符指向该线程中的新位置(除先前线程之外的位置)池。

  2. 反复创建和销毁线程后,线程池已用尽,因此 CPU被迫稍微减慢我们的程序周期,以便再次释放线程池为新线程腾出空间。

Intel TBB和OpenMP非常擅长线程池管理,因此在使用它们时可能不会发生此问题。

问::现在是否广泛支持2019年的TBB?

A:是的,您可以在OpenCV程序中利用TBB,同时在构建OpenCV时也可以启用TBB支持。

这是一个在medianBlur中执行TBB的程序:

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <chrono>

using namespace cv;
using namespace std;
using namespace std::chrono;

class Parallel_process : public cv::ParallelLoopBody
{

private:
    cv::Mat img;
    cv::Mat& retVal;
    int size;
    int diff;

public:
    Parallel_process(cv::Mat inputImgage, cv::Mat& outImage,
                     int sizeVal, int diffVal)
        : img(inputImgage), retVal(outImage),
          size(sizeVal), diff(diffVal)
    {
    }

    virtual void operator()(const cv::Range& range) const
    {
        for(int i = range.start; i < range.end; i++)
        {
            /* divide image in 'diff' number
               of parts and process simultaneously */

            cv::Mat in(img, cv::Rect(0, (img.rows/diff)*i,
                                     img.cols, img.rows/diff));
            cv::Mat out(retVal, cv::Rect(0, (retVal.rows/diff)*i,
                                         retVal.cols, retVal.rows/diff));

            cv::medianBlur(in, out, size);
        }
    }
};

int main()
{
    VideoCapture cap(0);

    cv::Mat img, out;

    while(1)
    {
        cap.read(img);
        out = cv::Mat::zeros(img.size(), CV_8UC3);

        // create 8 threads and use TBB
        auto start1 = high_resolution_clock::now();
        cv::parallel_for_(cv::Range(0, 8), Parallel_process(img, out, 9, 8));
        //cv::medianBlur(img, out, 9); //Uncomment to compare time w/o TBB
        auto stop1 = high_resolution_clock::now();
        auto duration1 = duration_cast<microseconds>(stop1 - start1);

        auto time_taken1 = duration1.count()/1000;
        cout << "TBB Time: " <<  time_taken1 << "ms" << endl;

        cv::imshow("image", img);
        cv::imshow("blur", out);
        cv::waitKey(1);
    }

    return 0;
}

在我的计算机上,TBB实施大约需要10毫秒,而没有TBB的实施大约需要40毫秒。

问:,如果可以,哪种性能更好,在应用程序级别(如果允许)或TBB / OpenMP上提供多线程?

A:我建议您在POSIX多线程(pthread / thread)上使用TBB / OpenMP,因为TBB为您提供了更好的线程控制能力和更好的并行代码编写结构,并在内部管理pthread。如果使用pthreads,则必须注意代码中的同步和安全性。但是使用这些框架会抽象出对处理线程的需求,这可能会变得非常复杂。

要比较TBB和OpenMP,请检查this answer