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

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

Жанры

Стандарты программирования на С++. 101 правило и рекомендация

Александреску Андрей

Шрифт:

Заметим, что сказанное выше применимо независимо от того, является ли тип некоторым строковым типом, контейнером STL наподобие

vector
или некоторым иным типом. (Мы заметили, что ряд авторов дают советы, из которых вытекает, что стандартные контейнеры представляют собой нечто отдельное. Это не так — контейнер представляет собой просто объект другого типа.) В частности, если вы хотите использовать компоненты стандартной библиотеки (например,
string
или контейнеры) в многопоточной программе, проконсультируйтесь сначала с документацией разработчика используемой вами стандартной библиотеки, чтобы узнать, как именно следует пользоваться ею в многопоточном приложении.

При разработке собственного типа,

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

• Внешняя блокировка. За блокировку отвечает вызывающий код. При таком выборе код, который использует объект, должен сам выяснять, используется ли этот объект другими потоками и, если это так, отвечает за сериализацию его использования. Например, строковые типы обычно используют внешнюю блокировку (или неизменяемость — см. третью возможность).

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

Push
,
Pop
). В общем случае заметим, что этот вариант применим, только если вы знаете две вещи.

Во-первых, вы должны заранее знать о том, что объекты данного типа практически всегда будут совместно использоваться разными потоками; в противном случае вы просто разработаете бесполезную блокировку. Заметим, что большинство типов не удовлетворяют этому условию; подавляющее большинство объектов даже в программах с интенсивным использованием многопоточности не разделяются разными потоками (и это хорошо — см. рекомендацию 10).

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

find
, возвращающий верный ответ, который становится неверным до того, как вы им воспользуетесь, или пользователь напишет код
if (с.empty) с.push_back(x);
(другие примеры можно найти в [Sutter02]). В таких случаях вызывающая функция должна выполнить внешнюю блокировку на время выполнения всех отдельных вызовов функций-членов, так что отдельные блокировки для каждой функции-члена оказываются ненужной расточительностью.

Итак, внутренняя блокировка связана с открытым интерфейсом типа. Она становится применима только тогда,

когда отдельные операции типа являются сами по себе завершенными; другими словами, когда уровень абстракции типа растет и выражается и инкапсулируется более точно (например, как у очереди производитель/потребитель по отношению к обычному контейнеру
vector
). Объединение примитивных операций для образования более крупных общих операций — этот подход требуется для того, чтобы обеспечить возможность простого вызова функции с большим внутренним содержанием. В ситуациях, когда комбинирование примитивов может быть произвольным и вы не можете определить разумный набор сценариев использования в виде одной именованной операции, имеются две альтернативы. Можно воспользоваться моделью функций обратного вызова (т.е. вызывающая функция должна вызвать одну функцию-член, передавая ей задание, которое следует выполнить, в виде команды или объекта-функции; см. рекомендации с 87 по 89). Второй метод состоит в некотором способе предоставления вызывающему коду возможности блокировки в открытом интерфейсе.

• Проектирование, не требующее блокировок, включая неизменяемость (объекты, предназначенные только для чтения). Можно разработать типы, для которых блокировка окажется полностью ненужной (см. ссылки). Одним из распространенных примеров являются неизменяемые объекты, которые не требуют блокировки, поскольку они никогда не изменяются. Например, будучи создан, объект неизменяемого строкового типа больше не модифицируется, а все строковые операции приводят к созданию новых строк.

Заметим, что вызывающий код ничего не должен знать о деталях реализации ваших типов (см. рекомендацию 11). Если ваш тип внутренне использует какие-то методики разделения данных (например, копирование при записи), вы не должны нести ответственность за все возможные вопросы безопасности потоков, но обязаны обеспечить корректность работы вызывающего кода при обычной работе — т.е. тип должен быть безопасен в плане многопоточности в той же мере, как если бы он не использовал методики совместного использования данных (см. [Sutter04c]). Как упоминалось, все корректно написанные типы должны позволять работу с различными объектами в разных потоках без синхронизации.

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

#ifdef
).

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

Ссылки

[Alexandrescu02a] • [Alexandrescu04] • [Butenhof97] • [Henney00] • [Henney01] • [Meyers04] • [Schmidt01] • [Stroustrup00] §14.9 • [Sutter02] §16 • [Sutter04c]

13. Ресурсы должны быть во владении объектов

Резюме

Не работайте вручную, если у вас есть мощные инструменты. Идиома С++ "выделение ресурса есть инициализация" (resource acquisition is initialization — RAII) представляет собой мощный инструмент для корректной работы с ресурсами. RAII позволяет компилятору автоматически обеспечить строгую гарантию того, что в других языках надо делать вручную. При выделении ресурса передайте его объекту-владельцу. Никогда не выделяйте несколько ресурсов в одной инструкции.

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

Печать Пожирателя

Соломенный Илья
1. Пожиратель
Фантастика:
попаданцы
аниме
сказочная фантастика
фэнтези
5.00
рейтинг книги
Печать Пожирателя

Привет из Загса. Милый, ты не потерял кольцо?

Лисавчук Елена
Любовные романы:
современные любовные романы
5.00
рейтинг книги
Привет из Загса. Милый, ты не потерял кольцо?

Мастер 2

Чащин Валерий
2. Мастер
Фантастика:
фэнтези
городское фэнтези
попаданцы
технофэнтези
4.50
рейтинг книги
Мастер 2

Нечто чудесное

Макнот Джудит
2. Романтическая серия
Любовные романы:
исторические любовные романы
9.43
рейтинг книги
Нечто чудесное

Клан

Русич Антон
2. Долгий путь домой
Фантастика:
боевая фантастика
космическая фантастика
5.60
рейтинг книги
Клан

Имя нам Легион. Том 3

Дорничев Дмитрий
3. Меж двух миров
Фантастика:
боевая фантастика
рпг
аниме
5.00
рейтинг книги
Имя нам Легион. Том 3

Запасная дочь

Зика Натаэль
Фантастика:
фэнтези
6.40
рейтинг книги
Запасная дочь

Убивать чтобы жить 7

Бор Жорж
7. УЧЖ
Фантастика:
героическая фантастика
космическая фантастика
рпг
5.00
рейтинг книги
Убивать чтобы жить 7

У врага за пазухой

Коваленко Марья Сергеевна
5. Оголенные чувства
Любовные романы:
остросюжетные любовные романы
эро литература
5.00
рейтинг книги
У врага за пазухой

Кодекс Охотника. Книга XXI

Винокуров Юрий
21. Кодекс Охотника
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Кодекс Охотника. Книга XXI

Генерал Скала и ученица

Суббота Светлана
2. Генерал Скала и Лидия
Любовные романы:
любовно-фантастические романы
6.30
рейтинг книги
Генерал Скала и ученица

Оцифрованный. Том 1

Дорничев Дмитрий
1. Линкор Михаил
Фантастика:
боевая фантастика
попаданцы
аниме
5.00
рейтинг книги
Оцифрованный. Том 1

Его маленькая большая женщина

Резник Юлия
Любовные романы:
современные любовные романы
эро литература
8.78
рейтинг книги
Его маленькая большая женщина

Хуррит

Рави Ивар
Фантастика:
героическая фантастика
попаданцы
альтернативная история
5.00
рейтинг книги
Хуррит