Closed. This question needs to be more
focused。它当前不接受答案。
想改善这个问题吗?更新问题,使其仅通过
editing this post专注于一个问题。
3年前关闭。
为什么在Boost-library中不使用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
我们可以像使用智能指针一样使用此惯用语,该指针还可以在访问成员变量或函数之前锁定互斥量,并在之后访问互斥量。总是这样做,并且总是仅锁定与此对象相关的互斥锁。
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
我们可以将
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年7月12日,编程语言C ++标准工作草案:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf
12.2临时对象
§12.2 6第三种情况是引用绑定到
115引用绑定到的临时文件或
临时对象,是子对象的完整对象,
引用绑定在引用的生存期内一直存在,但以下情况除外:
(6.1)...
(6.2)—临时绑定到a中返回值的生命周期
函数返回语句(6.6.3)未扩展;临时是
在return语句的全表达式结尾处销毁。
(6.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);
});
} );
至少使互斥体使用粗俗。同样,如果不通过读取或写入功能就无法访问数据,因此所有访问都具有互斥量。但是在这里,我们至少可以多重锁定,并且可以在代码中搜索互斥量。
它仍然遭受死锁的风险,但是在一般情况下,基于互斥的结构都存在该问题。
我是一名优秀的程序员,十分优秀!