从另一个线程调用存储的 c++ lambda 中的"this"实例指针时,是否可以防止它失效/销毁?



这个问题是这个问题的扩展,因为它是一个类似的问题,但并没有完全回答我的问题(更不用说这个问题中的问题不涉及线程/多线程)。

问题

正如标题所示,我遇到了一个关于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;
}

这可能不是一个非常简单的例子,但我想强调重要的细节(例如函数是如何存储的以及调用它们的类),这样就可以清楚地了解问题是如何产生的。
设计中还有一些可能不相关但重要的部分需要指出。

  1. 预期有许多类/类型派生Base类(即Derived1Derived2等),但这些派生类中的每一个只能有一个实例;因此,BaseHolder类的所有成员都是静态的。因此,如果这个设计确实需要重新设计,请记住这一点(尽管老实说,这可能会以比现在更好的方式实现,但这可能与问题无关)
  2. 使BaseHolder类模板化,并在编译时将我希望它保持的类/类型传递给它的模板可能是本能的(因此使用类似tuple而不是vector的东西),但我不是故意这样做的,因为我可能需要在稍后的运行时添加更多的派生类型
  3. 我真的无法更改f(std::function<>)的模板类型,因为我可能需要传递具有自己的返回类型和参数的不同函数(这就是为什么我有时使用lambda,有时只希望调用的是具有void返回类型的函数时使用std::bind)。为了实现这一点,我只将其制作为std::function<void()>
  4. 此设计的总体目标是静态调用和调用一个函数(就好像它是由事件触发的一样),该函数在被调用之前已经构造好,并且能够修改给定的类(在这种情况下,特别是它在-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通过值捕获Derivedthis指针(正如前面提到的问题的答案和提案文档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中的静态实例)存在相同的问题:静态实例尚未创建,因为它正在调用访问实例的构造函数来创建它

thisshared_ptr(直接)

在上述问题中不止一次提到的解决方案是制作并使用thisshared_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异常,因为这样做可能无论如何都是不正确的(或者至少我认为是这样)。

thisshared_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-将状态放在安全的地方

保证某种状态的最安全的方式是";"活着";在某一点上是把它放在一个更高的范围:

  1. 静态存储/全局范围
  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;
}

最新更新