通过 std::unique_ptr 的 LazyArray 模板,这是双重检查习语的正确实现吗?
LazyArray template via std::unique_ptr, is this a proper implementation of the double-check idiom?
此代码是否使用 C++11 原子安全地实现双重检查习惯用法?
我在 "The C++ Programing Language 4th ed." 中看到了一个使用 atomic<bool>
的例子,我尽我所能使事情保持等价,但我没有信心。另外,这个可以改进吗?
由于 once_flag
的存储开销,我想避免使用 call_once
。
此 "LazyArray" 是为了减少内存而编写的,目的是通过对客户端代码进行最少的更改来替换数组(预计只会访问元素的一小部分)。从多个线程访问数组是一个不争的事实,由于性能原因,广泛锁定会产生问题。
/**
* Lazy creation of array elements.
* They will only be created when they are referenced.
*
* This "array" does not support iteration because it can have holes
*
* Array bounds isn't checked to keep it fast.
* Destruction of the array destroys all the T objects (via the unique_ptr d'tor)
*/
template<class T, size_t size>
class LazyArray
{
typedef LazyArray<T, size> mytype;
public:
// copying is not allowed (unlike regular arrays)
LazyArray(const LazyArray&) = delete;
LazyArray& operator=(const LazyArray&) = delete;
LazyArray(){}
T& operator[](size_t i)
{
return at(i);
}
const T& operator[](size_t i) const
{
return const_cast<mytype *>(this)->at(i);
}
private:
using guard = std::lock_guard<std::mutex>;
// get T object at index i by reference
T& at(size_t i) // only non-const variant is implemented, const version will use const_cast
{
auto &p = m_array[i];
std::atomic<T*> ap(p.get());
if(!ap) // object not created yet
{
guard g(mtx);
ap = p.get();
if(!ap)
p.reset(new T);
}
return *p;
}
std::unique_ptr<T> m_array[size];
std::mutex mtx;
};
理论上没有。
支票:
std::atomic<T*> ap(p.get());
if(!ap) // object not created yet
应该是acquire对应这个版本:
p.reset(new T);
其实不是。
失败的原因:
- 对象构造的某些部分new T在看到p分配了新值[=49]后可能对其他线程可见=]
- 赋值 p.reset 可能被撕裂,因为它是非原子的
可能,在某些平台上。
- 如果存储可以在其他存储之后重新排序(在 x86 上不会发生)或者如果编译器决定自己交换这些存储(不太可能),就会发生这种情况
- 指针大小的变量写入不会被撕裂
使用 std::atomic
解决这两个问题:
std::atomic<T*> m_array[size];
确定您必须手动删除 T*。
我建议基于 std::atomic<T*>
创建 atomic_unique_ptr
并在那里使用它
此代码是否使用 C++11 原子安全地实现双重检查习惯用法?
我在 "The C++ Programing Language 4th ed." 中看到了一个使用 atomic<bool>
的例子,我尽我所能使事情保持等价,但我没有信心。另外,这个可以改进吗?
由于 once_flag
的存储开销,我想避免使用 call_once
。
此 "LazyArray" 是为了减少内存而编写的,目的是通过对客户端代码进行最少的更改来替换数组(预计只会访问元素的一小部分)。从多个线程访问数组是一个不争的事实,由于性能原因,广泛锁定会产生问题。
/**
* Lazy creation of array elements.
* They will only be created when they are referenced.
*
* This "array" does not support iteration because it can have holes
*
* Array bounds isn't checked to keep it fast.
* Destruction of the array destroys all the T objects (via the unique_ptr d'tor)
*/
template<class T, size_t size>
class LazyArray
{
typedef LazyArray<T, size> mytype;
public:
// copying is not allowed (unlike regular arrays)
LazyArray(const LazyArray&) = delete;
LazyArray& operator=(const LazyArray&) = delete;
LazyArray(){}
T& operator[](size_t i)
{
return at(i);
}
const T& operator[](size_t i) const
{
return const_cast<mytype *>(this)->at(i);
}
private:
using guard = std::lock_guard<std::mutex>;
// get T object at index i by reference
T& at(size_t i) // only non-const variant is implemented, const version will use const_cast
{
auto &p = m_array[i];
std::atomic<T*> ap(p.get());
if(!ap) // object not created yet
{
guard g(mtx);
ap = p.get();
if(!ap)
p.reset(new T);
}
return *p;
}
std::unique_ptr<T> m_array[size];
std::mutex mtx;
};
理论上没有。
支票:
std::atomic<T*> ap(p.get());
if(!ap) // object not created yet
应该是acquire对应这个版本:
p.reset(new T);
其实不是。
失败的原因:
- 对象构造的某些部分new T在看到p分配了新值[=49]后可能对其他线程可见=]
- 赋值 p.reset 可能被撕裂,因为它是非原子的
可能,在某些平台上。
- 如果存储可以在其他存储之后重新排序(在 x86 上不会发生)或者如果编译器决定自己交换这些存储(不太可能),就会发生这种情况
- 指针大小的变量写入不会被撕裂
使用 std::atomic
解决这两个问题:
std::atomic<T*> m_array[size];
确定您必须手动删除 T*。
我建议基于 std::atomic<T*>
创建 atomic_unique_ptr
并在那里使用它