3

我遇到了一个对我来说似乎很困扰的问题。似乎我发现了一种很容易解决的情况,但是如果 a) 我在编程时注意力不集中,或者 b) 其他人开始实现我的接口并且不知道如何处理,这可能会导致问题这个情况。

这是我的基本设置:

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

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

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

class Bar
{
protected:
    aImp( )
    {
        ...
        B( );
        ...
    }
    bImp( )
    {
        ...
    }
}

僵局。当然,解决这个问题的简单方法是始终调用受保护的虚函数而不是它们的公共变体(在上面的代码片段中将 B( ) 替换为 bImp( ))。但是,如果我犯了一个错误,或者更糟的是让别人上吊自己,似乎仍然很容易上吊。

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

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

4

3 回答 3

8

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

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
  }
};

从 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;
}

好处是,在派生自 FooImpl 的类中,它们不再具有“Foo”对象,因此不能轻易调用“A”或“B”。

于 2009-05-07T14:19:40.323 回答
6

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

你可能想看看:

boost::recursive_mutex

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

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

于 2009-05-07T14:18:45.863 回答
1

虽然递归锁可以解决您的问题,但我一直认为,虽然有时是必要的,但在许多情况下,递归锁被用作一种简单的出路,锁定方式太多了。

您发布的代码显然是出于演示目的而简化的,所以我不确定它是否适用。

例如,假设使用资源 X 不是线程安全的。你有类似的东西。

A() {
   ScopedLock
   use(x)
   aImp()
   use(x)
}

aImp() {
   ScopedLock
   use(x)
}

显然,这会导致死锁。

然而,使用更窄的锁可以消除这个问题。在尽可能小的范围内使用锁总是一个好主意,无论是出于性能原因还是避免死锁。

A() {
   {
      ScopedLock
      use(x)
   }
   aImp()
   {
      ScopedLock
      use(x)
   }
}

你明白了。

我知道这并不总是可能的(或者会导致代码效率极低),不知道更多细节我不知道它是否适用于您的问题。但认为无论如何都值得发布。

于 2009-05-07T14:53:49.210 回答