Скажем, у нас есть что-то вроде следующего псевдокода с целью достижения параллелизма и использования преимуществ RAII:
class Foo {
public:
vector<int> nums;
mutex lock;
};
class Bar {
public:
Bar(Foo &foo) : m_foo(foo)
{
lock_guard<mutex>(foo.lock);
m_num = foo.nums.back();
foo.nums.pop_back();
}
~Bar()
{
lock_guard<mutex>(foo.lock);
foo.nums.push_back(m_num);
}
private:
Foo &m_foo;
int m_num;
};
Затем, скажем, у нас может быть любое количество экземпляров Bar, при этом идея состоит в том, что когда они выходят за пределы области видимости, деструктор вернет удерживаемый ими «ресурс» классу контроллера Foo. Однако нам также необходимо обеспечить потокобезопасность, а значит и блокировки. Однако я немного настороженно отношусь к этому дизайну, поскольку использование мьютекса в деструкторе интуитивно кажется плохой идеей. Я слишком много думаю, или, если нет, есть ли лучший способ воспользоваться преимуществами RAII здесь?
Получение блокировки в деструкторе ничем не отличается от получения блокировки в любой другой функции.
(Но не забудьте перехватить возможные исключения непосредственно в деструкторе; если деструктор вызывается во время раскручивания стека из другого исключения, а он, в свою очередь, выдает дополнительное исключение при попытке заблокировать мьютекс, программа завершится безвозвратно)
Если бы вы реорганизовали свой дизайн так, чтобы у foo были мьютексированные операции для извлечения и отправки, вместо того, чтобы делать все общедоступным, ваш вопрос разрешился бы сам собой.
@Taekahn Я не думаю, что это не разрешится само собой, если только я неправильно понимаю намерение, стоящее за вопросом. Операция pop по-прежнему будет вызываться в деструкторе, и это по-прежнему будет блокировать мьютекс. Проблемы с блокировкой мьютекса в деструкторе не исчезнут, если просто переместить блокировку мьютекса на другую функцию, которую будет вызывать деструктор.





Нет ничего плохого в том, чтобы заблокировать мьютекс в деструкторе. Например, может потребоваться сделать общие ресурсы потокобезопасными. Таким образом, для освобождения права собственности на общий ресурс может потребоваться блокировка мьютекса. Если бы RAII просто развалился в многопоточном программировании, это был бы не очень полезный инструмент. Действительно, доступ к блоку управления std::shared_ptr является потокобезопасным, в том числе при уменьшении счетчика ссылок во время уничтожения общего указателя. По-видимому, это обычно реализуется с помощью атомарных операций, а не блокировки мьютекса (не цитируйте меня по этому поводу), но контекст тот же: вы освобождаете право собственности на общий ресурс во время уничтожения, и это должно быть записано в потокобезопасный способ.
Однако имейте в виду: блокировка мьютекса может вызвать исключение, и вы должны (почти) всегда поглощать исключения в деструкторах с помощью try/catch. В противном случае, если стек уже раскручивается из-за другого исключения, программа завершится немедленно и безвозвратно, независимо от того, оборудован ли вызывающий код для поглощения исходного исключения и/или исключения деструктора.
Но может быть способ реструктурировать ваш код, чтобы полностью избежать проблемы: Bar на самом деле не нужна ссылка на Foo; нужен только int. В вашем коде Bar запрашивает int у заданного Foo. Когда Bar уничтожается, ему нужно вернуть int обратно Foo, чтобы его можно было переработать; для этого требуется хранить внутреннюю ссылку на Foo на протяжении всего его жизненного цикла, чтобы он мог взаимодействовать с ним во время уничтожения. Вместо этого рассмотрите возможность отдать intBar непосредственно при строительстве и забрать int у него при разрушении. Это движущий принцип внедрения зависимостей, который представляет собой букву «D» в слове «SOILD». Следовательно, он приносит с собой все типичные преимущества внедрения зависимостей (например, улучшение тестируемости класса Bar).
Например, эту логику можно отследить в более крупном классе, который управляет объектом Foo вместе со всеми связанными с ним объектами Bar. Вот некоторый псевдокод, но точные детали интерфейса будут зависеть от вашего приложения:
class BarPool:
Foo foo;
Map<int, Bar> bars;
mutex m;
BarPool(Foo foo) : foo(foo) {}
int add_bar():
lock m;
// Note: foo.pop() should probably be made thread-safe
// by internally locking / unlocking foo's mutex
int i = foo.pop()
bars.add(i, new Bar(i));
unlock m;
return i
void remove_bar(int i):
lock m;
// foo.push() should also probably be made thread-safe
bars.remove(i)
foo.push(i)
unlock m;
...
Еще одним преимуществом этого дизайна является то, что объект Bar не должен получать свой int от объекта Foo; он может получить его из любого места. Если вы решите, что хотите иметь набор объектов Bar, которые вместо этого получают свои int от объекта Baz, вы можете написать отдельный класс, который делает это. Вы можете дополнительно описать интерфейс для объектов IntCollection, реализованный Foo и Baz. Тогда BarPool будет зависеть только от абстрактного IntCollection интерфейса, что также соответствует принципу внедрения зависимостей.
Это неплохо, но я не могу сказать то же самое о дизайне кода. Если
Fooвладеет критическими данными, имеетmutex lock;, то он должен заботиться о защищенном доступе к данным.