使用 OpenCV 时的 OpenMP 和显式线程互操作性

OpenMP and explicit thread interoperability when using OpenCV

我在商业应用程序中使用 OpenCV,没有购买 TBB 许可的管理权限,所以我用它构建了 OpenCV OpenMP 作为并行框架。

我们用作实时处理帧源的所有机器视觉相机都有 SDK,这些 SDK 用数据填充循环队列中的帧缓冲区,并调用用户提供的回调以在线程中同时处理它们SDK自带的线程池。

这在不考虑 OpenMP 时工作正常,因为我在通过线程间缓冲区对单个帧进行序列化之前对它们进行了一系列(无记忆)处理,以提供给需要按顺序处理帧的有状态处理阶段。如果它只是并发帧处理,那么我根本不需要 OpenMP;但是,我需要在 OpenCV 中启用它,以便也加速有序帧处理。

我担心的是,在第一阶段使用 OpenMP 时,我可以期望它的工作情况如何,即在相机 SDK 显式创建的线程中并发执行的回调。当在多个外部创建的线程中触发并行区域时,我是否可以假设 OpenMP 运行时足够智能以高效地使用其线程池?

平台保证为 x86-64(VC++15 或 GCC)。

情况

如果我正确理解了这个问题,您正在使用的相机库会产生许多线程,每个线程都会调用您的回调函数。在您的回调中,您想使用 OpenMP 来加速该处理。其结果通过一些线程间通道发送到线程管道进行更多处理。

如果有误,请忽略此答案的其余部分!

其余答案

在回调中使用 OpenMP 似乎会将应用程序这部分的计算负载分成小块,但没有太大好处。相机库已经重叠了帧的处理。在这里使用 OpenMP 意味着帧的处理实际上并不重叠(但相机库仍在使用多线程,就好像它重叠一样)。

如果确实仍然重叠,那么从逻辑上讲,您的系统中没有足够的内核来跟上总体工作负载(假设您使用 OpenMP 导致所有内核都在处理单个帧时已达到极限)...我假设您的系统成功地跟上了帧流,并且必须有足够的能力才能做到这一点。

因此,我认为 OpenMP 在使用其线程池时是否智能化并不是真正的问题;线程池将专门处理单帧,并在下一帧到达之前完成。

非重叠确实意味着延迟较低,这可能正是您想要的。但是,如果相机库使用单线程与您使用 OpenMP 的回调(并承担在下一帧到达之前完成的责任),则您可以实现相同的目的。随着线程上下文切换的减少,它甚至会更快一点。因此,如果您可以停止生成所有这些线程的库(可能有一个配置参数,或环境变量,或其 API 的其他部分),这可能是值得的。

下面是一些示例代码,解释了我找到的解决方法。 LibraryFunction() 表示一些我无法修改的函数,它已经使用了 OpenMP 并行化,例如来自 OpenCV 的函数。

void LibraryFunction(int phase, mutex &mtx)
{
    #pragma omp parallel num_threads(3)
    {
        lock_guard<mutex> l{mtx};
        cerr << "Phase: " << phase << "\tTID: " << this_thread::get_id() << "\tOMP: " << omp_get_thread_num() << endl;
    }
}

外部线程的超额订阅问题可见于此:

int main(void)
{
    omp_set_dynamic(thread::hardware_concurrency());
    omp_set_nested(0);
    vector<std::thread> threads;
    threads.reserve(3);
    mutex mtx;
    for (int i = 0; i < 3; ++i)
    {
        threads.emplace_back([&]
        {
            this_thread::sleep_for(chrono::milliseconds(200));
            LibraryFunction(1, mtx);
        });
    }
    for (auto &t : threads) t.join();
    cerr << endl;
    LibraryFunction(2, mtx);
    return EXIT_SUCCESS;
}

输出为:

Phase: 1        TID: 7812       OMP: 0
Phase: 1        TID: 3928       OMP: 0
Phase: 1        TID: 2984       OMP: 0
Phase: 1        TID: 9924       OMP: 1
Phase: 1        TID: 9560       OMP: 2
Phase: 1        TID: 2576       OMP: 1
Phase: 1        TID: 5380       OMP: 2
Phase: 1        TID: 3428       OMP: 1
Phase: 1        TID: 10096      OMP: 2

Phase: 2        TID: 9948       OMP: 0
Phase: 2        TID: 10096      OMP: 1
Phase: 2        TID: 3428       OMP: 2

第 1 阶段表示在相机 SDK 线程中执行 OpenMPed 库代码,而第 2 阶段是 OpenMPed 库代码,用于仅从一个线程触发的后续处理管道阶段。问题很明显——线程数乘以 OpenMP 外部线程的嵌套,并导致第 1 阶段超额订阅。在关闭 OpenMP 的情况下构建 OpenCV,同时在第 1 阶段修复超额订阅,另一方面会导致第 2 阶段没有加速。

我发现的解决方法是,在 #pragma omp parallel sections {} 的第 1 阶段包装对 LibraryFunction() 的调用会抑制在该特定调用中生成线程。现在结果是:

Phase: 1        TID: 3168       OMP: 0
Phase: 1        TID: 8888       OMP: 0
Phase: 1        TID: 5712       OMP: 0

Phase: 2        TID: 10232      OMP: 0
Phase: 2        TID: 5012       OMP: 1
Phase: 2        TID: 4224       OMP: 2

我还没有用 OpenCV 测试过这个,但我希望它能工作。