我正在为一系列优化算法进行抽象。这些算法可以使用锁定机制或原子操作来串行或多线程运行。
对于算法的多线程版本,我有一个关于完美转发的问题。举例来说,我有一些仿函数,因为价格昂贵,所以我不愿意复制。我可以确保函子是静态的,因为对函子的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_1
和algorithm::solve_ref_2
仅在使用lambda函数方面彼此不同。最后,他们两个都用kernel
的左值引用和f
的左值调用x
,其中x
由于std::thread
的工作方式或捕获而在每个线程中复制xlocal
副本中的f
。它是否正确?我应该谨慎选择一个吗?到目前为止,我还无法完成我想实现的目标。我没有制作不必要的
f
副本,但是我也没有遵守其参考限定词。然后,我想到了将kernel
转发到algorithm::solve_forward_1
。上面,由于删除了用于右值引用的std::ref
构造函数,我找不到编译algorithm::solve_forward_2
的方法。但是,使用lambda函数方法的f
似乎正在工作。 “似乎有效”是指以下主要程序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
简而言之,我有两个主要问题:
有什么理由让我比其他版本更喜欢lambda函数版本,反之亦然?
在我的情况下,是否可以多次完美转发函子
w
/好吗?我在上面问2.,因为在the answer中另一个问题,作者说:
但是,您不能转发多次,因为这没有任何意义。转发意味着您可能会将参数一直移动到最终调用者,并且一旦移动它就消失了,因此您不能再使用它。
我认为,就我而言,我没有采取任何行动,而是试图尊重参考资格。在主程序的输出中,我可以看到
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/