【发布时间】: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())也应该可用,我无法实现这一点。
请帮助解决问题。提前致谢。
【问题讨论】:
-
为什么是这些宏? ://
-
@Jarod42 ,对不起,是的,我应该创建函数。虽然在讨论的问题的背景下,这可能无关紧要......
-
@Jarod42 ,谢谢,这很有趣。但是可以和capture子句中的参数包一起使用吗?
标签: c++ templates asynchronous lambda perfect-forwarding