Параллельное выполнение изменяемого лямбда-генератора для std :: generate_n - PullRequest
4 голосов
/ 09 апреля 2019

При использовании параллельного выполнения для 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 потокобезопасным?

Ответы [ 2 ]

3 голосов
/ 09 апреля 2019

Прежде всего, давайте посмотрим на подпись generate_n:

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

Важно, чтобы последний аргумент (это ваша лямбда) передавался значением . Также вы не знаете, как это происходит внутри реализации, поэтому может быть некоторое количество копий вашей лямбды, и у каждой из них будет свой счетчик. Я полагаю, что это не намерение.

Существует несколько вариантов совместного использования счетчика между экземплярами:

  1. Используйте 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 голос
/ 09 апреля 2019

Является ли доступ к захваченному 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++; });

или, если вы настаиваете на лямбда-захвате вместе с ключевым словом 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 даже не должны компилироваться.Следовательно, первый пример явно предпочтителен и, возможно, даже единственный выполнимый.

...