代码之家  ›  专栏  ›  技术社区  ›  Crigges

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

  •  11
  • Crigges  · 技术社区  · 6 年前

    背景:

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

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

    关于TBB answer 从2012年起,获得5张赞成票:

    With_TBB=ON OpenCV尝试对某些函数使用多个线程。问题是目前只有一个漂亮的函数与TBB连接(可能有十几个)。所以,很难看到任何加速。OpenCV的原理是应用程序应该是多线程的,而不是OpenCV函数。[…]

    关于应用程序级的多线程,一个 comment 从主持人开始 answers.opencv.org :

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

    但是另一个 answer 以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;
        }
    }
    

    如果程序长时间运行,则

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

    越来越大了。

    运行程序约10分钟后,打印的时间跨度已翻倍,这在正常波动下是无法解释的。

    预期行为:

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

    笔记:

    直接调用函数时:

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

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

    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的应用程序级别上使用(多)线程可以吗?
    • 如果是,为什么上面的程序会打印时间跨度 增长的
    • 如果没有,为什么OpenCV是 considered thread safe 请解释如何解释 statement from Kirill Kornyakov 相反
    • 2019年的TBB/OpenMP现在是否得到广泛支持?
    0 回复  |  直到 6 年前
        1
  •  6
  •   FutureJJ Saba Chaudry    6 年前

    首先,谢谢你澄清了这个问题。

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

    答: 是的,在OpenCV的应用程序级别上使用多线程是完全可以的,除非并且直到您使用可以利用多线程的函数,例如模糊、颜色空间变化,在这里,您可以将图像分割成多个部分,并在分割的部分应用全局函数,然后重新组合它以给出最终输出。

    在Hough、pca_分析等函数中,如果将其应用于分割的图像区域,然后重新组合,则无法给出正确的结果,因此在应用程序级对这些函数应用多线程可能无法给出正确的结果,因此不应执行。

    正如±1所提到的,您的多线程实现不会给您带来优势,因为您将线程连接到for循环本身中。我建议您使用promise和future对象(如果您想要一个如何使用的示例,请在评论中让我知道,我将分享这个片段)。

    下面的答案需要大量的研究,谢谢你的提问,它真的帮助我增加了我的多线程知识的信息:)

    如果是,为什么我的程序打印的时间跨度会随着时间而增长?

    经过大量的研究,我发现创建和销毁线程需要大量的CPU和内存资源。当我们初始化一个线程时(在您的代码中: thread t(blurSlowdown, nullptr); )一个标识符被写入这个变量指向的内存位置,这个标识符使我们能够引用线程。现在,在您的程序中,您正在以非常高的速度创建和销毁线程,现在发生的情况是,有一个线程池分配给一个程序,通过它,我们的程序可以运行和销毁线程,我将保持它的简短,让我们看一下下面的解释:

    1. 当您销毁线程时,此内存将被释放

    但是

    1. 当您在第一个线程没有被破坏之后再次创建一个线程时,这个新线程的标识符指向 新位置 (上一个线程以外的位置)在线程池中。

    2. 在反复创建和销毁线程之后 线程池已耗尽 CPU被迫将我们的程序周期放慢一点,以便线程池再次被释放 为新的线腾出空间。

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

    是的,您可以在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上实现多线程?

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

    编辑: 我检查了有关图像尺寸与要分割处理的线程数不兼容的注释。所以这里有一个 解决方法(尚未测试但应该有效),将图像分辨率缩放到兼容的维度,如:

    Parallel_process 然后将输出缩小到原始大小458x 647。

    this answer