【问题标题】:Forwarding parameters to lambda for for subsequent asynchronous call将参数转发给 lambda 以供后续异步调用
【发布时间】:2020-12-01 10:03:49
【问题描述】:

我正在尝试以尽可能低的开销(最好是 C++14)来实现简单的线程池。

总体思路是将任务“打包”到不带参数的 lambda(但带有捕获列表),并提供一个公共函数来将任务添加到线程池。

让我们考虑下面这个简单(而且丑陋)的例子,说明问题(不是线程池本身,而是带有帮助类VerboseStr 的简化代码sn-p)。

#include <iostream>
#include <string>
#include <functional>
#include <thread>

//  Small helper macros
#define T_OUT(str) std::cout << Ident() \
  << "[" << std::to_string(id_) << "] " \
  << str << std::endl; \

#define T_OUT_FROMTO(str) std::cout << Ident() \
  << "[" << std::to_string(i.id_) << "->" << std::to_string(id_) << "] " \
  << str << std::endl; \

#define T_SLEEP(ms) std::this_thread::sleep_for(std::chrono::milliseconds(ms));

//  Just a verbose class, holding std::string inside
/////////////////////////////////////////////////////////
class VerboseStr
{
  std::string val_;
  int id_;

  static int GetId() { static int id = 0; return ++id; }

  //  Several spaces to ident lines
  std::string Ident() const { return std::string(id_, ' '); }
public:
  VerboseStr() : id_(GetId())
  {
    T_OUT("Default constructor called");
  };

  ~VerboseStr()
  {
    val_ = "~Destroyed!";
    T_OUT("Destructor called");
  }

  VerboseStr(const std::string& i) : val_(i), id_(GetId())
  {
    T_OUT("Create constructor called");
  };

  VerboseStr(const VerboseStr& i) : val_(i.val_), id_(GetId())
  {
    val_ = i.val_;
    T_OUT_FROMTO("Copy constructor called");
  };

  VerboseStr(VerboseStr&& i) noexcept : val_(std::move(i.val_)), id_(GetId())
  {
    T_OUT_FROMTO("Move constructor called");
  };

  VerboseStr& operator=(const VerboseStr& i)
  { 
    val_ = i.val_;
    T_OUT_FROMTO("Copy operator= called");
    return *this;
  }

  VerboseStr& operator=(VerboseStr&& i) noexcept
  { 
    val_ = std::move(i.val_);
    T_OUT_FROMTO("Move operator= called");
    return *this;
  }

  const std::string ToStr() const { return std::string("[") + std::to_string(id_) + "] " + val_; }
  void SetStr(const std::string& val) { val_ = val; }
};
/////////////////////////////////////////////////////////

//  Capturing args by VALUES in lambda
template<typename Fn, typename... Args>
void RunAsync_V(Fn&& func, Args&&... args)
{
  auto t = std::thread([func_ = std::forward<Fn>(func), args...]()
  {    
    T_SLEEP(1000);  //  "Guarantees" async execution
    func_(args...);
  });
  t.detach();
}

void DealWithVal(VerboseStr str)
{
  std::cout << "Str copy: " << str.ToStr() << std::endl;
}

void DealWithRef(VerboseStr& str)
{
  std::cout << "Str before change: " << str.ToStr() << std::endl;
  str.SetStr("Changed");
  std::cout << "Str after change: " << str.ToStr() << std::endl;
}

// It's "OK", but leads to 2 calls of copy constructor
//  Replacing 'str' with 'std::move(str)' leads to no changes
void Test1()
{
  VerboseStr str("First example");

  RunAsync_V(&DealWithVal, str);
}

//  It's OK
void Test2()
{
  VerboseStr str("Second example");

  RunAsync_V(&DealWithRef, std::ref(str));

  //  Waiting for thread to complete...
  T_SLEEP(1500);

  //  Output the changed value of str
  std::cout << "Checking str finally: " << str.ToStr() << std::endl;
}

int main()
{
  Test1();
//  Test2();

  T_SLEEP(3000);  //  Give a time to finish
}

正如上面评论所说,问题出在Test1() 函数中。

很明显,在Test1() 的上下文中,异步调用函数DealWithVal 的唯一可能方法是将str“移动”到lambda 主体。

Test1()main()调用时,输出如下:

 [1] Create constructor called
  [1->2] Copy constructor called
   [2->3] Move constructor called
  [2] Destructor called
 [1] Destructor called
    [3->4] Copy constructor called
Str copy: [4] First example
    [4] Destructor called
   [3] Destructor called

如我们所见,复制构造函数有 2 次调用。

考虑到按值传递(不移动)和按引用(看看Test2())也应该可用,我无法实现这一点。

请帮助解决问题。提前致谢。

【问题讨论】:

标签: c++ templates asynchronous lambda perfect-forwarding


【解决方案1】:

您的两份副本来自:

auto t = std::thread([func_ = std::forward<Fn>(func), args...]()
                                                   // ^^^^^^^ here
{    
  T_SLEEP(1000);  //  "Guarantees" async execution
  func_(args...);
     // ^^^^^^^ and here

您复制到 lambda 中,这很好,而且这是您可以做的所有事情,因为您被传递了一个左值引用。然后将 args from lambda 复制到按值函数中。但是按照设计,您现在拥有 args 并且 lambda 没有其他用途,因此您应该将它们从 lambda 中移出。即:

auto t = std::thread([func_ = std::forward<Fn>(func), args...]() mutable
                                                              // ^^^^^^^
{    
  T_SLEEP(1000);  //  "Guarantees" async execution
  func_(std::move(args)...);
     // ^^^^^^^^^^^^^^^

这会将您减少到一份必要的副本。


另一个答案隐式地将传递的左值包装在 reference_wrappers 中以获得零个副本。调用者需要维护异步生命周期的值,但在调用站点没有明确的记录。它与类似功能的预期背道而驰(例如,std::thread 应用了一个 decay_copy 并要求调用者在他们想要的情况下将其包装在一个引用包装器中)。

【讨论】:

  • Gattet,感谢您的参与!你的版本给了我新的想法。请看以下内容 - link。此时,我正在尝试简化解决方案 - RunAsync_V2,它接受 VALUE 本身的参数(特别是,它强制调用者在适当的时候使用 std::ref,但对我来说没关系)。但我无法意识到,如何在 C++14 的 lambda 捕获中“移动”参数包。描述在 Test1() 中的 cmets 中
  • 对不起,我在之前的评论中输入了你的名字不正确:( PS。如果“移动”参数包在 C++14 中根本不可能——我宁愿提供几个版本的 @ 987654325@ 在我的线程池中,允许传递 1,2,3... 参数。当然,最好有更通用的解决方案。
  • 理想情况下,你想写的应该是RunAsync 转发引用Args&amp;&amp;... args,以便尽可能长时间地保持引用,然后你会初始化 lambda通过转发[func_ = std::forward&lt;Fn&gt;(func), ...args_ = std::forward&lt;Args&gt;(args)]() mutable 捕获按值,从而在可能的情况下移动到 lambda,如果没有则复制。然后在 lambda 中,您将进入呼叫 std::move(func_)(std::move(args_)...)(或者更好的是 std::invoke)。你可以写这个,但是用 C++20。 :(
  • 可变参数 lambda 初始化器是 C++14 中不可用的。但是,您可以通过一个元组走私来编写基本相同的内容:[func_ = std::forward&lt;Fn&gt;(func), args_ = std::tuple&lt;Args...&gt;(std::forward&lt;Args&gt;(args)...)]() mutable。这与 Jarod42 的更新答案非常接近(它具有 Run 取值,并移动到 lambda,导致相对于通过引用获取和转发到 lambda 的额外移动 - 一个微小的差异)。
  • 那么当然,在 lambda 中,你需要像 std::apply 这样的东西,它在 C++17 中:apply(std::move(func_), std::move(args_))(我想我不会费心去模仿 std::invoke在 C++17 之前,但你可以根据你的需要)。我不确定为什么 Jarod42 用 lambda 将调用包装在 apply 内。要实现apply,您可以从cppreference 复制示例实现,例如(将std::invoke 替换为调用表达式)。编码愉快!
【解决方案2】:

您可以将参数捕获为std::tuple,如下所示:

//  Passing args by VALUE, Capturing moved args
template<typename Fn, typename... Args>
void RunAsync_V2(Fn&& func, Args... args)
{
    auto t = std::thread([func_ = std::forward<Fn>(func), tup = std::tuple<Args...>(std::move(args)...)]() mutable
    {
        T_SLEEP(1000);

        std::apply([&](auto&&...args){func_(std::move(args)...);}, std::move(tup));
    });
    t.detach();
}

Demo

没有副本,只有移动完成:

[1] Create constructor called
  [1->2] Copy constructor called
   [2->3] Move constructor called
    [3->4] Move constructor called
   [3] Destructor called
  [2] Destructor called
 [1] Destructor called
     [4->5] Move constructor called
Str copy: [5] First example
     [5] Destructor called
    [4] Destructor called

【讨论】:

  • 似乎在这种情况下我们失去了按值传递参数的机会。我注意到您已经更改了 Test1() 代码。但是如果我们返回我的版本(没有 std::move 并且没有在 Test1() 范围内休眠) - 测试失败,因为 std::is_lvalue_reference::value 为真。
  • 不清楚你对左值/右值的期望。
  • 例如,在 Test1()(初始版本)中,str 将在调用 RunAsync_V 后立即被销毁,因此通过引用传递 str 根本没有意义。
  • 好的,已更改,因此 lambda 拥有其捕获。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-04
  • 2012-11-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-24
相关资源
最近更新 更多