为什么不在Boost中使用Execute Around Idiom作为线程安全访问对象的智能指针
Why is not used Execute-Around Idiom in Boost as smart pointer for thread-safe access to object?
为什么不在Boost库中使用Execute Around Pointer Idiom作为线程安全访问对象的智能指针?
众所周知,有一个围绕指针执行的习语:https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Execute-Around_Pointer
Execute Around Pointer Idiom的主要思想-我们没有返回指向类成员的引用或指针,但我们返回了一个类型为proxy
的临时对象:http://ideone.com/cLS8Ph
- 在访问类成员
proxy (T * const _p, mutex_type& _mtx) : p(_p), lock(_mtx) {}
的指针之前创建的临时对象 - 则CCD_ 3给予对指向类成员CCD_
- 并在整个表达式完成后销毁:在完成成员用作参数的所有函数后,以及在其他计算后
~proxy () {}
这就是为什么这个代码是线程安全的:
execute_around<std::vector<int>> vecc(10, 10);
...
int res = std::sort(vecc->begin(), vecc->end()); // thread-safe in all threads
我们可以使用这个习惯用法,比如智能指针,它在访问成员变量或函数之前锁定互斥体,然后解锁互斥体。这样做总是,并且总是只锁定与该对象相关的互斥对象。
http://ideone.com/kB3wnu
#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>
#include <numeric>
#include <algorithm>
template<typename T, typename mutex_type = std::recursive_mutex>
class execute_around {
std::shared_ptr<mutex_type> mtx;
std::shared_ptr<T> p;
void lock() const { mtx->lock(); }
void unlock() const { mtx->unlock(); }
public:
class proxy {
std::unique_lock<mutex_type> lock;
T *const p;
public:
proxy (T * const _p, mutex_type& _mtx) : p(_p), lock(_mtx) {}
T* operator -> () {return p;}
const T* operator -> () const {return p;}
};
template<typename ...Args>
execute_around (Args ... args) :
p(std::make_shared<T>(args...)), mtx(std::make_shared<mutex_type>()) {}
proxy operator -> () { return proxy(p.get(), *mtx); }
const proxy operator -> () const { return proxy(p.get(), *mtx); }
template<class... Args> friend class std::lock_guard;
};
void thread_func(execute_around<std::vector<int>> vecc)
{
vecc->push_back(100); // thread-safe
int res = std::accumulate(vecc->begin(), vecc->end(), 0); // thread-safe
std::cout << std::string("res = " + std::to_string(res) + "n");
{ //all the following code in this scope is thread safe
std::lock_guard<decltype(vecc)> lock(vecc);
auto it = std::find(vecc->begin(), vecc->end(), 100);
if(it != vecc->end()) std::cout << *it << std::endl;
}
}
int main()
{
execute_around<std::vector<int>> vecc(10, 10);
auto copied_vecc_ptr = vecc; // copy-constructor
std::thread t1([&]() { thread_func(copied_vecc_ptr); });
std::thread t2([&]() { thread_func(copied_vecc_ptr); });
t1.join(); t2.join();
return 0;
}
输出:
res = 200
100
res = 300
100
如果添加为friend
,我们可以将execute_around
用于任何类型、任何互斥和任何锁,并具有以下几个特性:
优于标准的优势std::recursive_mutex
:
- 如果不锁定互斥锁,就无法访问对象的成员,不会忘记-是自动完成的
- 不能选择错误的互斥对象来保护另一个对象或代码段
其他功能:
- 访问后,您不会忘记解锁互斥对象
- 如果您知道这个对象可以从多个线程使用,那么在不锁定互斥对象的情况下,任何人都不应该访问它——
execute_around
保证了这一点(但此外,您可以将它与其他互斥对象一起使用,这些互斥对象保护整个代码段,而不仅仅是一个对象) - 您可以将对象的成员作为参数传递给函数,这将在函数的整个执行过程中是线程安全的,就像我们对
std::accumulate()
所做的那样 - 当我们访问多个成员(变量和函数)并在一个表达式中执行多个锁时,我们不会得到死锁——如果我们使用
std::recursive_mutex
- 它没有
operator *
,所以你不能获得线程对对象的不安全引用,但可以获得对对象成员的不安全的引用 - 它有复制构造函数,但没有赋值-
operator =
- 它可能具有指向单个对象的许多副本&互斥
可能的问题
在某些情况下,我们应该使用executive_around
作为标准std::mutex
,即使用lock_guard
,但如果我们忘记了这一点(std::lock_guard<decltype(vecc)> lock(vecc);
),那么我们就会遇到问题:
- 我们可以获取对对象成员的引用,然后在线程不安全的情况下使用它
- 我们可以获取这个对象的迭代器,然后在线程不安全的情况下使用它,它也可以被其他线程无效
在解释为什么Boost中不使用Execute Around Idiom作为线程安全访问对象的智能指针时,是否存在其他可能的问题?
也就是说,executive_around
还有什么问题,但标准互斥和锁没有这些问题?
proxy
类的行为方式:临时对象生存期
2016-07-12程序设计语言C++标准工作草案:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf
12.2临时物体
§12.26第三种情况是,当引用绑定到temporary.115引用绑定到的临时或临时的,是子对象的完整对象引用在引用的生存期内持续存在,除了:
(6.1)…
(6.2)--临时绑定到函数返回语句(6.6.3)未扩展;临时的是在return语句中的完整表达式末尾销毁。
(6.3)…
对一个临时生命的破坏在销毁每个临时的,其在相同的完整表达式中较早构建。如果引用绑定到的两个或多个临时库的生存期在同一点结束,这些临时物品在该点被销毁以相反的顺序完成它们的建造。
通常情况下,基于互斥的线程安全性不构成。
Ie,如果操作A
是线程安全的,并且操作B
是线程安全,则操作A
和操作B
一起不是。
正因为如此,你不能"顺便来忘记"。您必须意识到您正在执行基于互斥的操作,这使得执行的透明性非常危险。
举个例子,假设您有一个线程安全的容器。
你这样做:
std::vector<Element> dest;
std::copy_if( c->begin(), c->end(), std::back_inserter(dest),
[&](auto&& e){
this->ShouldCopy(e);
});
看起来很安全,不是吗?我们将容器c
从普通的容器智能指针升级为围绕执行的智能指针,现在它在访问之前锁定c
。
一切都很好。
但事实并非如此。如果this->ShouldCopy(Element const&)
获取任何互斥(比如bob
),我们刚刚创建了一个潜在的死锁。
如果bob
在c
中的互斥之前被锁定,那么两个线程都可能永远被锁定并饿死。
这可能是非确定性的,并且它不取决于所讨论代码的本地正确性(至少在C++中是这样)。您只能通过全局代码分析来发现它。
在这种情况下,proxy
0上锁的透明性会使代码的安全性降低,而不是更明显地得到互斥。因为至少如果它明显且昂贵,它可能会更孤立,更容易追踪。
这也是为什么有些人认为递归互斥是一种反模式:如果你对互斥的使用控制太少,以至于无法阻止递归获取它,那么你的代码可能无法管理所有互斥的全局顺序。
此外,保护shared_ptr
独立互斥对象的内容是愚蠢的。将互斥对象和对象存储在同一结构中,不要破坏局部性。
话虽如此,我确实使用了你所写内容的变体。
template<class T>
struct mutex_guarded {
template<class F>
auto write( F&& f ) { return access( std::forward<F>(f), *this); }
template<class F>
auto read( F&& f ) const { return access( std::forward<F>(f), *this); }
template<class F, class...Guarded>
friend auto access( F&& f, Guarded&&...guardeds );
private:
T t;
std::shared_timed_mutex m;
};
其中access
取任意数量的mutex_guarded
并按顺序正确锁定它们,然后将封装的t
传递给传入的f
。
这允许:
c.read( [&](auto&& c){
std::copy_if( c.begin(), c.end(), std::back_inserter(dest),
[&](auto&& e){
this->ShouldCopy(e);
});
} );
这至少使得互斥体的使用粗俗。同样,如果不通过读或写函数,就无法访问数据,因此所有访问都有一个互斥对象。但在这里,我们至少可以使用多锁,并且可以搜索代码以使用互斥锁。
它仍然存在死锁风险,但一般来说,基于互斥的结构都有这个问题。
- 从不同线程使用int64的不同字节安全吗
- 如何将元素添加到数组的线程安全函数?
- C++中的线程安全删除
- 在std::thread中,joinable()然后join()线程安全吗
- 在c++队列中使用pop和visit实现线程安全
- 以线程安全的方式调用"QQuickPaintedItem::updateImage(const QImage&image)"(no QThread)
- 全局变量 多读取器 一个写入器多线程安全?
- 共享队列的线程安全
- boost::文件系统::recursive_directory_iterator多线程安全
- 静态 constexpr 类成员变量对多线程读取是否安全?
- 以线程安全的方式转换 C/C++ 中时区名称字符串的时区偏移量
- 线程安全运算符<<
- 如何使缓存线程安全
- C++线程安全:如果只有一个线程可以写入非原子变量,但多个线程从中读取. 会遇到问题吗?
- 提升精神 V2 Qi 语法线程安全吗?
- 线程调用的函数对对象删除是否安全?
- asio 链对象线程安全吗?
- 线程安全队列 c++
- 提供对不同类型的数据(建议、代码审查)的线程安全访问的类
- 有没有更好的方法可以使此代码线程安全?线程局部静态似乎是一个生硬的工具