这个问题是这个问题的扩展,因为它是一个类似的问题,但并没有完全回答我的问题(更不用说这个问题中的问题不涉及线程/多线程)。
问题
正如标题所示,我遇到了一个关于lambdas和std::thread
的特定问题,我的背景知识对我没有帮助。我实际上是在试图将包含成员函数的std::function
存储在一个静态类中,稍后将在一个单独的std::thread
中调用。问题是,该成员函数需要引用的this
实例指针,因为该成员函数修改了同一类的数据成员,但通过引用传递到lambda的this
指针在调用该成员函数时无效/销毁,这会导致看似未定义的行为。这个问题实际上在提案文档P0018R3中有一些讨论,其中讨论了lambdas和并发性。
半最小工作示例
//Function class
struct FuncWrapper {
//random data members
std::string name;
std::string description;
//I still have this problem even if this member is const
std::function<void()> f;
};
class FuncHolder {
public:
//For the FuncWrapper argument, I've tried pass by reference, const-reference, rvalue, etc. doesn't make a difference in terms of the problem (as I expect)
//add a FuncWrapper object to the static holder
static void add(FuncWrapper f) {
return const_cast<std::vector<FuncWrapper>&>(funcs).push_back(f);
}
//Same here with the return
//get a static FuncWrapper object by index (with bounds checking)
static const std::function<void()>& get(size_t index) {
return funcs.at(index);
}
private:
//C++17 inline static definition; If I weren't using it I would do it the non-inline way
const static inline std::vector<FuncWrapper> funcs;
};
//Data member class
struct Base {
//This is meant to be like a constexpr; its unique for each derived type
inline virtual const char* const getName() const = 0;
protected:
//This will be important for a solution I tried
const size_t storedIdx = 0;
};
class Derived : public Base {
public:
Derived(){
FuncHolder::add({"add5", "adds 5",
[&](){increment(5);} //This is the problem-causing statement
});
FuncHolder::add({"add1", "adds 1",
std::bind(&FuncHolder::increment, 1) //This syntax also causes the same problem
});
}
inline const char* const getName() const override {
return "Derived";
}
void increment(int amount){
//Do some stuff...
privMember += amount;
//Do some other stuff..
}
private:
int privMember = 0;
};
//Class to hold the static instances of the different derived type
class BaseHolder {
public:
//make a new object instace in the static vector
template<class DerivedTy>
static void init(const DerivedTy& d){
static_assert(std::is_base_of_v<Base, DerivedTy>); //make sure it's actually derived
size_t idx = baseVec.size(); //get the index of the object to be added
const_cast<std::vector<std::unique_ptr<Base>>&>(baseVec).emplace_back(std::make_unique<DerivedTy>(d)); //forward a new unique_ptr object of derived type
const_cast<size_t&>(baseVec.at(idx)->storedIdx) = idx; //store the object's index in the object itself
}
///This function is used later for one of the solutions I tried; it goes unused for now
///So, assume the passed size_t is always correct in this example
///There's probably a better way of doing this, but ignore it for the purposes of the example
//get a casted reference to the correct base pointer
template<class DerivedTy>
static DerivedTy& getInstance(size_t derivedIdx){
return *(static_cast<DerivedTy*>(baseVec.at(derivedIdx).get()));
}
private:
//C++17 inline static again
const static inline std::vector<std::unique_ptr<Base>> baseVec{};
};
int main() {
BaseHolder::init(Derived()); //add a new object to the static holder
//Do stuff...
std::thread runFunc([&](){
FuncHolder::Get(0)(); //Undefined behavior invoked here; *this pointer used in the function being called is already destroyed
});
//Main thread stuff...
runFunc.join();
return 0;
}
这可能不是一个非常简单的例子,但我想强调重要的细节(例如函数是如何存储的以及调用它们的类),这样就可以清楚地了解问题是如何产生的。
设计中还有一些可能不相关但重要的部分需要指出。
- 预期有许多类/类型派生
Base
类(即Derived1
、Derived2
等),但这些派生类中的每一个只能有一个实例;因此,BaseHolder
类的所有成员都是静态的。因此,如果这个设计确实需要重新设计,请记住这一点(尽管老实说,这可能会以比现在更好的方式实现,但这可能与问题无关) - 使
BaseHolder
类模板化,并在编译时将我希望它保持的类/类型传递给它的模板可能是本能的(因此使用类似tuple
而不是vector
的东西),但我不是故意这样做的,因为我可能需要在稍后的运行时添加更多的派生类型 - 我真的无法更改
f
(std::function<>
)的模板类型,因为我可能需要传递具有自己的返回类型和参数的不同函数(这就是为什么我有时使用lambda,有时只希望调用的是具有void返回类型的函数时使用std::bind
)。为了实现这一点,我只将其制作为std::function<void()>
- 此设计的总体目标是静态调用和调用一个函数(就好像它是由事件触发的一样),该函数在被调用之前已经构造好,并且能够修改给定的类(在这种情况下,特别是它在-
Derived
中构造的类)
问题来源
通过研究这个问题,我知道在lambda中通过引用捕获的this
指针可能会在lambda在不同线程中运行时失效。使用我的调试器,当在Derived
的构造函数中构造lambda时,this
指针似乎被破坏了,这与我以前的知识相悖,所以我不能100%确定这是怎么回事;调试器显示Derived
的整个this
实例已填充垃圾值或不可读
Derived(){
FuncHolder::add({"add5", "adds 5", //`this` pointer is fine here
[&](){increment(5);} //`this` pointer is filled with junk and pointing to a different random address
});
//...
}
不过,我更确定的是,当lambda/函数被调用/运行时,由于其Derived
指针的this
实例似乎已损坏,会出现未定义的行为。我每次都会从不同的文件中得到不同的异常,有时只是得到一个完全的访问读取访问违规,有时则不然。调试器在调用lambda时也无法读取其this
指针的内存;所有这些看起来都像是指针被毁的迹象。
我以前也在lambdas中处理过这种类型的问题,知道在不涉及std::thread
时该怎么办,但线程似乎会使事情复杂化(我稍后会对此进行详细解释)。
我尝试过的
按价值捕获
最简单的解决方案是让lambda通过值捕获Derived
的this
指针(正如前面提到的问题的答案和提案文档P0018R3中所提到的),因为我使用的是C++17。提案文件甚至提到了按值捕获this
对于并发应用程序(如线程:)是必要的
Derived(){
FuncHolder::add({"add5", "adds 5",
[&, *this](){increment(5);} //Capture *this by value (C++17); it's thread-safe now
});
//...
}
问题是,正如我所说,传递到lambda中/由lambda捕获的函数需要修改类的数据成员;如果我通过值捕获this
,那么函数只是修改Derived实例的副本,而不是预期的副本。
使用静态实例
好吧,如果每个派生类只有一个静态实例,并且派生类有一个静态持有者,为什么不在lambda中使用静态实例并直接修改该实例呢?:
Derived(){
FuncHolder::add({"add5", "adds 5",
[=](){BaseHolder::getInstance(storedIdx)::increment(5);} //use static instance in lambda; Again assume the passed index is always correct for this example
});
//...
}
这在纸面上可能看起来不错,但问题是在使用构造函数创建实际实例之前,在构造函数中调用了getInstance()
。具体来说,BaseHolder::init(Derived())
调用Derived()
构造函数,其中init
首先尝试在向量中创建实例;但是,向量在Derived()
构造函数中被访问,该构造函数在init
被调用之前被调用。
将静态实例传递给成员函数
上述问题中的另一个答案是,更改lambda中的函数,使其具有一个参数,该参数取其类的一个实例。在我们的例子中,它看起来像这样:
class Derived : public Base {
public:
Derived(){
FuncHolder::add({"add5", "adds 5",
[&](){increment(BaseHolder::getInstance(storedIdx), 5);} //Pass the static instance to the actual function
});
//...
}
//rest of the class...
void increment(Derived& instance, int amount){
//Do some stuff...
instance.privMember += amount;
//Do some other stuff..
}
private:
int privMember = 0;
};
但这与之前尝试的解决方案(使用lambda中的静态实例)存在相同的问题:静态实例尚未创建,因为它正在调用访问实例的构造函数来创建它
this
的shared_ptr
(直接)
在上述问题中不止一次提到的解决方案是制作并使用this
的shared_ptr
(或任何智能指针)来延长其寿命,以及不延长其寿命的原因(尽管答案没有深入讨论如何实现它)。快速而肮脏的方法是直接:
Derived(){
FuncHolder::add({"add5", "adds 5",
[self=std::shared_ptr<Derived>()](){self->increment(5);} //pass a shared_ptr of *this; syntax can differ
});
//...
}
这样做的问题是,您会得到一个std::bad_weak_ptr异常,因为这样做可能无论如何都是不正确的(或者至少我认为是这样)。
this
的shared_ptr
(std::enable_shared_from_this<T>
)
这篇博客文章中的解决方案,以及我通常在不涉及线程时使用的解决方案是,使用std::enable_shared_from_this<T>::shared_from_this
来捕获this
:的适当shared_ptr
class Derived : public Base, public std::enable_shared_from_this<Derived> {
Derived(){
FuncHolder::add({"add5", "adds 5",
[self=shared_from_this()](){self->increment(5);} //pass a shared_ptr of *this
});
//...
}
//rest of class...
}
这在纸面上看起来不错,并没有真正引起任何例外,但似乎没有改变任何事情;问题仍然存在,它似乎与正常参考捕获this
没有什么不同。
结论
我可以防止lambda中派生类的this
指针在另一个线程中被调用时被破坏/无效吗?如果没有,做我正在努力实现的目标的正确方法是什么?也就是说,我怎样才能在保留我的设计原则的同时重新设计,使其正常工作?
我认为有很多解决方案可以在这里工作,但它不是一个"短";要描述的问题。以下是我对两种方法的看法:
1-促进意图
看起来您需要在对象和lambda之间共享一些状态。重写生存期很复杂,所以按照你的意愿去做,并将公共状态提升到一个共享指针中:
class Derived // : Base classes here
{
std::shared_ptr<DerivedImpl> _impl;
public:
/*
Public interface delegates to _impl;
*/
};
这就提供了与共享状态进行两种交互的能力:
// 1. Keep the shared state alive from the lambda:
func = [impl = _impl]() { /* your shared pointer is valid */ };
// 2. Only use the shared state if the original holder is alive:
func = [impl = std::weak_ptr(_impl)]() {
if (auto spt = impl.lock())
{
// Use the shared state
}
};
毕竟,enable_shared_from_this
方法对您不起作用,因为您一开始没有共享状态。通过在内部存储共享状态,您可以保留原始设计的值语义,并在生命周期管理变得复杂的情况下方便使用。
2-将状态放在安全的地方
保证某种状态的最安全的方式是";"活着";在某一点上是把它放在一个更高的范围:
- 静态存储/全局范围
- 一个堆栈帧,它比用户、派生类和lambda都高(或低,具体取决于您的心理内存模型)
具有这样的";存储";将允许你做";放置新的";在不使用免费存储(堆)的情况下构造它们。然后将此工具与引用计数配对,这样最后一个引用派生对象的将是调用析构函数的。
这个方案实现起来并不简单,只有当这个层次结构有主要的性能要求时,你才应该争取它。如果你决定走这条路,你也可以考虑内存池,它提供了这种终身操作,并提前解决了许多相关的难题。
以下是基于实际源代码的更新版本。
#include <future>
#include <functional>
#include <iostream>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
//-------------------------------------------------------------------------------------------------
//Function class
struct FuncWrapper
{
//random data members
std::string name;
std::string description;
std::function<void()> f;
void operator()() const
{
std::cout << "Calling '" << name << "', description = '" << description << "'n";
f();
}
};
//-------------------------------------------------------------------------------------------------
class FuncHolder
{
public:
size_t add(const FuncWrapper& wrapper)
{
std::unique_lock<std::mutex> lock{ m_mtx };
auto id = funcs.size();
funcs.push_back(wrapper);
return id;
}
const FuncWrapper& get(size_t index)
{
std::unique_lock<std::mutex> lock{ m_mtx };
return funcs.at(index);
}
~FuncHolder() = default;
// meyer's singleton, note this design is threadsafe by nature of C++11 or later
// https://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton
static FuncHolder& Instance()
{
static FuncHolder instance;
return instance;
}
private:
FuncHolder() = default;
std::vector<FuncWrapper> funcs;
std::mutex m_mtx;
};
//-------------------------------------------------------------------------------------------------
// Data member base class
class Base
{
public:
explicit Base(const size_t& id) :
storedIdx(id)
{
}
//This is meant to be like a constexpr; its unique for each derived type
inline virtual const std::string& getName() const = 0;
protected:
//This will be important for a solution I tried
const size_t storedIdx = 0;
};
//-------------------------------------------------------------------------------------------------
class Derived :
public Base
{
public:
explicit Derived(const size_t& id) :
Base(id),
m_name{ "Derived" }
{
FuncHolder::Instance().add( { "add5", "adds 5", [this] { increment(5); } });
FuncHolder::Instance().add( { "add1", "adds 1", [this] { increment(1); } });
}
inline const std::string& getName() const override
{
return m_name;
}
void increment(int amount)
{
m_value += amount;
}
private:
const std::string m_name;
int m_value = 0;
};
//-------------------------------------------------------------------------------------------------
//Class to hold the static instances of the different derived type
class BaseHolder
{
public:
// meyer's singleton
static BaseHolder& Instance()
{
static BaseHolder instance;
return instance;
}
template<typename type_t>
size_t add()
{
static_assert(std::is_base_of_v<Base, type_t>,"type_t is not derived from base");
auto id = m_objects.size();
m_objects.push_back(std::make_unique<type_t>(id));
return id;
}
template<typename type_t>
const type_t* getInstance(size_t id)
{
static_assert(std::is_base_of_v<Base, type_t>);
auto& base_ptr = m_objects.at(id);
type_t* derived_ptr = dynamic_cast<type_t*>(base_ptr);
return derived_ptr;
}
private:
std::vector<std::unique_ptr<Base>> m_objects;
};
int main()
{
// Create an instance of BaseHolder and objects in itfirst this will ensure
// that it will live longer then any threads
auto derived_object_id = BaseHolder::Instance().add<Derived>();
// no need to capture anything (you can also use std::thread here)
auto future = std::async(std::launch::async, []
{
// making a call to Instance() of funcholder is threadsafe
// the 0 is a bit of a magic number in your desing how would you determine it at runtime?
auto func = FuncHolder::Instance().get(0);
func();
});
// synchronize
future.get();
return 0;
}