将参数转发给 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();
}
没有副本,只完成移动:
[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 并要求调用者包装在引用包装器中,如果这是他们想要的)。
我正在尝试以尽可能低的开销(最好是在 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();
}
没有副本,只完成移动:
[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 并要求调用者包装在引用包装器中,如果这是他们想要的)。