我正在阅读“C ++ Concurrency in action”一书,并尝试理解线程安全数据结构(例如堆栈)中的异常安全性。作者声明,为了避免竞争条件,pop
应该执行两个操作 - 弹出并从堆栈返回项目,但是:
如果pop()函数被定义为返回弹出的值,并将其从堆栈中删除,则可能存在一个问题:弹出的值只有在修改了堆栈后才返回给调用者,但是复制数据以返回调用者的过程可能会引发异常。
这是建议的解决方案:
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw runtime_error("empty");
std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
data.pop();
return res;
}
在这种情况下,我们在make_shared
行调用了复制构造函数。如果复制构造函数抛出异常,则堆栈尚未修改,我们就是好的。
但是我不知道它与这个片段的区别很大:
T pop2() {
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw runtime_error("empty");
auto res = data.top();
data.pop();
return res;
}
这里我们在data.top()
行进行了复制构造函数调用,在返回时有移动构造函数。同样,如果复制构造函数抛出异常,我们就很好,因为堆栈尚未修改。移动contstructor不应该抛出异常。
我错过了什么吗?将shared_ptr
比较返回到(可移动)T
会有什么好处?
答案 0 :(得分:1)
如果T
不可移动,您的代码最终可能会执行多个复制构造。在这种情况下,使用shared_ptr
在堆上分配副本可能会更有效。
即使T
可移动,您的代码仍可能执行多个(可能代价高昂的)移动构造。与移动或复制构造任何可能的类型shared_ptr
相比,已知移动构造和/或复制构造T
相对便宜。
当然,您的里程可能会因具体类型,编译器以及操作环境和硬件而异。
答案 1 :(得分:1)
move-constructor可能会抛出(一般情况下)。 std::shared_ptr<T>
有一个noexcept
移动构造函数。
因此,在第一种情况下,return res;
无法抛出,但在第二种情况下,return res;
可能会抛出(并且data.pop()
已被调用)。