Избегайте тупиковых ситуаций с использованием невиртуального публичного интерфейса и замков Scoped в C ++ - PullRequest
3 голосов
/ 07 мая 2009

Я столкнулся с проблемой, которая кажется мне тревожной. Кажется, я нашел ситуацию, которая достаточно проста для обхода, но это может привести к проблемам, если: а) у меня потеря концентрации при программировании или б) кто-то еще начинает реализовывать мои интерфейсы и не знает, как с этим справиться эта ситуация.

Вот мои основные настройки:

У меня есть абстрактный класс, который я использую в качестве универсального интерфейса для нескольких типов данных. Я принял парадигму не виртуального публичного интерфейса (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 в своей реализации блокировки области действия, и, насколько я видел, она не пытается избежать тупика (что, я думаю, я почти предпочитаю).

Ответы [ 3 ]

7 голосов
/ 07 мая 2009

Использование частного наследования потенциально решит вашу проблему:

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 означают, что они все еще могут быть вызваны из бара.

В качестве альтернативы, опция, которая поможет , но не решит проблему, заключается в использовании шаблона 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".

6 голосов
/ 07 мая 2009

Ваш мьютекс не должен быть рекурсивным мьютексом. Если это не рекурсивный мьютекс, вторая попытка заблокировать мьютекс в том же потоке приведет к блокировке этого потока. Поскольку этот поток заблокировал мьютекс, но заблокирован на этом мьютексе, у вас тупик.

Вы, вероятно, хотите посмотреть:

boost::recursive_mutex

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

Предполагается реализовать рекурсивное мьютексное поведение кросс-платформенного. Заметки Win32 CRITICAL_SECTION (используемые через Enter / LeaveCriticalSection) являются рекурсивными, что создаст описываемое вами поведение.

1 голос
/ 07 мая 2009

Хотя рекурсивная блокировка решит вашу проблему, я всегда чувствовал, что, хотя иногда это необходимо, во многих ситуациях рекурсивная блокировка используется в качестве простого выхода, слишком много блокируя.

Ваш опубликованный код явно упрощен для демонстрационных целей, поэтому я не уверен, будет ли он применяться.

В качестве примера, скажем, использование ресурса X не является потокобезопасным. У вас есть что-то вроде.

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

aImp() {
   ScopedLock
   use(x)
}

Очевидно, это приведет к тупику.

Однако, использование ваших замков гораздо уже, устранит проблему. Использование блокировок в как можно меньшем объеме - это всегда хорошая идея, как по соображениям производительности, так и во избежание тупиков.

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

Вы поняли идею.

Я знаю, что это не всегда возможно (или может привести к ужасно неэффективному коду), не зная больше деталей, я не знаю, относится ли это к вашей проблеме. Но думал, что это все равно стоит опубликовать.

...