《C++并发实例》2.4 运行时选择线程数量
C++ 标准库的一项有帮助的功能是 std:: thread::hardware_ concurrency()。该函数返回当前执行程序可真正并发运行的线程数的指标。例如,多核系统中它可能是CPU的核心数量。这只是一个提示,信息不可获取时它可能返回0,但它在拆分任务线程时非常有用。
listing 2.8 中展示了 std::accumulate 并行版本的简单实现。每个线程使用最少元素量,以避免过多线程的开销。请注意,该实现假设所有操作都不会抛出异常,即使异常确实可能出现;比如,std::thread 构造函数无法启动新的执行线程时就会抛出异常。处理此类算法中的异常超出了这个简单示例的范围,将在第 8 章中介绍。
Listing 2.8 A naïve parallel version of std::accumulate
template <typename Iterator, typename T>
struct accumulate_block
{
void operator()(Iterator first, Iterator last, T &result)
{
result = std::accumulate(first, last, result);
}
};
template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last);
if (!length)
return init; -[1]
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread; -[2]
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads = -[3]
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads; -[4]
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads - 1); -[5]
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size); -[6]
threads[i] = std::thread( -[7]
accumulate_block<Iterator, T>(),
block_start, block_end, std::ref(results[i]));
block_start = block_end; -[8]
}
accumulate_block<Iterator, T>()(
block_start, last, results[num_threads - 1]); -[9]
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join)); -[10]
return std::accumulate(results.begin(), results.end(), init); -[11]
}
虽然函数很长,但实际上很简单。如果输入队列(range)是空[1],你只需要返回初始值 init 。否则,该队列内至少有一个元素,因此您可以将要处理的元素数量除以最小块大小,以获得最大线程数[2]。这是为了避免队列中只有5个元素时,在 32 核计算机上创建 32 个线程。
最小运行线程数的值取你计算出的最大值和硬件线程数中的最小值[3]。您不会想运行超出硬件支持范围的线程的(这叫作c超额订阅oversubscription),因为上下文切换意味着更多线程会降低性能。如果对 std:: thread::hardware_concurrency() 的调用返回 0,您只需替换您选择的数字即可;在这种情况下,我选择了 2。您不会想运行太多线程,因为这会降低单核机器上的速度,但同样您也不想运行太少,因为那样您就会损失可用的并发性。
每个线程要处理的条目数是队列的长度除以线程数 [4]。如果您担心数字不能被除尽,不用担心——稍后您会处理这个问题。
现在您知道有多少个线程,您可以使用 std::vector
启动线程只是一个简单的循环:从指向 block_end 的迭代器移动到当前块的末尾[6],并启动一个线程来计算这个块的结果[7]。下一个块的起始就是这个块的末尾[8]。
启动所有线程后,该线程就可以处理队列中最后一个块[9]。这里你可以考虑未被除尽的部分:你知道最后一个块的结尾是last,而该块中有多少元素并不重要。
一旦你统计了最后一个块的结果,就可以等待使用 std::for_each 生成的所有线程,如listing2.7,然后通过最终调用 std::accumulate 将结果相[11]。
在离开这个示例之前,值得指出的是,类型 T 的额外运算符并不相关(例如对于 float 或 double),parallel_accumulate 的运算结果会根据 std::accumulate 的结果变化,因为队列分成了块。此外,对迭代器的要求稍微严格一些:它们必须至少是前向迭代器(forwaord iterators),
而 std::accumulate 可以使用单向的输入迭代器(input iterator),并且 T 必须是默认可构造的,以便您可以创建保存结果的vector。此类需求变化在并行算法中很常见;就其本质而言,为了保证并发它们在一些程度上有不同,而这会对结果和需求有影响。第 8 章更深入地介绍了并发算法。。还值得注意的是,因为无法直接从线程返回值,所以必须传递对保存结果vetor的引用。另一种方法使用 future 返回结果将在第四章介绍。
在这种情况下,线程所需的所有信息都是在线程启动时传入的,包括存储其计算结果的地址。况并非总是如此:有时在运行的某个阶段你需要标识线程。你可以传递一个标示号,像 listing2.7 中那样。但如果需要在静态调用或者线程内部调用中标识某个线程,这样会很不方便。当我们设计 C++ 线程库时,我们预见到了这种需求,因此每个线程都有一个唯一的标识符。
浙公网安备 33010602011771号