c++ - 线程 ID 在 std::thread 和 tbb::task_group 之间重用导致 OpenMP 中的死锁

标签 c++ multithreading openmp tbb stdthread

*** 更新:将代码更改为重现问题的真实案例 ***
我正在处理一些使用多种多线程技术的预先存在的代码; std::thread,加上英特尔 TBB 任务组,加上 OpenMP。 🙄 看起来我在 std::thread join 中遇到了竞争条件,并且可能在 OpenMP 中也遇到了竞争条件。 (但当然这些库是由聪明人编写的,所以如果我正在使用的代码中有错误,我希望你能帮我弄清楚。)
场景是主线程启动了一堆 I/O 工作线程 std::threads,它们本身启动了一些任务,并且这些任务有一些使用 OpenMP 进行并行处理的代码段。主线程执行 std::thread::join() 等待 std::threads,然后 tbb::TaskGroup::wait() 等待任务完成。

#include <Windows.h>
#include <tbb/task_group.h>
#include <tbb/concurrent_vector.h>
#include <iostream>
#include <sstream>
#include <thread>

void DoCPUIntensiveWork(int chunkIndex);

int main ()
{
    unsigned int hardwareConcurrency = 64;
    tbb::concurrent_vector<std::shared_ptr<std::thread>> ioThreads;
    tbb::task_group taskGroup;

    wprintf(L"Starting %u IO threads\n", hardwareConcurrency);
    for (unsigned int cx = 0; cx < hardwareConcurrency; ++cx)
    {
        ioThreads.push_back(std::shared_ptr<std::thread>(new std::thread([&taskGroup, cx]
            {
                wprintf(L"IO thread %u starting\r\n", GetCurrentThreadId());
            
                // Not doing any actual IO

                taskGroup.run([cx]
                    {
                        wprintf(L"CPU task %u starting\r\n", GetCurrentThreadId());
                        DoCPUIntensiveWork(cx);
                        wprintf(L"CPU task %u done\r\n", GetCurrentThreadId());
                    });

                //Sleep(1000);  Un-commenting this will make the program terminate
                wprintf(L"IO thread %u done\r\n", GetCurrentThreadId());
            })));
    }

    // Join the IO workers
    for (std::shared_ptr<std::thread>& thread : ioThreads)
    {
        std::stringstream ss;
        ss << thread->get_id();
        wprintf(L"Wait for thread %S\r\n", ss.str().c_str());
        thread->join();  // main thread hangs here
    }

    wprintf(L"IO work complete\n");

    // And then wait for the CPU tasks
    taskGroup.wait();

    wprintf(L"CPU work complete\n");

    return 0;
}
CPU 密集型工作包括 OpenMP 的使用。 (注意,如果我删除计划(静态),结果是一样的。)
// Note: I shrunk these numbers down until the amount of work is actually
// small, not CPU-intensive at all, and it still hangs
static const int GlobalBufferChunkSize = 64;
static const int NumGlobalBufferChunks = 64;
static const int StrideSize = 16;
static const int OverwriteCount = 4;
BYTE GlobalBuffer[NumGlobalBufferChunks * GlobalBufferChunkSize];

void DoCPUIntensiveWork(int chunkIndex)
{
    BYTE* pChunk = GlobalBuffer + (chunkIndex * GlobalBufferChunkSize);

#pragma omp parallel for schedule(static)
    for (int i = 0; i < (GlobalBufferChunkSize / StrideSize); i++)
    {
        BYTE* pStride = pChunk + (i * StrideSize);
        for (int j = 0; j < OverwriteCount; j++)
        {
            memset(pStride, i, StrideSize);
        }
    }  // Task thread hangs here
}
此代码挂起;主线程永远等待 thread->join() 。即使在只有一个 IO 作业/CPU 密集型任务的测试用例上。我添加了您在上面看到的 printf,结果显示 IO 作业完成得很快,该线程退出,然后 CPU 密集型任务在主线程甚至进入 join() 调用之前以相同的线程 ID 启动。
Starting 64 IO threads
IO thread 3708 starting
IO thread 23728 starting
IO thread 23352 starting
IO thread 3588 starting
IO thread 3708 done
IO thread 23352 done
IO thread 22080 starting
IO thread 23728 done
IO thread 3376 starting
IO thread 3588 done
IO thread 27436 starting
IO thread 10092 starting
IO thread 22080 done
IO thread 10480 starting
CPU task 3708 starting
IO thread 3376 done
IO thread 27436 done
IO thread 10092 done
IO thread 10480 done
Wait for thread 3708
... hang forever ...
线程完成后,IO 线程 ID 被重新用于任务,而 thread->join() 调用仍然坐在那里等待。当我查看调试器时,thread->join() 正在等待 ID 为 3708 的线程,并且确实存在具有该 ID 的线程,但该线程正在执行任务而不是 IO 工作。所以看起来进程的主线程实际上是在等待任务,而不是由于 ID 重用而等待 IO 线程。 (我找不到文档或代码来查看 std::thread::join() 是否基于 ID 或句柄等待,但它似乎使用了 ID,这将是一个错误。)
第二个有趣的事情是,该任务从未完成,当我查看在调试器中执行任务的线程时,它位于 OpenMP 并行执行的末尾。我没有看到任何其他线程在执行并行工作。 ntdll.dll 代码中有许多来自 vcomp140[d].dll 的线程,我没有符号 - 我认为这些线程只是在等待新工作,而不是执行我的任务。 CPU 为 0%。我非常有信心没有人在循环。因此,TBB 任务卡在 OpenMP 多线程实现中的某处。
但是,为了让事情变得复杂,任务似乎不会挂起,除非 IO 线程的线程 ID 恰好被重用于任务。因此,在 std::thread 和 TBB 任务与 OpenMP 并行之间的某个地方,存在由线程 ID 重用触发的竞争条件。
我找到了两种解决方法,使挂起消失:
  • 将 Sleep(1000) 放在 IO 线程的末尾,因此任务不会重用 IO 线程 ID。当然,该错误仍然存​​在,等待时机不佳。
  • 删除 OpenMP 并行性的使用。

  • 一位同事提出了第三种可能的选择,用 TBB parallel_for 替换 OpenMP 并行性。我们可能会这样做。当然,这是我们希望尽可能少接触的来自不同来源的所有代码层。 🙄
    我更多地将此报告为可能的错误报告,而不是寻求帮助。
  • 如果重用线程 ID, std::thread::join() 最终可能会等待错误的线程,这似乎是一个错误。它应该通过句柄等待,而不是通过 ID。
  • 似乎 TBB 任务和 OpenMP 之间存在错误或不兼容,因此如果 OpenMP 主线程在 TBB 任务上运行,而该任务恰好具有被重用的线程 ID,则它可能会挂起。
  • 最佳答案

    更新:关于超额认购的假设是不正确的。见 https://github.com/oneapi-src/oneTBB/issues/353
    我认为这个问题可能是由 OpenMP 语义引起的。默认情况下,它总是创建与硬件并发一样多的线程。
    TBB 将创建 std::thread::hardware_concurrency()线程和 OpenMP 将创建 std::thread::hardware_concurrency()对于从中调用它的每个 TBB 工作线程。 IE。在这个例子中,我们将有最多 std::thread::hardware_concurrency()*std::thread::hardware_concurrency()线程(+64 IO 线程)。如果机器比较大,例如32+ 个线程,它将是 32*32 = 1024应用程序中的线程(总体而言,它接近默认 ulimit 还是 Windows?)无论如何,在并行区域末尾使用 OpenMP 屏障语义进行如此大的超额订阅会导致非常长的执行时间(例如几分钟甚至几小时) .
    为什么Sleep(1000)帮助?我不确定,但它可能会为系统提供一些 CPU 资源以继续前进。
    要检查这个想法,请添加 num_threads(1)条款到 #pragma omp parallel for num_threads(1)限制 OpenMP 运行时创建的线程数。

    关于c++ - 线程 ID 在 std::thread 和 tbb::task_group 之间重用导致 OpenMP 中的死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66346225/

    相关文章:

    c++ - RAW指针容器包装器

    c++ - 用有限的钱购买元素的算法

    java - 等待 JVM 处理的对象锁导致线程处于阻塞状态

    java - Thread.currentThread().interrupt() 在 Android 中不工作

    java - 如果一个方法只是调用 Java 中的另一个线程安全方法,它是否是线程安全的?

    gcc - 对于 gnu 和 ibm openmp 库,KMP_AFFINITY=verbose 等效吗?

    gcc - 配置 FFTW `configure: error: don' 不知道如何启用 OpenMP`

    c - 仅在将数组设为私有(private)后才进行多线程加速

    c++ - 函数(隐式声明)不能被引用——它是一个被删除的函数

    c++ - 静态数据成员数组边界的求值范围