将参数转发给 lambda 用于后续的异步调用

Forwarding parameters to lambda for for subsequent asynchronous call

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

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

让我们考虑以下简单(丑陋)的示例,说明问题(它不是线程池本身,而是带有帮助程序的简化代码片段 class VerboseStr)。

#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 主体。

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

 [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())应该也可用,我不知道如何实现它。

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

您可以将参数捕获为 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

您的两份副本来自:

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

你复制到 lambda 中,这很好,而且这是你能做的,因为你被传递了一个左值引用。然后将 args 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 并要求调用者包装在引用包装器中,如果这是他们想要的)。