在使用 FreeRTOS 在 ESP32 上运行的应用程序中,我实现了一个负责存储各种设置的设置类。我目前担心这个类的线程安全性,特别是关于“Get”方法。
有人可以检查我的实现并确认它是否正确且线程安全吗?我怀疑“复制”是否总是真正的复制,尤其是当设置类型是字符串或对象时。
template<typename T>
class Setting : public ISetting
{
Mutex& mutex;
T Value;
public:
Setting(Mutex& mutex, const T& defaultValue)
: mutex(mutex)
, Value(defaultValue)
{
}
void Set(const T& value)
{
mutex.Take();
Value = value; // Assign the new value to the setting
mutex.Give();
}
T Get()
{
T copy;
mutex.Take();
copy = Value;
mutex.Give();
return copy;
}
void Accept(ISettingVisitor* handler)
{
handler->Visit(this);
}
};
Get()
是线程安全的,因为 T
的复制赋值运算符的执行仅由一个线程同时完成。但是,总体来说并不安全。
Take()
和 Give()
是非常不寻常的锁定/解锁名称;对于 std::mutex
,它们被称为 .lock()
和 .unlock()
。
如果像这样调用它们,您可以使用 std::scoped_lock
而不是手动锁定/解锁:
void Set(const T& value)
{
std::scoped_lock lock{mutex}; // or std::lock_guard before C++17
Value = value;
}
T Get()
{
std::scoped_lock lock{mutex}; // or std::lock_guard before C++17
return Value;
}
或者,可以使用
std::experimental::scope_exit
或其他方式来解锁析构函数中的互斥体。
无论如何,在析构函数中解锁互斥体“非常重要”,因为这使得异常安全变得容易。如果 mutex
抛出异常,则代码中的
copy = Value
永远不会解锁,并且复制赋值运算符可以合理地做到这一点。同样的问题也适用于 Set()
。如果 T
T
没有值语义,那么这段代码仍然不是
真正线程安全的。例如,如果
T = std::span
,则调用者可以同时且不安全地修改跨度引用的数据。复制 std::span
就是所谓的“浅复制”;它不会复制底层数据(“深层复制”)。但是,这不是您可以解决的问题。无法检查给定类型的语义是什么。确保整体安全使用是Setting
用户的责任。
Setting
的职责只是线程安全地获取和设置一些值。