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

时间:2019-04-09 10:20:13

标签: c++ lambda parallel-processing c++17

当使用可变的lambda对std :: generate_n进行并行执行时,该可变lambda的捕获中包含初始化程序,那么并行访问初始化值是线程安全的吗?

[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线程安全吗?

2 个答案:

答案 0 :(得分: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,因为您需要自己进行同步。

答案 1 :(得分: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中的函数对象的副本甚至应该无法编译。因此,第一个示例显然是可取的,甚至可能是唯一可行的示例。