为什么不在Boost中使用Execute Around Idiom作为线程安全访问对象的智能指针

Why is not used Execute-Around Idiom in Boost as smart pointer for thread-safe access to object?

本文关键字:安全 线程 访问 对象 指针 智能 Idiom Boost Around Execute 为什么不      更新时间:2023-10-16

为什么不在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),我们刚刚创建了一个潜在的死锁。

如果bobc中的互斥之前被锁定,那么两个线程都可能永远被锁定并饿死。

这可能是非确定性的,并且它不取决于所讨论代码的本地正确性(至少在C++中是这样)。您只能通过全局代码分析来发现它。

在这种情况下,proxy0上锁的透明性会使代码的安全性降低,而不是更明显地得到互斥。因为至少如果它明显且昂贵,它可能会更孤立,更容易追踪。

这也是为什么有些人认为递归互斥是一种反模式:如果你对互斥的使用控制太少,以至于无法阻止递归获取它,那么你的代码可能无法管理所有互斥的全局顺序。

此外,保护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);
  });
} );

这至少使得互斥体的使用粗俗。同样,如果不通过读或写函数,就无法访问数据,因此所有访问都有一个互斥对象。但在这里,我们至少可以使用多锁,并且可以搜索代码以使用互斥锁。

它仍然存在死锁风险,但一般来说,基于互斥的结构都有这个问题。