为什么没有在Boost-library中使用Execute-Around Pointer Idiom作为线程安全访问对象的智能指针?
众所周知,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) {}
proxy
可以访问指向类成员T* operator -> () {return p;}
~proxy () {}
这就是为什么这段代码是线程安全的:
execute_around<std::vector<int>> vecc(10, 10);
...
int res = std::sort(vecc->begin(), vecc->end()); // thread-safe in all threads
我们可以使用这个成语,就像智能指针一样,在我们访问成员变量或函数之前锁定互斥锁并在之后解锁互斥锁。这样做总是只会锁定与此对象相关的互斥锁。
#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
我们可以将execute_around
用于任何类型,任何互斥锁以及添加为friend
的任何锁定,并具有以下几个功能:
优于标准的优势 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
类:Temporary object lifetime
2016-07-12编程语言C ++标准工作草案:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf
12.2临时物品
§12.26第三个上下文是指引用绑定到a temporary.115引用绑定的临时或 临时的,是子对象的完整对象 引用的约束持续存在于引用的生命周期中,除了:
(6.1)......
(6.2) - 临时绑定到a中返回值的生命周期 函数返回语句(6.6.3)未扩展;暂时的是 在return语句中的完整表达结束时被销毁。
(6.3)......
一个临时的破坏,其寿命不会延长 绑定到引用在每个被破坏之前被排序 在同一个完整表达式中较早构造的临时表。如果 两个或多个临界参与者的生命周期 在同一点结束,这些临时工作在那时被摧毁 按照完工建设的相反顺序。
答案 0 :(得分:3)
通常,基于互斥锁的线程安全性不构成。
即如果操作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 ++中)。您只能通过全局代码分析来发现它。
在这种情况下,mutex
上的锁定透明度可以使代码更少安全,而不是更明显我们获得互斥锁。因为至少如果它显而易见且昂贵,它可能更加孤立,更容易追踪。
这也是为什么有些人认为递归互斥体是反模式的原因:如果你对使用互斥锁的控制很少,你无法阻止递归,你的代码可能不会管理全局顺序所有的互斥体。
此外,保护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);
});
} );
至少使互斥量使用粗俗。同样,如果不通过读取或写入功能,则无法访问数据,因此所有访问都具有互斥锁。但是在这里,我们至少可以进行多重锁定,并且可以使用代码来搜索互斥锁。
它仍然存在死锁风险,但总的来说,基于互斥的结构都存在这个问题。