使用C++中的非虚拟公共接口和Scoped锁来避免死锁

Per*_*tor 3 c++ multithreading synchronization abstract-class

我遇到了一个让我感到不安的问题.似乎我发现了一种容易解决的情况,但如果a)我在编程时失去了注意力,或者b)其他人开始实现我的界面并且不知道如何处理,这可能会导致问题这个情况.

这是我的基本设置:

我有一个抽象类,我正在使用它作为几种数据类型的通用接口.我采用了非虚拟公共接口范例(Sutter,2001)以及范围锁定来提供一些线程安全性.一个示例接口类看起来像这样(我省略了有关作用域锁定和互斥锁实现的细节,因为我不认为它们是相关的):

class Foo
{
public:
    A( )
    {
        ScopedLock lock( mutex );
        aImp( );
    }
    B( )
    {
        ScopedLock lock( mutex );
        bImp( );
    }
protected:
    aImp( ) = 0;
    bImp( ) = 0;
}
Run Code Online (Sandbox Code Playgroud)

然后由用户来实现aImp和bImp,这是问题所在.如果aImp执行一些使用bImp的操作,那么执行此操作非常容易(在某种意义上几乎是逻辑的):

class Bar
{
protected:
    aImp( )
    {
        ...
        B( );
        ...
    }
    bImp( )
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

僵局.当然,对此的简单解决方案是始终调用受保护的虚拟函数而不是它们的公共变体(在上面的代码段中用bImp()替换B()).但是如果我犯了错误,或者更糟糕的是让其他人自己上吊,那么自己挂起来似乎仍然很容易.

有没有人有办法试图阻止抽象类的实现者在编译时调用那些公共函数,或者有助于避免死锁解决方案?

只是为了踢,一些互斥体允许操作,这将避免死锁问题.例如,如果我使用Windows函数EnterCriticalSection和LeaveCriticalSection实现它,则没有问题.但我宁愿避免使用特定于平台的功能.我目前在我的作用域锁实现中使用boost :: mutex和boost :: shared_mutex,据我所知,它并不试图避免死锁(我认为我几乎更喜欢).

Ric*_*den 7

使用私有继承可能会解决您的问题:

class Foo
{
public:
  void A( )
    {
      ScopedLock lock( mutex );
      aImp( );
    }
  void B( )
    {
      ScopedLock lock( mutex );
      bImp( );
    }

protected:
  virtual void aImp( ) = 0;
  virtual void bImp( ) = 0;
};

class FooMiddle : private Foo
{
public:
  using Foo::aImp;
  using Foo::bImp;
};

class Bar : public FooMiddle
{
  virtual void aImpl ()
  {
    bImp ();
    B ();                   // Compile error - B is private
  }
};
Run Code Online (Sandbox Code Playgroud)

私有地从Foo派生,然后使用FooMiddle确保Bar无法访问A或B.但是,bar仍然可以覆盖aImp和bImp,而FooMiddle中的using声明意味着仍然可以从Bar调用它们.

或者,有助于但不能解决问题的选项是使用Pimpl模式.你最终得到的东西如下:

class FooImpl
{
public:
  virtual void aImp( ) = 0;
  virtual void bImp( ) = 0;
};

class Foo
{
public:
  void A( )
    {
      ScopedLock lock( mutex );
      m_impl->aImp( );
    }
  void B( )
    {
      ScopedLock lock( mutex );
      m_impl->bImp( );
    }

private:
  FooImpl * m_impl;
}
Run Code Online (Sandbox Code Playgroud)

好处是在源自FooImpl的类中,它们不再具有"Foo"对象,因此不能轻易地调用"A"或"B".

  • 我认为使用pimpl模式的版本更清晰.关注点分离很好:类Foo负责客户端接口和锁定,而FooImpl和派生类处理A()和B()的算法.关键是要将Foo和FooImpl的声明保存在单独的头文件中(类Foo的头只需要类FooImpl的前向声明).使用Foo的客户端代码不了解FooImpl,实现从FooImpl派生的类的代码知道需要知道Foo类甚至存在. (2认同)

Dou*_* T. 6

您的互斥锁不能是递归互斥锁.如果它不是递归互斥锁,则在同一线程中锁定互斥锁的第二次尝试将导致该线程阻塞.由于该线程锁定了互斥锁,但在该互斥锁上被阻塞,因此会出现死锁.

你可能想看看:

boost::recursive_mutex
Run Code Online (Sandbox Code Playgroud)

http://www.boost.org/doc/libs/1_32_0/doc/html/recursive_mutex.html

它应该实现跨平台的递归互斥行为.注意Win32 CRITICAL_SECTION(通过Enter/LeaveCriticalSection使用)是递归的,这将创建您描述的行为.