Чтение онлайн

на главную - закладки

Жанры

Эффективное использование STL
Шрифт:

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

«Золотой стандарт» поддержки многопоточности в контейнерах STL (которым руководствуется большинство разработчиков) был определен компанией SGI и опубликован на ее web-сайте, посвященном STL [21]. Фактически в нем сказано, что в лучшем случае можно надеяться на следующее:

• безопасность параллельного чтения. Несколько потоков могут одновременно читать содержимое контейнера, и это не помешает его правильной работе. Естественно, запись в контейнер при этом не допускается;

• безопасность записи в разные контейнеры. Несколько потоков могут одновременно производить запись в разные контейнеры.

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

Многопоточное программирование считается сложной задачей, и многие программисты желают, чтобы реализации STL изначально обеспечивали полную потоковую безопасность. Это избавило бы их от необходимости самостоятельно синхронизировать доступ. Конечно, это было бы очень удобно, однако добиться этой цели очень сложно. Рассмотрим несколько способов реализации полной потоковой безопасности контейнеров:

• блокировка контейнера на время вызова любой функции;

• блокировка контейнера в течение жизненного цикла каждого возвращаемого итератора (например посредством вызова

begin
или
end
);

• блокировка контейнера на протяжении работы каждого алгоритма, вызванного для этого контейнера. В действительности это бессмысленно, поскольку, как будет показано в совете 32, алгоритм не располагает средствами идентификации контейнера, с которым он работает. Тем не менее, мы изучим этот вариант — будет поучительно увидеть, почему он в принципе неработоспособен.

Рассмотрим следующий фрагмент, который ищет в

vector<int>
первое вхождение числа 5 и заменяет его нулем:

vector<int> v;

vector<int>::iterator first5(find(v.begin, v.end, 5)); // Строка 1

if (first5 != v.end) { // Строка 2

 *first5 = 0; // Строка 3

}

В многопоточной среде существует вероятность того, что другой поток изменит содержимое

v
сразу же после выполнения строки 1. Если это произойдет, сравнение
first5
с
v.end
в строке 2 становится бессмысленным, поскольку содержимое
v
будет не тем, каким оно было в конце строки 1. Более того, такая проверка может привести к непредсказуемым результатам, поскольку третий поток может перехватить управление между строками 1 и 2 и сделать
first5
недействительным (например, при выполнении вставки вектор может заново выделить память, вследствие чего все итераторы данного вектора станут недействительными. За подробностями о перераспределении памяти обращайтесь к совету 14). Присваивание
*first5
в строке 3 тоже небезопасно, поскольку между строками 2 и 3 другой поток может удалить элемент, на который указывает (или, по крайней мере, указывал раньше) итератор
first5
.

Ни одно из описанных выше решений с блокировкой не решает этих проблем. Вызовы

begin
и
end
в строке 1 сразу возвращают управление, сгенерированные ими итераторы остаются действительными только до конца строки, а
find
тоже возвращает управление в конце строки.

Чтобы этот фрагмент был потоково-безопасным, блокировка

v
должна сохраняться от строки 1 до строки 3. Трудно представить, каким образом реализация STL могла бы автоматически придти к такому выводу. А если учесть, что использование примитивов синхронизации (семафоров, мьютексов [1] и т. д.) обычно сопряжено с относительно высокими затратами, еще труднее представить, каким образом реализация могла бы сделать это без значительного снижения быстродействия по сравнению с программами, которым априорно известно, что в строках 1-3 с
v
будет работать только один программный поток.

1

В среде программистов данный термин (англ. mutex) встречается также в варианте «мутекс». — Примеч. ред.

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

vector<int> v;

getMutexFor(v);

vector<int>::iterator first5(find(v.begin, v.end, 5));

if (first5 != v.end) {// Теперь эта строка безопасна

 *first5 = 0; // И эта строка тоже

}

releaseMutexFor(v);

В другом, объектно-ориентированном, решении создается класс

Lock
, который захватывает мьютекс в конструкторе и освобождает его в деструкторе, что сводит к минимуму вероятность вызова
getMutexFor
без парного вызова
releaseMutexFor
. Основа такого класса (точнее, шаблона) выглядит примерно так:

template<typename Container> // Базовый шаблон для классов,

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
вполне может использоваться в рассматриваемом примере:

Поделиться:
Популярные книги

Наследство Карны

Вассму Хербьёрг
3. Книга Дины
Проза:
современная проза
5.00
рейтинг книги
Наследство Карны

Эволюционер из трущоб. Том 9

Панарин Антон
9. Эволюционер из трущоб
Фантастика:
попаданцы
аниме
фэнтези
фантастика: прочее
5.00
рейтинг книги
Эволюционер из трущоб. Том 9

Я еще князь. Книга XX

Дрейк Сириус
20. Дорогой барон!
Фантастика:
юмористическое фэнтези
попаданцы
аниме
5.00
рейтинг книги
Я еще князь. Книга XX

Гримуар тёмного лорда I

Грехов Тимофей
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Гримуар тёмного лорда I

Очерки времен и событий из истории российских евреев. 1945 – 1970 гг. Книга 6

Кандель Феликс Соломонович
Научно-образовательная:
история
5.00
рейтинг книги
Очерки времен и событий из истории российских евреев. 1945 – 1970 гг. Книга 6

Развод в 45. От любви до ненависти

Гофман Крис
6. Развод
Любовные романы:
остросюжетные любовные романы
5.40
рейтинг книги
Развод в 45. От любви до ненависти

Адвокат империи

Карелин Сергей Витальевич
1. Адвокат империи
Фантастика:
городское фэнтези
попаданцы
фэнтези
5.75
рейтинг книги
Адвокат империи

Дама с коготками

Донцова Дарья
3. Любительница частного сыска Даша Васильева
Детективы:
иронические детективы
8.36
рейтинг книги
Дама с коготками

С Том 3

Вязовский Алексей
3. Сапер
Фантастика:
попаданцы
альтернативная история
5.00
рейтинг книги
С Том 3

Охотник за головами

Вайс Александр
1. Фронтир
Фантастика:
боевая фантастика
космическая фантастика
5.00
рейтинг книги
Охотник за головами

Газлайтер. Том 29

Володин Григорий Григорьевич
29. История Телепата
Фантастика:
боевая фантастика
попаданцы
аниме
5.00
рейтинг книги
Газлайтер. Том 29

Абордажник

Султанов Дмитрий Игоревич
Вселенная EVE Online
Фантастика:
боевая фантастика
космическая фантастика
8.05
рейтинг книги
Абордажник

Вечный. Книга VI

Рокотов Алексей
6. Вечный
Фантастика:
рпг
фэнтези
5.00
рейтинг книги
Вечный. Книга VI

Девочка из прошлого

Тоцка Тала
3. Айдаровы
Любовные романы:
современные любовные романы
5.00
рейтинг книги
Девочка из прошлого