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

How to properly Multithread in OpenCV in 2019?

本文关键字:多线程 2019年 OpenCV      更新时间:2023-10-16

背景:

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

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

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

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

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

关于应用程序级的多线程,answers.opencv.org/上一位主持人的评论

请避免在opencv中使用您自己的多线程。许多函数显然不是线程安全的。而是使用TBB或openmp支持重建opencv-lib。

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

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

问题描述:

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

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

#include "opencv2opencv.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()函数的同时,只有一个线程处于活动状态
  • 我知道创建线程并在之后清理它们并不是免费的,而且会比直接调用函数慢
  • 这是NOT关于代码通常很慢问题是打印的时间跨度随着时间的推移越来越长
  • 这个问题与medianBlur()函数无关,因为它也发生在其他函数上,如erode()blur()
  • 这个问题是在Mac的clang++下复制的,请参阅@Mark Setchell的评论
  • 当使用调试库而不是版本时,问题会被放大

我的测试环境:

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

我的问题:

  • 在OpenCV的应用程序级别上使用(多)线程可以吗
  • 如果是,为什么我上面的程序打印的时间跨度会随着时间的推移而增长
  • 如果没有,为什么OpenCV被认为是线程安全的,请解释如何解释Kirill Kornyakov的声明
  • 2019年的TBB/OpenMP现在得到了广泛支持吗
  • 如果是,是什么提供了更好的性能,应用程序级别的多线程(如果允许)还是TBB/OpenMP

首先,感谢您澄清问题。

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

A:是的,在OpenCV的应用程序级别上使用多线程是完全可以的,除非您使用可以利用多线程的功能,如模糊、颜色空间更改,在这里,您可以将图像分割为多个部分,并在分割的部分应用全局函数,然后重新组合以提供最终输出。

在一些函数中,如Hough、pca_sanalysis,当它们被应用于分割的图像部分然后重新组合时,不能给出正确的结果,在应用程序级别上对这些函数应用多线程可能不能给出正确结果,因此不应该这样做。

AsπάΓταῥεῖ前面提到,多线程的实现不会给您带来优势,因为您将线程加入for循环本身。我建议你使用promise和future对象(如果你想要一个如何使用的例子,请在评论中告诉我,我会分享这个片段

下面的答案花了很多研究,谢谢你提出这个问题,它真的帮助我为我的多线程知识添加信息:)

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

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的实现大约需要10ms,如果没有TBB,大约需要40ms。

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

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

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

如果您的图像分辨率是485 x 647,请将其缩放到488 x 648,然后将其传递给Parallel_process,然后将输出缩小到458 x 647的原始大小。

为了比较TBB和OpenMP,请检查此答案