c++ - 在多线程代码中转发

标签 c++ multithreading c++11 perfect-forwarding

我正在研究一系列优化算法的抽象。这些算法可以串行或多线程运行,使用锁定机制或原子操作。

当涉及到算法的多线程版本时,我有一个关于完美转发的问题。比方说,我有一些我不愿意复制的仿函数,因为它很昂贵。我可以确保仿函数是静态的,因为调用它们的 operator()(...) 不会改变对象的状态。一个这样的虚拟仿函数如下:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <thread>
#include <vector>

template <class value_t> struct WeightedNorm {
  WeightedNorm() = default;
  WeightedNorm(std::vector<value_t> w) : w{std::move(w)} {}

  template <class Container> value_t operator()(Container &&c) const & {
    std::cout << "lvalue version with w: " << w[0] << ',' << w[1] << '\n';
    value_t result{0};
    std::size_t idx{0};
    auto begin = std::begin(c);
    auto end = std::end(c);
    while (begin != end) {
      result += w[idx++] * *begin * *begin;
      *begin++ /* += 1 */; // <-- we can also modify
    }
    return result; /* well, return std::sqrt(result), to be precise */
  }

  template <class Container> value_t operator()(Container &&c) const && {
    std::cout << "rvalue version with w: " << w[0] << ',' << w[1] << '\n';
    value_t result{0};
    std::size_t idx{0};
    auto begin = std::begin(c);
    auto end = std::end(c);
    while (begin != end) {
      result += w[idx++] * *begin * *begin;
      *begin++ /* += 1 */; // <-- we can also modify
    }
    return result; /* well, return std::sqrt(result), to be precise */
  }

private:
  std::vector<value_t> w;
};

这个仿函数可能也有它的一些成员函数的引用限定符,如上所示(尽管在上面,它们彼此没有区别)。此外,允许函数对象修改其输入 c。为了将这个仿函数正确地转发给算法中的工作线程,我想到了以下几点:

template <class value_t> struct algorithm {
  algorithm() = default;
  algorithm(const unsigned int nthreads) : nthreads{nthreads} {}

  template <class InputIt> void initialize(InputIt begin, InputIt end) {
    x = std::vector<value_t>(begin, end);
  }

  template <class Func> void solve_ref_1(Func &&f) {
    std::vector<std::thread> workers(nthreads);
    for (auto &worker : workers)
      worker = std::thread(&algorithm::kernel<decltype((f)), decltype(x)>, this,
                           std::ref(f), x);
    for (auto &worker : workers)
      worker.join();
  }

  template <class Func> void solve_ref_2(Func &&f) {
    auto &xlocal = x;
    std::vector<std::thread> workers(nthreads);
    for (auto &worker : workers)
      worker = std::thread([&, xlocal]() mutable { kernel(f, xlocal); });
    for (auto &worker : workers)
      worker.join();
  }

  template <class Func> void solve_forward_1(Func &&f) {
    std::vector<std::thread> workers(nthreads);
    for (auto &worker : workers)
      worker = std::thread(
          &algorithm::kernel<decltype(std::forward<Func>(f)), decltype(x)>,
          this, std::ref(f), x); /* this is compilation error */
    for (auto &worker : workers)
      worker.join();
  }

  template <class Func> void solve_forward_2(Func &&f) {
    auto &xlocal = x;
    std::vector<std::thread> workers(nthreads);
    for (auto &worker : workers)
      worker = std::thread(
          [&, xlocal]() mutable { kernel(std::forward<Func>(f), xlocal); });
    for (auto &worker : workers)
      worker.join();
  }

private:
  template <class Func, class Container> void kernel(Func &&f, Container &&c) {
    std::forward<Func>(f)(std::forward<Container>(c));
  }

  std::vector<value_t> x;
  unsigned int nthreads{std::thread::hardware_concurrency()};
};

基本上,我在写上述代码时想到的是 algorithm::solve_ref_1algorithm::solve_ref_2 彼此的不同之处仅在于 lambda 的使用功能。最后,它们都调用了 kernel 并带有对 f 的左值引用和对 x 的左值引用,其中 x由于 std::thread 的工作方式或通过在 lambda 中复制捕获 xlocal,code> 被复制到每个线程中。它是否正确?我应该小心选择其中之一吗?

到目前为止,我无法实现我想要实现的目标。我没有对 f 进行不必要的复制,但我也没有尊重它的引用限定符。于是,想到了将f转发给kernel。在上面,由于删除了右值引用的 std::ref 构造函数,我找不到使 algorithm::solve_forward_1 编译的方法。但是,使用 lambda 函数方法的 algorithm::solve_forward_2 似乎可以正常工作。 “似乎在工作”,我的意思是下面的主程序

int main(int argc, char *argv[]) {
  std::vector<double> x{1, 2};
  algorithm<double> alg(2);
  alg.initialize(std::begin(x), std::end(x));

  alg.solve_ref_1(WeightedNorm<double>{{1, 2}});
  alg.solve_ref_2(WeightedNorm<double>{{1, 2}});
  // alg.solve_forward_1(WeightedNorm<double>{{1, 2}});
  alg.solve_forward_2(WeightedNorm<double>{{1, 2}});

  return 0;
}

编译并打印以下内容:

./main.out
lvalue version with w: 1,2
lvalue version with w: 1,2
lvalue version with w: 1,2
lvalue version with w: 1,2
rvalue version with w: 1,2
rvalue version with w: 1,2

简而言之,我有两个主要问题:

  1. 有什么理由让我更喜欢 lambda 函数版本而不是其他版本(反之亦然),并且,
  2. 在我的情况下是否允许/OK 多次完美转发仿函数 f

我在上面问 2.,因为在 the answer 中对于另一个问题,作者说:

You cannot forward something more than once, though, because that makes no sense. Forwarding means that you're potentially moving the argument all the way through to the final caller, and once it's moved it's gone, so you cannot then use it again.

我假设,在我的例子中,我没有移动任何东西,而是试图尊重引用限定符。在我的主程序的输出中,我可以看到 w 在右值版本中具有正确的值,1, 2,但这并不意味着我正在做一些未定义的行为,例如尝试访问已经移动的 vector 的值。

如果您能帮助我更好地理解这一点,我将不胜感激。我也乐于接受有关我尝试解决问题的方式的任何其他反馈。

最佳答案

  1. 没有理由更喜欢这两者
  2. for 内转发循环不行。您不能将同一个变量转发两次:

template <typename T> void func(T && param) { func1(std::forward<T>(param)); func2(std::forward<T>(param)); // UB }

另一方面,链转发 ( std::forward(std::forward(…)) ) 没问题。

关于c++ - 在多线程代码中转发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49239642/

相关文章:

c++ - 控制流 C++

c++ - OpenCV Flann - 断言失败

c++ - mmap + madvise 真的是异步 I/O 的一种形式吗?

c# - 游戏逻辑的最快迭代方法 : thread, 空闲处理程序、winproc 或其他我不知道的东西?

c++11 - 未解析的 std::__cxx11::basic_string::~basic_string 和 std::allocator<char>::~allocator() 符号

c++ - 是否存在比较不比较完整对象状态的常规类型的概念名称?

c# - 如何在 C# 中生成线程

c - GCC 不合格?

c++ - STL 容器在该类的声明中持有类

c++ - 成员函数模板特化可以具有与主模板不同的访问级别吗?