【并行计算】tbb::parallel c++并行计算的用法总结

简介

tbb::parallel是指Intel Threading Building Blocks (TBB)库中的一个功能,它是一个C++库,用于并行化任务和数据处理。TBB旨在简化多核处理器上的并行编程,并提供高性能、可扩展性和跨平台性。

在TBB库中,tbb::parallel提供了一组并行执行的算法,可用于在多个处理器核心上同时处理数据,从而提高应用程序的性能。这些算法自动处理任务的分解和负载平衡,使得开发者无需显式编写线程管理代码,从而简化了并行编程的过程。

tbb::parallel中的算法主要利用了任务并行和数据并行的思想。任务并行是指将任务分解成小的子任务,然后并行执行这些子任务。数据并行是指将数据分成多个部分,然后并行处理这些数据片段。TBB库通过这两种方式,自动将工作负载分配到多个处理器核心上,充分利用了多核处理器的潜力。

一些常见的tbb::parallel算法包括:

tbb::parallel_for:在一个范围内并行执行for循环。
tbb::parallel_reduce:并行化归约操作,将数据聚合为单个值。
tbb::parallel_invoke:并行地执行多个任务。
tbb::parallel_sort:并行排序操作。
tbb::parallel_scan:并行化扫描操作,对输入数据执行一系列的累积操作。

tbb::parallel_invoke

tbb::parallel_invoke用于并行地调用多个函数。这些函数将在多个线程上并行执行,从而利用多核处理器的优势来提高程序的性能。

函数签名

template<typename Func1, typename Func2, typename... Funcs>
void parallel_invoke(Func1&& f1, Func2&& f2, Funcs&&... fs);

其中,Func1、Func2、Funcs...是要并行执行的函数参数。这些函数可以是普通函数、函数对象、lambda表达式等。

tbb::parallel_invoke会并行地启动多个线程,每个线程分别执行一个函数,以实现同时调用多个函数的效果。这在一些情况下可以带来性能的提升,特别是当这些函数之间相对独立且计算密集时。

需要注意的是,tbb::parallel_invoke函数会等待所有并行调用的函数执行完毕,然后才会继续执行下面的代码。因此,在使用这个函数时,确保被调用的函数不会造成主线程的阻塞,否则可能会影响整体性能。

示例

#include <iostream>
#include <tbb/tbb.h>

void Function1() {
    
    
    std::cout << "Function1 is running in thread " << tbb::this_tbb_thread::get_id() << std::endl;
}

void Function2() {
    
    
    std::cout << "Function2 is running in thread " << tbb::this_tbb_thread::get_id() << std::endl;
}

int main() {
    
    
    tbb::parallel_invoke(Function1, Function2);

    std::cout << "Main thread is running in thread " << tbb::this_tbb_thread::get_id() << std::endl;
    return 0;
}

输出

Function1 is running in thread 140372388376576
Function2 is running in thread 140372365193984
Main thread is running in thread 140372406011392

在这个示例中,Function1Function2将会在不同的线程上并行执行,而主线程在调用tbb::parallel_invoke后会等待这两个函数执行完毕后才会继续执行。

需要注意的是,tbb::parallel_invoke并行调用的函数之间是独立执行的,它们之间没有任何数据交换。如果需要在函数之间共享数据,必须采取其他同步机制,例如互斥锁或原子操作,来保证数据的正确性。

tbb::parallel_reduce

tbb::parallel_reduce 用于实现数据的并行归约操作。并行归约的目的是将大规模数据范围划分为较小的部分,在多个线程上并行地进行归约计算,然后将结果合并得到最终的归约值。

函数签名

template<typename Range, typename Body> 
parallel_reduce( 
    const Range& range, 
    Body& body 
);

template<typename Range, typename Body, typename Reduction>
parallel_reduce(
    const Range& range,
    Body& body,
    Reduction& reduction 
);

用法主要包含以下几个步骤:

  1. 定义一个Range,表示要并行处理的数据范围,可以是数组,vector等容器。
  2. 定义一个Body函数,对Range中的每个块进行处理,并返回结果。
  3. 定义一个Reduction函数,将每个块的结果合并起来。
  4. 调用parallel_reduce,传入Range,Body和Reduction。

示例

例如,计算0-99的整数平方和:

#include <tbb/parallel_reduce.h> 
#include <iostream>

int main() {
    
    

  // 1. 定义Range
  tbb::blocked_range<int> r(0, 100); 

  // 2. Body函数 
  auto body = [&](const tbb::blocked_range<int> &r, int running_total){
    
    
    for(int i=r.begin(); i!=r.end(); ++i){
    
    
      running_total += i*i; 
    }
    return running_total;
  };

  // 3. Reduction
  auto reduction = [](int x, int y) {
    
    
    return x + y;
  };

  // 4. parallel_reduce
  int sum = tbb::parallel_reduce(r, 0, body, reduction);

  std::cout << "Sum of squares from 0 to 99 is " << sum << "\n";

}

paralle_reduce会自动将0-99的范围分块,调用body对每个块进行平方和计算,然后用reduction将结果累加,实现并行计算。

tbb::parallel_sort

tbb::parallel_sort用于在多线程环境下并行排序数据。它能够充分利用多核处理器的优势,在数据量较大时提高排序的性能。

函数签名

template <typename RandomAccessIterator, typename Compare>
void tbb::parallel_sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

RandomAccessIterator: 迭代器类型,指向要排序范围的首元素和末尾元素的后一个位置。
Compare: 比较函数类型,用于指定元素之间的比较方式。默认情况下,使用std::less来进行升序排序。

示例

使用tbb::parallel_sort对一个包含9个元素的整数向量进行排序。

#include <iostream>
#include <vector>
#include <tbb/tbb.h>

int main() {
    
    
    std::vector<int> data = {
    
    9, 3, 1, 7, 5, 8, 2, 6, 4};

    // 使用 tbb::parallel_sort 进行排序
    tbb::parallel_sort(data.begin(), data.end());

    std::cout << "Sorted data: ";
    for (int num : data) {
    
    
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出

Sorted data: 1 2 3 4 5 6 7 8 9

由于tbb::parallel_sort会在多个线程上并行执行排序,因此可以更快地完成排序操作。

需要注意的是,虽然tbb::parallel_sort能够在多线程环境下加速排序,但在某些情况下,串行排序算法(如std::sort)在小规模数据上可能更快。因此,对于小规模数据,可以根据实际情况选择适合的排序方法。同时,tbb::parallel_sort可能与标准库中的排序函数有细微的差异,建议在具体应用中进行性能测试和比较。

tbb::parallel_scan

tbb::parallel_scan用于在多线程环境下进行并行扫描(也称为并行前缀和)操作。它用于对一个数据序列进行逐元素操作并产生一个新的序列,其中每个元素是原始序列中从开头到当前位置的部分的聚合结果。

函数签名

template<typename Range, typename Value, typename ScanBody>
Value tbb::parallel_scan(const Range& range, const Value& identity, const ScanBody& scan_body);

  • Range: 输入数据的范围,可以是一个迭代器范围或支持迭代器的容器。
  • Value: 并行扫描的结果类型,它可以是一个数值类型、自定义结构或容器等。
  • ScanBody: 一个函数对象,负责定义扫描操作的逻辑。该函数对象必须提供两个操作符重载:
    • void operator()(const Range& range) const: 该操作符用于在一个范围上执行扫描操作。
    • Value get_value() const: 该操作符返回当前扫描的最终结果。

用法

#include <iostream>
#include <vector>
#include <tbb/tbb.h>

class SumScanBody {
    
    
public:
    // 定义扫描操作的逻辑,计算局部累积和并返回
    void operator()(const tbb::blocked_range<size_t>& r) const {
    
    
        int local_sum = 0;
        for (size_t i = r.begin(); i != r.end(); ++i) {
    
    
            local_sum += data[i];
            partial_sums[i] = local_sum;
        }
    }

    // 返回最终的累积和
    int get_value() const {
    
    
        return partial_sums.back();
    }

    // 构造函数,初始化数据和结果容器
    SumScanBody(const std::vector<int>& data, std::vector<int>& partial_sums)
        : data(data), partial_sums(partial_sums) {
    
    }

private:
    const std::vector<int>& data;
    std::vector<int>& partial_sums;
};

int main() {
    
    
    const int num_elements = 10;
    std::vector<int> data(num_elements);
    std::vector<int> partial_sums(num_elements);

    // 初始化数据
    for (int i = 0; i < num_elements; ++i) {
    
    
        data[i] = i + 1;
    }

    // 使用 tbb::parallel_scan 计算累积和
    tbb::parallel_scan(
        tbb::blocked_range<size_t>(0, data.size()), 0,
        SumScanBody(data, partial_sums)
    );

    // 输出结果
    std::cout << "Input data: ";
    for (int num : data) {
    
    
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "Partial sums: ";
    for (int sum : partial_sums) {
    
    
        std::cout << sum << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出

Input data: 1 2 3 4 5 6 7 8 9 10 
Partial sums: 1 3 6 10 15 21 28 36 45 55 

在这个示例中,定义了一个 SumScanBody 类,其中包含了 operator()get_value 方法。

operator() 方法负责在输入数据范围上计算累积和并保存在 partial_sums 容器中,get_value 方法返回最终的累积和结果。

然后,我们使用 tbb::parallel_scan 来并行计算输入数据的累积和。最终结果将存储在 partial_sums 容器中。

需要注意的是,tbb::parallel_scan 要求扫描操作是可结合的,即不同线程并行执行的结果与串行执行的结果是一致的。因此,要确保扫描操作满足结合性。如果扫描操作不满足结合性,则可能导致最终结果错误。

tbb::parallel_for

tbb::parallel_for函数签名和参数

tbb::parallel_forIntel Threading Building Blocks(TBB)库中的一个函数模板,用于在并行环境下进行循环迭代。它可以将一个循环任务划分为多个子任务,并在多个线程上并行执行这些子任务,从而提高程序的并行性和性能。

tbb::parallel_for的函数原型如下:

template<typename BodyType>
void parallel_for(size_t begin, size_t end, const BodyType& body);

这个函数接收三个参数:

  • begin:循环的起始索引
  • end:循环的结束索引(不包含)
  • body:执行循环体的函数对象

parallel_for函数模板的一般用法如下:

tbb::parallel_for(start, end, [capture](int i) {
    
    
    // 循环体代码
});

start:循环的起始值(包含在循环中)。
end:循环的结束值(不包含在循环中)。
[capture]:可选的捕获列表,用于在循环体中访问外部变量。
int i:循环变量,表示当前迭代的索引。
在使用parallel_for时,它会根据系统的线程数和可用的处理器核心数自动划分任务,并在多个线程上并行执行循环体代码。每个线程将负责处理一部分循环迭代,从而实现并行化。

需要注意的是,在循环体代码中对共享资源的访问需要进行适当的同步操作,以避免竞争条件和数据不一致性。

用法示例

一个tbb::parallel_for的简单例子如下:

#include <tbb/parallel_for.h>
#include <iostream>

using namespace tbb;

parallel_for(0, 10, [&](size_t i) {
    
    
  std::cout << "Hello from thread " << i << std::endl; 
});

这个示例中,循环区间是[0, 10),body函数打印当前线程的索引。运行结果可能是:

Hello from thread 5  
Hello from thread 0
Hello from thread 7
Hello from thread 3
Hello from thread 1
Hello from thread 6 
Hello from thread 2
Hello from thread 4
Hello from thread 8
Hello from thread 9

顺序可能不同,但可以看到10次调用确实是并行执行的。

tbb::parallel_for的线程安全问题

需要注意,tbb::parallel_for并不保证body函数内部是线程安全的。如果body函数修改全局变量,就必须自己加锁保证正确性。

下面简单演示如何使用tbb::parallel_for来并行计算向量的平方和:

#include <iostream>
#include <vector>
#include <tbb/parallel_for.h>

int main() {
    
    
    std::vector<int> numbers = {
    
    1, 2, 3, 4, 5};
    int sum = 0;

    tbb::parallel_for(0, numbers.size(), [&numbers, &sum](int i) {
    
    
        sum += numbers[i] * numbers[i];
    });

    std::cout << "Sum of squares: " << sum << std::endl;

    return 0;
}

在上述示例中,parallel_for函数将循环迭代任务划分为多个子任务,并在多个线程上并行执行计算。最终,将计算结果累加到变量sum中,并输出结果。

但是,存在的问题是:多个线程同时修改sum可能导致运算结果错误。解决方法是用mutex或atomic变量保证线程安全:

#include <iostream>
#include <vector>
#include <tbb/parallel_for.h>

int main() {
    
    
    std::vector<int> numbers = {
    
    1, 2, 3, 4, 5};
    int sum = 0;
    std::mutex mutex;

    tbb::parallel_for(0, numbers.size(), [&numbers, &sum](int i) {
    
    
        mutex.lock();
        sum += numbers[i] * numbers[i];
        mutex.unlock();  
    });

    std::cout << "Sum of squares: " << sum << std::endl;

    return 0;
}

tbb::parallel_for的工作拆分机制

tbb::parallel_for使用一种工作拆分(Work Stealing)的机制来实现任务的划分和负载平衡。该机制基于工作窃取者线程模型,其中每个线程都有自己的工作队列,并且可以从其他线程的队列中窃取任务来执行。这样可以使空闲的线程获取更多的任务,从而提高并行性和性能。

具体来说,当调用tbb::parallel_for时,它会根据系统的线程数和可用的处理器核心数将任务划分为多个子任务。每个线程都会接收到一部分迭代任务,并在本地执行。当某个线程完成自己的任务后,它可以从其他线程的任务队列中窃取任务并执行。这种工作窃取机制确保了任务的负载平衡,使得各个线程的工作量尽量均匀。

tbb::parallel_for会将[begin, end)区间拆分为多个子区间,每个子区间由一个线程执行,拆分的规则有:

  • 尽量将区间拆分为大小相等的部分
  • 最小拆分粒度为1(不会拆分成空区间)
  • 动态检测系统线程数,调整拆分粒度

这样的设计可以尽可能发挥多核CPU的能力,同时又能减少线程管理的开销。

如果区间大小不是线程数的整数倍,最后一个子区间的大小可能会稍大一些

下面是一个使用tbb::parallel_for演示工作拆分机制的示例:

#include <iostream>
#include <vector>
#include <tbb/parallel_for.h>
#include <tbb/task_scheduler_init.h>

void WorkFunc(int start, int end) {
    
    
    std::cout << "Thread " << tbb::this_task_arena::current_thread_index() << ": working on range [" << start << ", " << end << ")" << std::endl;
    // 模拟一些计算任务
    for (int i = start; i < end; ++i) {
    
    
        // Do some computation
        std::cout << "Thread " << tbb::this_task_arena::current_thread_index() << ": processing item " << i << std::endl;
    }
}

int main() {
    
    
    // 初始化任务调度器
    tbb::task_scheduler_init init;

    std::vector<int> numbers = {
    
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 使用tbb::parallel_for进行并行循环计算
    tbb::parallel_for(0, numbers.size(), [](int i) {
    
    
        WorkFunc(i, i + 1);
    });

    return 0;
}

当我们运行上述示例代码时,可能会得到以下输出结果的一个示例:

Thread 3: working on range [0, 1)
Thread 3: processing item 0
Thread 3: working on range [1, 2)
Thread 3: processing item 1
Thread 3: working on range [2, 3)
Thread 3: processing item 2
Thread 2: working on range [3, 4)
Thread 2: processing item 3
Thread 2: working on range [4, 5)
Thread 2: processing item 4
Thread 1: working on range [5, 6)
Thread 1: processing item 5
Thread 1: working on range [6, 7)
Thread 1: processing item 6
Thread 0: working on range [7, 8)
Thread 0: processing item 7
Thread 0: working on range [8, 9)
Thread 0: processing item 8
Thread 0: working on range [9, 10)
Thread 0: processing item 9

在上述示例中,我们使用tbb::parallel_fornumbers中的每个元素进行并行计算。为了演示工作拆分机制,我们在WorkFunc函数中打印了线程的工作范围,并模拟了一些计算任务。每个线程都会打印自己负责的迭代范围,并执行相应的计算任务。

通过运行上述示例,可以观察到不同线程的工作范围和任务执行情况。每个线程会根据系统的线程数和可用的处理器核心数自动划分任务,并以一种负载平衡的方式进行执行。线程可以从其他线程的任务队列中窃取任务,以提高并行性和性能。

需要注意的是,在使用tbb::parallel_for时,需要先初始化tbb::task_scheduler_init对象(已废弃),以便为并行循环提供适当的线程池和任务调度器。

总结起来,tbb::parallel_for使用工作拆分机制实现任务的划分和负载平衡,通过将任务分配给不同的线程,并允许线程窃取任务来提高并行性和性能。开发人员可以通过合理设计任务和使用tbb::parallel_for来充分利用多核处理器的计算能力。

注意事项

在使用tbb::parallel_for时,需要注意以下几点以确保正确的并行执行:

  • 数据共享和同步
    并行循环可能涉及对共享数据的访问和修改。在访问共享资源时,需要进行适当的同步操作,以避免竞争条件和数据不一致性。可以使用互斥锁、原子操作或其他同步机制来确保数据的一致性和正确性。

  • 循环迭代的独立性
    并行循环的每次迭代应该是相互独立的,即每个迭代之间没有依赖关系或相互影响。这样可以确保在并行执行时不会出现意外的结果或错误。如果循环迭代之间存在依赖关系,可能需要重新设计算法或使用其他并行编程模式。

  • 任务划分和负载平衡
    tbb::parallel_for会自动将任务划分为多个子任务,并在多个线程上并行执行。然而,任务划分可能会导致负载不平衡,即某些线程的工作量较重,而其他线程的工作量较轻。为了提高并行性和性能,需要确保任务的划分和负载平衡是合理的。

猜你喜欢

转载自blog.csdn.net/qq_40145095/article/details/131783443