【问题标题】:Is this an acceptable way to lock a container using C++?这是使用 C++ 锁定容器的可接受方式吗?
【发布时间】:2012-07-30 13:18:20
【问题描述】:

我需要(在 C++ 中)实现一个线程安全容器,使得只有一个线程能够从容器中添加或删除项目。我以前通过在线程之间共享互斥体来完成这种事情。这导致在我的代码中散落大量互斥对象,使事情变得非常混乱且难以维护。

我想知道是否有一种更简洁、更面向对象的方式来执行此操作。我想到了以下围绕容器的简单类包装器(半伪 C++ 代码)

 class LockedList {
    private:
        std::list<MyClass> m_List;

    public:
        MutexObject Mutex;
 };

这样就可以通过以下方式进行锁定

 LockedList lockableList;     //create instance
 lockableList.Mutex.Lock();    // Lock object

 ... // search and add or remove items

 lockableList.Mutex.Unlock();   // Unlock object

所以我的问题真的是问从设计的角度来看,这是否是一个好方法?我知道从设计的角度来看,允许公众访问成员是不受欢迎的,上述设计是否有任何严重的缺陷。如果是这样,是否有更好的方法来实现线程安全的容器对象?

我读过很多关于设计和 C++ 的书籍,但似乎确实缺乏关于多线程编程和多线程软件设计的文献。

如果上述方法是解决问题的糟糕方法,我是否可以提出一种改进方法,或者向我指出一些解释设计线程安全的类的好方法的信息?非常感谢。

【问题讨论】:

  • 谁在乎“更面向对象”?这需要“更多的 RAII”。
  • 发布的代码不是异常安全的。如果搜索、添加或删除操作引发异常,则互斥锁未解锁。提供作用域的锁定-解锁机制有助于避免这种情况。
  • 那你不应该问你的老板这个代码是否可以接受吗?似乎他的意见才是最重要的
  • 为什么你的老板会规定你如何编写代码?那他为什么不自己写呢?
  • @mathematician1975 当然,这些都是很好的目标。但它们并不一定意味着“更面向对象”。 OOP 只是尝试实现其中一些目标的几种方法之一。但重要的是目标,而不是 OOP。如果另一种范式可以让您更好地实现这些目标,那么唯一明智的做法就是说“screw OOP”

标签: c++ multithreading oop thread-safety


【解决方案1】:

我宁愿设计一个资源所有者来锁定一个互斥体并返回一个线程可以使用的对象。一旦线程完成并停止使用该对象,资源automatically 将返回给其所有者并释放锁。

template<typename Resource>
class ResourceOwner
{
      Lock         lock; 
      Resource     resource;

      public:
         ResourceHolder<Resource>  getExclusiveAccess()
         {
              // Let the ResourceHolder lock and unlock the lock
              // So while a thread holds a copy of this object only it
              // can access the resource. Once the thread releases all
              // copies then the lock is released allowing another
              // thread to call getExclusiveAccess().
              //
              // Make it behave like a form of smart pointer
              //    1) So you can pass it around.
              //    2) So all properties of the resource are provided via ->
              //    3) So the lock is automatically released when the thread
              //       releases the object.

              return ResourceHolder<Resource>(lock, resource);
         }
};

资源持有者(没有经过深思熟虑,因此可以改进)

template<typename Resource>
class ResourceHolder<
{
    // Use a shared_ptr to hold the scopped lock
    // When first created will lock the lock. When the shared_ptr
    // destroyes the scopped lock (after all copies are gone)
    // this will unlock the lock thus allowding other to use
    // getExclusiveAccess() on the owner
    std::shared_ptr<scopped_lock>    locker;
    Resource&                        resource;   // local reference on the resource.

    public:
        ResourceHolder(Lock& lock, Resource& r)
            : locker(new scopped_lock(lock))
            , resource(r)
        {}

        // Access to the resource via the -> operator
        // Thus allowing you to use all normal functionality of 
        // the resource.
        Resource* operator->() {return &resource;}
};

现在一个可锁定的列表是:

ResourceOwner<list<int>>  lockedList;

void threadedCode()
{
    ResourceHolder<list<int>>  list = lockedList.getExclusiveAccess();

    list->push_back(1);
}
// When list goes out of scope here. 
// It is destroyed and the the member locker will unlock `lock`
// in its destructor thus allowing the next thread to call getExclusiveAccess()

【讨论】:

  • 我想我会把它重命名为GetExclusiveAccess。至少对我来说,“UnsharedAccess”似乎不太能描述意图。不过设计不错。
  • @JerryCoffin:这确实更有意义。完成(尽管我将第一个 g 小写)。
  • @LokiAstari 谢谢。这里的一些答案很有帮助,但我想我会选择你的设计。
  • 不要忽视@Dirk Holsopple 的想法。通过我在评论中建议的轻微改动,它变得像这样灵活。他还具有更高效的额外优势(因为您不需要共享指针)。实际上,在重新阅读之后,我更喜欢他的解决方案(如果您添加 -> 并将其模板化并使其不可复制(您无法分配,为什么要使其可复制)并清理它)。您仍然可以通过引用传递它,但线程将不再能够意外地保持副本处于活动状态。
【解决方案2】:

我会做这样的事情,通过使用 RAII 使其更加安全。

class LockedList {
private:
    std::list<MyClass> m_List;
    MutexObject Mutex;
    friend class LockableListLock;
};

class LockableListLock {
private:
    LockedList& list_;
public:
    LockableListLock(LockedList& list) : list_(list) { list.Mutex.Lock(); }
    ~LockableListLock(){ list.Mutex.Unlock(); }
}

你会这样使用它

LockableList list;
{
    LockableListLock lock(list); // The list is now locked.

    // do stuff to the list

} // The list is automatically unlocked when lock goes out of scope.

您还可以通过在 LockableListLock 中的 std::list 接口周围添加包装器来使该类强制您在对其进行任何操作之前将其锁定,这样您就可以通过 LockedList 类访问列表,而不是通过 LockedList 类访问列表LockableListLock 类。例如,您可以围绕 std::list::begin() 制作这个包装器

std::list::iterator LockableListLock::begin() {
    return list_.m_List.begin();
}

然后像这样使用它

LockableList list;
LockableListLock lock(list);
// list.begin();   //This is a compiler error so you can't 
                   //access the list without locking it
lock.begin(); // This gets you the beginning of the list

【讨论】:

  • 您可以通过 LockableListLock 中的operator-&gt;() 公开列表,而不是为您要使用的每个方法制作一个包装器。这允许用户在其上调用方法,但永远不会获得用于存储的引用或指针。 std::list&lt;MyClass&gt;* LockableListLock::operator-&gt;() {return &amp;list_.m_List;}
【解决方案3】:

好的,我将更直接地说明其他人已经暗示的内容:这种设计的至少一部分,很可能是全部,可能不是您想要的。至少,您需要 RAII 样式的锁定。

我还将locked(或任何您喜欢的名称)设为模板,这样您就可以将锁定与容器本身分离。

// C++ like pesudo-code. Not intended to compile as-is.
struct mutex {
    void lock() { /* ... */ }
    void unlock() { /* ... */ }
};

struct lock {
    lock(mutex &m) { m.lock(); }
    ~lock(mutex &m) { m.unlock(); }
};

template <class container>
class locked {
    typedef container::value_type value_type;
    typedef container::reference_type reference_type;
    // ...

    container c;
    mutex m;
public:
    void push_back(reference_type const t) {
        lock l(m);
        c.push_back(t);
    }

    void push_front(reference_type const t) { 
        lock l(m);
        c.push_front(t);
    }

    // etc.
};

这使得代码相当容易编写并且(至少在某些情况下)仍然可以获得正确的行为——例如,您的单线程代码可能如下所示:

std::vector<int> x;

x.push_back(y);

...您的线程安全代码如下所示:

locked<std::vector<int> > x;

x.push_back(y);

假设您提供通常的begin()end()push_frontpush_back 等,您的locked&lt;container&gt; 仍然可以像普通容器一样使用,因此它适用于标准算法、迭代器等.

【讨论】:

    【解决方案4】:

    这种方法的问题在于它使 LockedList 不可复制。有关此障碍的详细信息,请查看此问题:

    Designing a thread-safe copyable class

    这些年来我尝试过各种各样的事情,在容器声明旁边声明的互斥锁总是被证明是最简单的方法(一旦在天真地实现其他方法后修复了所有错误)。

    您无需在代码中“乱扔”互斥锁。你只需要一个互斥锁,声明在它保护的容器旁边。

    【讨论】:

      【解决方案5】:

      很难说粗粒度锁定是一个糟糕的设计决策。我们需要了解代码所在的系统才能讨论这个问题。如果您不知道它不起作用,这是一个很好的起点。先做最简单的事情。

      如果您在未解锁的情况下确定范围,则可以通过降低失败的可能性来改进该代码。

      struct ScopedLocker {
        ScopedLocker(MutexObject &mo_) : mo(mo_) { mo.Lock(); }
        ~ScopedLocker() { mo.Unlock(); }
      
        MutexObject &mo;
      };
      

      您还可以对用户隐藏实现。

      class LockedList {
        private:
          std::list<MyClass> m_List;
          MutexObject Mutex;
      
        public:
          struct ScopedLocker {
             ScopedLocker(LockedList &ll);
             ~ScopedLocker();
          };
      };
      

      然后您只需将锁定列表传递给它,而不必担心MutexObject 的详细信息。

      您还可以让列表在内部处理所有锁定,这在某些情况下是可以的。设计问题是迭代。如果列表在内部锁定,那么这样的操作比让列表的用户决定何时锁定要糟糕得多。

      void foo(LockedList &list) {
        for (size_t i = 0; i < 100000000; i++) {
          list.push_back(i);
        }
      }
      

      一般来说,由于此类问题,很难给出建议。更多的时候,更多的是关于你如何使用一个对象。当您尝试编写解决多处理器编程的代码时,会有很多抽象的漏洞。这就是为什么您会看到更多的工具包,这些工具包可以让人们编写满足他们需求的解决方案。

      books that discuss multi-processor programming,虽然很少。随着所有新的 C++11 特性的出现,未来几年内应该会有更多的文献出现。

      【讨论】:

        【解决方案6】:

        我想出了这个(我确信可以改进它以采用两个以上的参数):

        template<class T1, class T2>
        class combine : public T1, public T2
        {
        public:
        
            /// We always need a virtual destructor.
            virtual ~combine() { }
        };
        

        这允许你做:

            // Combine an std::mutex and std::map<std::string, std::string> into
            // a single instance.
            combine<std::mutex, std::map<std::string, std::string>> mapWithMutex;
        
            // Lock the map within scope to modify the map in a thread-safe way.
            {
                // Lock the map.
                std::lock_guard<std::mutex> locked(mapWithMutex);
        
                // Modify the map.
                mapWithMutex["Person 1"] = "Jack";
                mapWithMutex["Person 2"] = "Jill";
            }
        

        如果您希望使用 std::recursive_mutex 和 std::set,那也可以。

        【讨论】:

          猜你喜欢
          • 2011-03-20
          • 2017-12-02
          • 1970-01-01
          • 2013-09-02
          • 1970-01-01
          • 2017-06-14
          • 1970-01-01
          • 2017-07-04
          • 2011-10-08
          相关资源
          最近更新 更多