代码之家  ›  专栏  ›  技术社区  ›  Paul Mrozowski

可以是用户定义的类型,以帮助编写线程安全代码

  •  24
  • Paul Mrozowski  · 技术社区  · 7 年前

    我知道,在之前的几个问题/答案中已经非常清楚地表明 volatile 与C++内存模型的可见状态相关,而不是多线程。

    另一方面 article 作者Alexandrescu使用 不稳定的 关键字不是一个运行时特性,而是一个编译时检查,以迫使编译器无法接受可能不是线程安全的代码。在这篇文章中,这个关键词更像是一个 required_thread_safety 标签的实际预期用途 不稳定的 .

    这(ab)是使用 不稳定的 适当的这种方法可能隐藏着什么陷阱?

    首先想到的是一种困惑: 不稳定的 与线程安全无关,但由于缺乏更好的工具,我可以接受它。

    文章的基本简化:

    如果你声明一个变量 不稳定的 只有 不稳定的 可以对其调用成员方法,因此编译器将阻止对其他方法的调用代码。宣布 std::vector 例如 不稳定的 将阻止该类的所有使用。以锁定指针的形式添加包装器,以执行 const_cast 释放 不稳定的 根据要求,允许通过锁定指针进行任何访问。

    从文章中窃取:

    template <typename T>
    class LockingPtr {
    public:
       // Constructors/destructors
       LockingPtr(volatile T& obj, Mutex& mtx)
          : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx)
       { mtx.Lock(); }
       ~LockingPtr()   { pMtx_->Unlock(); }
       // Pointer behavior
       T& operator*()  { return *pObj_; }
       T* operator->() { return pObj_; }
    private:
       T* pObj_;
       Mutex* pMtx_;
       LockingPtr(const LockingPtr&);
       LockingPtr& operator=(const LockingPtr&);
    };
    
    class SyncBuf {
    public:
       void Thread1() {
          LockingPtr<BufT> lpBuf(buffer_, mtx_);
          BufT::iterator i = lpBuf->begin();
          for (; i != lpBuf->end(); ++i) {
             // ... use *i ...
          }
       }
       void Thread2();
    private:
       typedef vector<char> BufT;
       volatile BufT buffer_;
       Mutex mtx_; // controls access to buffer_
    };
    

    笔记

    在前两个答案出现后,我想我必须澄清,因为我可能没有使用最合适的词语。

    使用 不稳定的 不是因为它在运行时提供了什么,而是因为它在编译时意味着什么。也就是说,同样的把戏也可以用 const 关键字,如果它在用户定义的类型中很少使用 不稳定的 是也就是说,有一个关键字(碰巧拼写为volatile)允许我阻止成员函数调用,Alexandrescu正在使用它欺骗编译器,使其无法编译线程不安全的代码。

    我认为它有很多元编程技巧,不是因为它们在编译时做了什么,而是因为它迫使编译器为您做了什么。

    8 回复  |  直到 16 年前
        1
  •  6
  •   Abhay    16 年前

    我认为问题不在于 volatile .没有,而且 安德烈的 文章并没有说这是真的。给,一个 mutex 是用来实现这一目标的。问题是 不稳定的 要提供的关键字 静态类型检查 除了在线程安全代码中使用互斥外,还有滥用 不稳定的 关键词?我觉得这很聪明,但我遇到过不喜欢它的开发者 严格的类型检查 只是为了它。

    在我看来,当您为多线程环境编写代码时,已经有足够多的注意事项需要强调,您希望人们不要忽视竞争条件和死锁。

    这种包装方法的一个缺点是,包装类型上的每个操作都使用 LockingPtr 必须通过成员函数。这将增加一个间接层次,这可能会极大地影响开发人员在团队中的舒适度。

    但是如果你是一个纯粹主义者,相信C++的灵魂。 严格的类型检查 ; 这是一个很好的选择。

        2
  •  4
  •   YeahStu    16 年前

    这会捕获某些类型的线程不安全代码(并发访问),但会错过其他代码(由于锁反转而导致的死锁)。两者都不是特别容易测试的,所以这是一个适度的局部胜利。在实践中,记住强制某个特定的私有成员只能在某个指定的锁下访问,这对我来说并不是什么大问题。

    这个问题的两个答案表明,你说混淆是一个显著的缺点是正确的——维护人员可能已经非常习惯于理解volatile的内存访问语义与线程安全无关,在声明错误之前,他们甚至不会阅读代码/文章的其余部分。

    我认为Alexandrescu在文章中概述的另一个大缺点是,它不适用于非类类型。这可能是一个难以记住的限制。如果你认为标记你的数据成员 volatile 在不锁定的情况下停止使用它们,然后期望编译器告诉您何时锁定,那么您可能会意外地将其应用于 int ,或模板参数相关类型的成员。由此产生的错误代码可以正常编译, 但您可能已经停止检查代码中的此类错误 .想象一下,如果可以分配给 const int ,但程序员仍然希望编译器能为他们检查常量的正确性。。。

    我认为数据成员的类型实际上存在任何风险 不稳定的 成员函数应该被注意到,然后被打折,尽管它可能有一天会咬人。

    我想知道,对于通过属性提供额外的const样式类型修饰符的编译器,有什么可说的吗。 Stroustrup says “建议只使用属性来控制不影响程序含义但可能有助于检测错误的内容”。如果你能替换所有提到的 不稳定的 在代码中使用 [[__typemodifier(needslocking)]] 那我觉得会更好。因此,如果没有计算机,就不可能使用该对象 const_cast ,希望你不会写 康斯塔 不考虑你要抛弃的是什么。

        3
  •  2
  •   Fred Nurk    15 年前

    C++03§7.1.5.1p7:

    如果试图通过使用具有非易失性限定类型的左值来引用使用易失性限定类型定义的对象,则程序行为未定义。

    因为在您的示例中,buffer_uu定义为volatile,所以丢弃它是未定义的行为。但是,您可以使用一个适配器来解决这个问题,该适配器将对象定义为非易失性,但会增加易失性:

    template<class T>
    struct Lock;
    
    template<class T, class Mutex>
    struct Volatile {
      Volatile() : _data () {}
      Volatile(T const &data) : _data (data) {}
    
      T        volatile& operator*()        { return _data; }
      T const  volatile& operator*() const  { return _data; }
    
      T        volatile* operator->()        { return &**this; }
      T const  volatile* operator->() const  { return &**this; }
    
    private:
      T _data;
      Mutex _mutex;
    
      friend class Lock<T>;
    };
    

    友谊需要通过已经锁定的对象严格控制非易失性访问:

    template<class T>
    struct Lock {
      Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
      ~Lock() { _data._mutex.unlock(); }
    
      T& operator*() { return _data._data; }
      T* operator->() { return &**this; }
    
    private:
      Volatile<T> &_data;
    };
    

    例子:

    struct Something {
      void action() volatile;  // Does action in a thread-safe way.
      void action();  // May assume only one thread has access to the object.
      int n;
    };
    Volatile<Something> data;
    void example() {
      data->action();  // Calls volatile action.
      Lock<Something> locked (data);
      locked->action();  // Calls non-volatile action.
    }
    

    这里有两个警告。首先,您仍然可以访问公共数据成员(Something::n),但它们是限定的;这可能会在不同的时候失败。第二,有些东西不知道它是否真的被定义为volatile,如果它被定义为volatile,那么在方法中丢弃volatile(来自“this”或成员)仍然是UB:

    Something volatile v;
    v.action();  // Compiles, but is UB if action casts away volatile internally.
    

    主要目标已经实现:对象不必知道它们是以这种方式使用的,并且编译器将阻止对非易失性方法(对于大多数类型都是方法)的调用,除非您显式地通过锁。

        4
  •  2
  •   anon    9 年前

    Building on other code 并且完全消除了对volatile说明符的需求,这不仅可以工作,而且可以正确地传播const(类似于迭代器vs const_迭代器)。不幸的是,对于这两种接口类型,它需要相当多的样板代码,但您不必重复任何方法逻辑:每个方法仍然定义一次,即使您必须“复制”“易失性”版本,类似于常量和非常量上方法的正常重载。

    #include <cassert>
    #include <iostream>
    
    struct ExampleMutex {  // Purely for the sake of this example.
      ExampleMutex() : _locked (false) {}
      bool try_lock() {
        if (_locked) return false;
        _locked = true;
        return true;
      }
      void lock() {
        bool acquired = try_lock();
        assert(acquired);
      }
      void unlock() {
        assert(_locked);
        _locked = false;
      }
    private:
      bool _locked;
    };
    
    // Customization point so these don't have to be implemented as nested types:
    template<class T>
    struct VolatileTraits {
      typedef typename T::VolatileInterface       Interface;
      typedef typename T::VolatileConstInterface  ConstInterface;
    };
    
    template<class T>
    class Lock;
    template<class T>
    class ConstLock;
    
    template<class T, class Mutex=ExampleMutex>
    struct Volatile {
      typedef typename VolatileTraits<T>::Interface       Interface;
      typedef typename VolatileTraits<T>::ConstInterface  ConstInterface;
    
      Volatile() : _data () {}
      Volatile(T const &data) : _data (data) {}
    
      Interface       operator*()        { return _data; }
      ConstInterface  operator*() const  { return _data; }
      Interface       operator->()        { return _data; }
      ConstInterface  operator->() const  { return _data; }
    
    private:
      T _data;
      mutable Mutex _mutex;
    
      friend class Lock<T>;
      friend class ConstLock<T>;
    };
    
    template<class T>
    struct Lock {
      Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
      ~Lock() { _data._mutex.unlock(); }
    
      T& operator*() { return _data._data; }
      T* operator->() { return &**this; }
    
    private:
      Volatile<T> &_data;
    };
    
    template<class T>
    struct ConstLock {
      ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); }
      ~ConstLock() { _data._mutex.unlock(); }
    
      T const& operator*() { return _data._data; }
      T const* operator->() { return &**this; }
    
    private:
      Volatile<T> const &_data;
    };
    
    struct Something {
      class VolatileConstInterface;
      struct VolatileInterface {
        // A bit of boilerplate:
        VolatileInterface(Something &x) : base (&x) {}
        VolatileInterface const* operator->() const { return this; }
    
        void action() const {
          base->_do("in a thread-safe way");
        }
    
      private:
        Something *base;
    
        friend class VolatileConstInterface;
      };
    
      struct VolatileConstInterface {
        // A bit of boilerplate:
        VolatileConstInterface(Something const &x) : base (&x) {}
        VolatileConstInterface(VolatileInterface x) : base (x.base) {}
        VolatileConstInterface const* operator->() const { return this; }
    
        void action() const {
          base->_do("in a thread-safe way to a const object");
        }
    
      private:
        Something const *base;
      };
    
      void action() {
        _do("knowing only one thread accesses this object");
      }
    
      void action() const {
        _do("knowing only one thread accesses this const object");
      }
    
    private:
      void _do(char const *restriction) const {
        std::cout << "do action " << restriction << '\n';
      }
    };
    
    int main() {
      Volatile<Something> x;
      Volatile<Something> const c;
    
      x->action();
      c->action();
    
      {
        Lock<Something> locked (x);
        locked->action();
      }
    
      {
        ConstLock<Something> locked (x);  // ConstLock from non-const object
        locked->action();
      }
    
      {
        ConstLock<Something> locked (c);
        locked->action();
      }
    
      return 0;
    }
    

    将class Something与Alexandrescu使用volatile的要求进行比较:

    struct Something {
      void action() volatile {
        _do("in a thread-safe way");
      }
    
      void action() const volatile {
        _do("in a thread-safe way to a const object");
      }
    
      void action() {
        _do("knowing only one thread accesses this object");
      }
    
      void action() const {
        _do("knowing only one thread accesses this const object");
      }
    
    private:
      void _do(char const *restriction) const volatile {
        std::cout << "do action " << restriction << '\n';
      }
    };
    
        5
  •  1
  •   John Dibling    16 年前

    从另一个角度来看这个问题。当您将一个变量声明为const时,您告诉编译器该值不能被您的代码更改。但这并不意味着价值 不会 改变例如,如果您这样做:

    const int cv = 123;
    int* that = const_cast<int*>(&cv);
    *that = 42;
    

    ...根据标准,这会引发未定义的行为,但在实践中会发生一些事情。也许这个值会改变。也许会有一个错误。也许飞行模拟器会发射——谁知道呢。关键是,你不知道在独立于平台的基础上会发生什么。所以 显然的 承诺 const 没有实现。该值可能是常量,也可能不是常量。

    既然这是真的,那么 康斯特 滥用语言?当然不是。它仍然是该语言提供的一种工具,可以帮助您编写更好的代码。它永远不会是确保值保持不变的终极工具——程序员的大脑最终就是那个工具——但这会让 康斯特 无用的?

    我说不,使用const作为工具来帮助您编写更好的代码并不是对语言的滥用。事实上,我会更进一步,说这是 意图 这一点很重要。

    现在,volatile也是如此。将某些内容声明为volatile不会使程序线程安全。它甚至可能不会使变量或对象线程安全。但编译器将强制执行CV限定语义,细心的程序员可以利用这一事实,通过帮助编译器识别可能编写错误的地方,帮助他编写更好的代码。就像编译器在他尝试这样做时帮助他一样:

    const int cv = 123;
    cv = 42;  // ERROR - compiler complains that the programmer is potentially making a mistake
    

    忘掉内存围栏和易失性对象和变量的原子性,就像你早已忘记的那样 cv 这才是真正的康斯坦斯。但是使用语言提供的工具来编写更好的代码。其中一个工具是 volatile .

        6
  •  0
  •   Malkocoglu    16 年前

    你最好不要那样做。 不稳定的 甚至不是为了提供线程安全而发明的。它是为了正确地访问内存映射的硬件寄存器而发明的。 不稳定的 关键字对CPU的无序执行功能没有影响。您应该使用适当的操作系统调用或CPU定义的CAS指令、内存围栏等。

    CAS

    Memory Fence

        7
  •  0
  •   Konrad Rudolph    15 年前

    在这篇文章中,这个关键词更像是一个 required_thread_safety 标签的实际预期用途比挥发性。

    没有读过这篇文章——为什么没有——安德烈说 所需的螺纹安全 那么,标签呢?虐待 volatile 这听起来不是个好主意。我相信这会导致 更多 困惑(就像你说的),而不是逃避。

    也就是说, 不稳定的 有时在多线程代码中可能需要,即使它不是 足够的 条件,只是为了防止编译器优化依赖于异步更新值的检查。

        8
  •  -2
  •   Marcelo Cantos    16 年前

    我不知道亚历山德雷斯库的建议是否合理,但尽管我认为他是一个超级聪明的家伙,但他对volatile语义的处理表明,他远远超出了自己的专业领域。Volatile在多线程处理中绝对没有价值(参见 here 为了更好地处理这个问题)所以Alexandrescu的说法 对多线程访问很有用,这让我很想知道我能对他的文章的其余部分抱有多大信心。