《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 保存中间结果,使用std::vector< std::thread> 保存线程。请注意,您需要启动比 num_threads 少一个的线程,因为您已经拥有一个线程。

启动线程只是一个简单的循环:从指向 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++ 线程库时,我们预见到了这种需求,因此每个线程都有一个唯一的标识符。

posted @ 2024-06-13 21:05  李思默  阅读(91)  评论(0)    收藏  举报