Ни одно из описанных выше решений с блокировкой не решает этих проблем. Вызовы begin
и end
в строке 1 сразу возвращают управление, сгенерированные ими итераторы остаются действительными только до конца строки, а find
тоже возвращает управление в конце строки.
Чтобы этот фрагмент был потоково-безопасным, блокировка v
должна сохраняться от строки 1 до строки 3. Трудно представить, каким образом реализация STL могла бы автоматически придти к такому выводу. А если учесть, что использование примитивов синхронизации (семафоров, мьютексов [1] В среде программистов данный термин (англ. mutex) встречается также в варианте «мутекс». — Примеч. ред.
и т. д.) обычно сопряжено с относительно высокими затратами, еще труднее представить, каким образом реализация могла бы сделать это без значительного снижения быстродействия по сравнению с программами, которым априорно известно, что в строках 1-3 с v
будет работать только один программный поток.
Понятно, почему в решении проблем многопоточности не стоит полагаться на реализацию STL. Вместо этого в подобных случаях следует самостоятельно синхронизировать доступ. В приведенном примере это может выглядеть так:
vector v;
…
getMutexFor(v);
vector::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) {// Теперь эта строка безопасна
*first5 = 0; // И эта строка тоже
}
releaseMutexFor(v);
В другом, объектно-ориентированном, решении создается класс Lock
, который захватывает мьютекс в конструкторе и освобождает его в деструкторе, что сводит к минимуму вероятность вызова getMutexFor
без парного вызова releaseMutexFor
. Основа такого класса (точнее, шаблона) выглядит примерно так:
template // Базовый шаблон для классов,
class Lock{ // захватывающих и освобождающих мьютексы
public:// для контейнеров: многие технические
// детали опущены
Lock(const Containers container) : c(container) {
getMutexFor(с);// Захват мьютекса в конструкторе
}
~Lock () {
releaseMutexFor(c); // Освобождение мьютекса в деструкторе
}
private:
const Container& с;
Концепция управления жизненным циклом ресурсов (в данном случае — мьютексов) при помощи специализированных классов вроде Lock
рассматривается в любом серьезном учебнике C++. Попробуйте начать с книги Страуструпа (Stroustrup) «The C++ Programming Language» [7], поскольку именно Страуструп популяризировал эту идиому, однако информацию также можно найти в совете 9 «More Effective C++». Каким бы источником вы ни воспользовались, помните, что приведенный выше класс Lock
урезан до абсолютного минимума. Полноценная версия содержала бы многочисленные дополнения, не имеющие никакого отношения к STL. Впрочем, несмотря на минимализм, приведенная версия Lock
вполне может использоваться в рассматриваемом примере:
vector v;
…
{ // Создание нового блока
Lock > lock(v); // Получение мьютекса
vector::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) {
*first5 = 0;
}
} // Закрытие блока с автоматическим
// освобождением мьютекса
Поскольку мьютекс контейнера освобождается в деструкторе Lock
, важно обеспечить уничтожение Lock
сразу же после освобождения мьютекса. Для этого мы создаем новый блок, в котором определяется объект Lock
, и закрываем его, как только надобность в мьютексе отпадает. На первый взгляд кажется, что вызов releaseMutexFor
попросту заменен необходимостью закрыть блок, но это не совсем так. Если мы забудем создать новый блок для Lock
, мьютекс все равно будет освобожден, но это может произойти позднее положенного момента — при выходе из внешнего блока. Если забыть о вызове releaseMutexFor
, мьютекс вообще не освобождается.
Более того, решение, основанное на классе Lock
, лучше защищено от исключений. C++ гарантирует уничтожение локальных объектов при возникновении исключения, поэтому Lock
освободит мьютекс, даже если исключение произойдет при использовании объекта Lock
. При использовании парных вызовов getMutexFor/releaseMutexFor
мьютекс не будет освобожден, если исключение происходит после вызова getMutexFor
, но перед вызовом releaseMutexFor
.
Читать дальше