首页 > 解决方案 > OpenMP 行为 - 嵌套多线程

问题描述

我的问题与嵌套并行和 OpenMP 有关。让我们从以下单线程代码片段开始:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

现在假设我们要performAnotherTask使用 OpenMP 并行调用。

所以我们得到以下代码:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

我的理解是调用performAnotherTask将并行执行,默认情况下,openMP 将尝试使用您机器上的所有可用线程(也许这个假设是不正确的)。

假设我们现在还想并行化调用,performTask以便我们得到以下代码:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

这将如何运作?两个 for 循环仍然是多线程的吗?我们能说一下每个循环将使用的线程数吗?有没有办法强制内部 for 循环(within performTask)只使用单个线程,而外部 for 循环使用所有可用线程?

标签: c++multithreadingopenmp

解决方案


在您的最后一个示例中,执行行为取决于一些环境设置。

首先,OpenMP 确实支持这种模式,但默认情况下禁用嵌套并行区域中的并行执行。要启用它,您必须在代码中设置OMP_NESTED=true或调用omp_set_nested(1)。然后启用对嵌套并行执行的支持。

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

其次,当 OpenMP 到达外部parallel区域时,它可能会抓取所有可用内核并假设它可以在它们上执行线程,因此您可能希望减少外部级别的线程数,以便某些内核可用于嵌套区域。比如说,如果你有 32 个核心,你可以这样做:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for num_threads(8)
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel for num_threads(4)
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

外部并行区域将使用 4 个线程执行,每个线程将使用 8 个线程执行内部区域。请注意,4 个外部线程中的每一个都将是四个同时执行的嵌套并行区域的主线程之一。如果您想更加灵活,您可以使用环境变量注入每个级别使用的线程数OMP_NUM_THREADS。如果您将其设置为,OMP_NUM_THREADS=4,8您将获得与上面我发布的第一个代码片段相同的行为。

编码模式的问题是您需要小心平衡每个级别,以免系统过载或嵌套并行区域之间的负载不平衡。另一种解决方案是改用 OpenMP 任务:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp taskloop
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel 
#pragma omp single
#pragma omp taskloop
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

在这里,每个taskloop构造都将生成 OpenMP 任务,这些任务计划在由parallel代码中的单个区域创建的线程上执行。需要注意的是,任务的行为本质上是动态的,因此您可能会丢失局部性属性,因为您不知道任务将在系统中的确切位置执行。


推荐阅读