Разработка многопоточного копируемого класса - PullRequest
9 голосов
/ 21 февраля 2011

Простой способ сделать класс потокобезопасным - добавить атрибут mutex и заблокировать мьютекс в методах доступа

class cMyClass {
  boost::mutex myMutex;
  cSomeClass A;
public:
  cSomeClass getA() {
    boost::mutex::scoped_lock lock( myMutex );
    return A;
  }
};

Проблема в том, что это делает класс недоступным для копирования.

Я могу заставить вещи работать, сделав мьютекс статичным. Однако это означает, что каждый экземпляр класса блокируется при обращении к любому другому экземпляру, поскольку все они используют один и тот же мьютекс.

Интересно, есть ли лучший способ?

Мой вывод заключается в том, что лучшего способа нет. Делать класс потокобезопасным с частным атрибутом статического мьютекса «лучше»: это просто, работает и скрывает неловкие детали.

class cMyClass {
  static boost::mutex myMutex;
  cSomeClass A;
public:
  cSomeClass getA() {
    boost::mutex::scoped_lock lock( myMutex );
    return A;
  }
};

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

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


Спасибо за все ответы.

Несколько человек упоминали о написании моего собственного конструктора копирования и оператора присваивания. Я попробовал это. Проблема в том, что у моего реального класса много атрибутов, которые постоянно меняются в процессе разработки. Поддержка как конструктора копирования, так и оператора assignmet утомительна и подвержена ошибкам, а ошибки создают труднодоступные ошибки. Позволить компилятору сгенерировать их для сложного класса - это огромная экономия времени и исправление ошибок.


Многие ответы касаются того, чтобы сделать конструктор копирования и оператор присваивания потокобезопасными. Это требование добавляет еще большей сложности ко всему! К счастью, мне это не нужно, поскольку все копирование выполняется во время настройки в одном потоке.


Теперь я думаю, что наилучшим подходом было бы создать крошечный класс, содержащий только мьютекс и критические атрибуты. Затем я могу написать конструктор небольших копий и оператор присваивания для критического класса и оставить компилятор для просмотра всех других атрибутов в основном классе.
class cSafe {
  boost::mutex myMutex;
  cSomeClass A;
public:
  cSomeClass getA() {
    boost::mutex::scoped_lock lock( myMutex );
    return A;
  }
  (copy constructor)
  (assignment op )

};
class cMyClass {
  cSafe S;
  ( ... other attributes ... )
public:
  cSomeClass getA() {
    return S.getA();
  }
};

Ответы [ 3 ]

4 голосов
/ 21 февраля 2011

Как бы просто ни был вопрос, понять это не так просто.Для начала мы можем поработать с простым конструктором копирования:

// almost pseudo code, mutex/lock/data types are synthetic
class test {
   mutable mutex m;
   data d;
public:
   test( test const & rhs ) {
      lock l(m);         // Lock the rhs to avoid race conditions,
                         // no need to lock this object.
      d = rhs.d;         // perform the copy, data might be many members
   }
};

Теперь создание оператора присваивания является более сложным.Первое, что приходит на ум, это просто делать то же самое, но в этом случае блокировать как lhs, так и rhs:

class test { // wrong
   mutable mutex m;
   data d;
public:
   test( test const & );
   test& operator=( test const & rhs ) {
      lock l1( m );
      lock l2( rhs.m );
      d = rhs.d;
      return *this;
   }
};

Достаточно просто и неправильно.Хотя мы гарантируем однопоточный доступ к объектам (обоим) во время операции, и, таким образом, у нас нет условий гонки, у нас есть потенциальный тупик:

test a, b;
// thr1              // thr2
void foo() {         void bar() {
   a = b;               b = a;
}                    }

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

test& test::operator=( test const & rhs ) {
   if ( this == &rhs ) return *this; // nothing to do
   // same (invalid) code here
}

Для другой части проблемы вам необходимо установить порядок получения мьютексов.Это может быть обработано по-разному (сохранение уникального идентификатора для каждого объекта для сравнения ...)

test & test::operator=( test const & rhs ) {
   mutex *first, *second;
   if ( unique_id(*this) < unique_id(rhs ) {
      first = &m;
      second = &rhs.m;
   } else {
      first = &rhs.m;
      second = &rhs.m;
   }
   lock l1( *first );
   lock l2( *second );
   d = rhs.d;
}

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

class test {
   mutable std::mutex m;
   data d;
public:
   test( const test & );
   test& operator=( test const & rhs ) {
      if ( this == &rhs ) return *this;        // avoid self deadlock
      std::lock( m, rhs.m );                   // acquire both mutexes or wait
      std::lock_guard<std::mutex> l1( m, std::adopt_lock );      // use RAII to release locks
      std::lock_guard<std::mutex> l2( rhs.m, std::adopt_lock );
      d = rhs.d;
      return *this;
   }
};

Функция std::lock получит все блокировки, переданные в качестве аргумента, и гарантирует, что порядокполучение является тем же, гарантируя, что если весь код, который должен получить эти два мьютекса, сделает это с помощью std::lock, то не будет тупиков.(Вы все еще можете заблокировать, вручную заблокировав их где-то отдельно)Следующие две строки хранят блокировки в объектах, реализующих RAII, так что, если операция присваивания завершается неудачей (исключение выдается), блокировки снимаются.

Это можно записать по-разному, используя std::unique_lock вместо std::lock_guard:

std::unique_lock<std::mutex> l1( m, std::defer_lock );     // store in RAII, but do not lock
std::unique_lock<std::mutex> l2( rhs.m, std::defer_lock );
std::lock( l1, l2 );                                       // acquire the locks

Я просто подумал о другом, гораздо более простом подходе, который я здесь зарисовываю.Семантика немного отличается, но может быть достаточной для многих приложений:

test& test::operator=( test copy ) // pass by value!
{
   lock l(m);
   swap( d, copy.d );   // swap is not thread safe
   return *this;
}

}

Существует семантическое различие в обоих подходах, поскольку тот, который имеет идиому копирования и замены, имеетпотенциальное состояние гонки (которое может или не может повлиять на ваше приложение, но вы должны знать об этом).Поскольку обе блокировки никогда не удерживаются одновременно, объекты могут меняться в промежутке времени, когда первая блокировка снята (копия аргумента завершена), а вторая блокировка получена внутри operator=.

. Например, какэто может не сработать, примите во внимание, что data является целым числом и что у вас есть два объекта, инициализированные одним и тем же целочисленным значениемОдин поток получает обе блокировки и увеличивает значения, в то время как другой поток копирует один из объектов в другой:

test a(0), b(0); // ommited constructor that initializes the ints to the value
// Thr1
void loop() { // [1]
   while (true) {
      std::unique_lock<std::mutex> la( a.m, std::defer_lock );
      std::unique_lock<std::mutex> lb( b.m, std::defer_lock );
      std::lock( la, lb );
      ++a.d;
      ++b.d;
   }
}
// Thr1
void loop2() {
   while (true) {
      a = b; // [2]
   }
}
// [1] for the sake of simplicity, assume that this is a friend 
//     and has access to members

С реализациями operator=, которые выполняют одновременные блокировки для обоих объектов, вы можете утверждать вв любой момент времени (безопасное выполнение потоков путем получения обеих блокировок), что a и b одинаковы, что, как кажется, ожидается при быстром чтении кода.Это не выполняется, если operator= реализован в терминах идиомы копирования и обмена.Проблема в том, что в строке, помеченной как [2], b заблокирована и скопирована во временную, а затем разблокировка снята.Затем первый поток может получить обе блокировки одновременно и увеличить значения a и b до того, как a будет заблокирован вторым потоком в [2].Затем a перезаписывается значением, которое b имело до приращения.

4 голосов
/ 21 февраля 2011

Вы можете определить свой собственный конструктор копирования (и оператор назначения копирования). Конструктор копирования, вероятно, будет выглядеть примерно так:

cMyClass(const cMyClass& x) : A(x.getA()) { }

Обратите внимание, что getA() должен быть квалифицирован const, чтобы это работало, что означает, что мьютекс должен быть mutable; вы можете сделать параметр неконстантной ссылкой, но тогда вы не сможете скопировать временные объекты, что обычно нежелательно.

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

3 голосов
/ 22 февраля 2011

Простой факт заключается в том, что вы не можете сделать поток классов безопасным, извергнув мьютексы при проблеме. Причина, по которой вы не можете сделать эту работу, состоит в том, что она не работает , а не потому, что вы неправильно выполняете эту технику. Это то, что все заметили, когда впервые пришла многопоточность и начала убивать реализации строк COW.

Проектирование потоков происходит на уровне приложений, а не для отдельных классов. Только определенные классы управления ресурсами должны иметь безопасность потоков на этом уровне, и для них вам все равно нужно писать явные конструкторы копирования / операторы присваивания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...