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

Cri*_*ges 20 c++ performance multithreading opencv vectorization

背景:

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

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

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

关于TBB,2012年的答案有5个赞成票:

使用WITH_TBB = ON时,OpenCV会尝试使用多个线程来执行某些功能.问题是目前只有一个功能强大的TBB(可能是十几个).所以,很难看到任何加速.这里的OpenCV理念是应用程序应该是多线程的,而不是OpenCV函数.[...]

关于应用程序级别的多线程,主持人对answers.opencv.org评论:

请避免在opencv中使用自己的多线程.很多函数显然不是线程安全的.而是使用TBB或openmp支持重建opencv库.

但有3个赞成票的另一个答案是:

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

问题描述:

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

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

#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;
    }
}
Run Code Online (Sandbox Code Playgroud)

实际行为:

如果程序运行了很长一段时间,则打印时间跨度

cout << duration_cast<microseconds>(end - start).count() << endl;
Run Code Online (Sandbox Code Playgroud)

越来越大了.

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

预期行为:

我期望的程序的行为是时间跨度保持非常恒定,即使它们可能比直接调用函数更长.

笔记:

直接调用函数时:

[...]
for (int k = 0; k < 100; k++) {
    blurSlowdown(nullptr);
}
[...]
Run Code Online (Sandbox Code Playgroud)

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

什么时候不调用cv函数:

void blurSlowdown(void*) {
    Mat m1(360, 640, CV_8UC3);
    Mat m2(360, 640, CV_8UC3);
    //medianBlur(m1, m2, 3);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我知道上面的代码没有实现实际的多线程,只有一个线程在调用blurSlowdown()函数的同时处于活动状态.
  • 我知道创建线程并在之后清理它们并不是免费的,而且比直接调用函数要慢.
  • 并不是说代码一般都很慢.问题是印刷的时间跨度随着时间的推移越来越.
  • 问题是不相关的medianBlur(),因为它发生在与其他像其他功能的功能erode()blur()太.
  • 这个问题在Mac下的clang ++下重现,请参阅@Mark Setchell的评论
  • 使用调试库而不是发行版时,问题会被放大

我的测试环境:

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

我的问题:

  • 在OpenCV的应用程序级别上使用(多)线程是否可以?
  • 如果是的话,为什么我的计划上面的时间跨度会随着时间推移而增长
  • 如果没有,为什么OpenCV的则认为是线程安全的,并请解释如何解释从基里尔Kornyakov语句代替
  • TB9/OpenMP在2019年现在得到广泛支持吗?
  • 如果是,那么在应用程序级别(如果允许)或TBB/OpenMP上提供更好的性能,多线程?

Fut*_*eJJ 5

首先,感谢您清楚问题。

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

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

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

作为????? ??? 提到,多线程的实现不会给您带来优势,因为您是在for循环本身中加入线程的。我建议您使用Promise和Future对象(如果您想使用示例,请在注释中告诉我,我将分享该代码段。

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

问:如果可以,为什么我的程序在“ GROWING”上方显示了时间跨度?

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

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

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

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

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

问: 2019年的TBB是否得到广泛支持?

答:是的,您可以在OpenCV程序中利用TBB的优势,同时还可以在构建OpenCV时启用TBB支持。

这是在meanBlur中实现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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

编辑:我检查了有关图像尺寸与要在其中划分处理的线程数不兼容的注释。因此,这是一个潜在的解决方法(尚未测试,但应该可以工作),将图像分辨率缩放到兼容尺寸,例如:

如果您的图像分辨率为485 x 647,则将其缩放为488 x 648,然后传递它,Parallel_process然后将输出缩放为原始尺寸458 x 647。

对于TBB和OpenMP的比较,请查看此答案