用于std :: generate_n的并行执行可变lambda生成器

问题描述 投票:4回答:2

当使用在其捕获中具有初始化器的可变lambda对std :: generate_n使用并行执行时,是否可以并行访问初始化值线程安全?

[MCVE]

#include<vector>
#include <algorithm>
#include <execution>

int main()
{
  std::vector<int> v(1000);
  std::generate_n(std::execution::par, v.data(), v.size(), [i = 0]() mutable { return i++; });

  return 0;
}

是否可以访问捕获的i线程安全?

c++ lambda parallel-processing c++17
2个回答
1
投票

是否可以访问捕获的i线程安全?

不。客户端代码负责确保不会发生数据争用。你能做的就是这个(从cppreference复制和定制)

int i = 0;
std::mutex m;

std::generate_n(std::execution::par, v.data(), v.size(), [&]() {
    std::lock_guard<std::mutex> guard(m);    
    return i++; });

或者,如果你坚持lambda捕获与mutable关键字:

std::generate_n(std::execution::par, v.data(), v.size(),
    [i = 0, m = std::mutex()] () mutable  {
        std::lock_guard<std::mutex> guard(m);    
        return i++; });

请注意,正如@Eric在评论中指出的那样,@ DmitryGordon在他的回答中指出,std::generate_n可能会复制函数对象。这是有问题的,因为每个复制的实例都有自己的计数器i,该计数器独立于其他实例递增。另请注意,@ rubenvb指出std::generate_n中的函数对象的副本甚至无法编译。因此,第一个例子显然是可取的,甚至可能是唯一可行的例子。


3
投票

首先,让我们看一下generate_n的签名:

template< class ExecutionPolicy, class ForwardIt , class Size, class Generator >
ForwardIt generate_n(ExecutionPolicy&& policy, ForwardIt first, Size count, Generator g);

重要的是最后一个参数(它是你的lambda)是按值传递的。另外你不知道它是如何在实现中内部传递的,所以你的lambda可能有一些副本,每个副本都有它自己的计数器。我想这不是意图。

在实例之间共享计数器有几种选择:

  1. 在lambda上使用std :: ref: const auto func = [i = std::atomic<int>()]() mutable -> int { return i++; }; std::vector<int> v(1000); std::generate_n(std::execution::par, v.data(), v.size(), std::ref(func));
  2. 在仿函数实例之间共享计数器: std::atomic<int> i = 0; std::vector<int> v(1000); std::generate_n(std::execution::par, v.data(), v.size(), [&i]() -> int { return i++; });

请注意,在这两种情况下我都使用std :: atomic,因为您需要自己处理同步。