Стандарты программирования на С++. 101 правило и рекомендация
Шрифт:
Заметим, что сказанное выше применимо независимо от того, является ли тип некоторым строковым типом, контейнером STL наподобие
При разработке собственного типа,
• Внешняя блокировка. За блокировку отвечает вызывающий код. При таком выборе код, который использует объект, должен сам выяснять, используется ли этот объект другими потоками и, если это так, отвечает за сериализацию его использования. Например, строковые типы обычно используют внешнюю блокировку (или неизменяемость — см. третью возможность).
• Внутренняя блокировка. Каждый объект сериализует все обращения к себе, обычно путем соответствующего блокирования всех открытых функций-членов, так что пользователю не надо предпринимать никаких дополнительных действий по сериализации использования объекта. Например, очереди производитель/потребитель обычно используют внутреннюю блокировку, поскольку сам смысл их существования состоит в совместном использовании разными потоками, и их интерфейсы спроектированы с использованием блокировок соответствующего уровня для отдельных вызовов функций-членов (
Во-первых, вы должны заранее знать о том, что объекты данного типа практически всегда будут совместно использоваться разными потоками; в противном случае вы просто разработаете бесполезную блокировку. Заметим, что большинство типов не удовлетворяют этому условию; подавляющее большинство объектов даже в программах с интенсивным использованием многопоточности не разделяются разными потоками (и это хорошо — см. рекомендацию 10).
Во-вторых, вы должны заранее быть уверены, что блокировка на уровне функций обеспечивает корректный уровень модульности, которого достаточно для большинства вызывающий функций. В частности, интерфейс типа должен быть спроектирован в пользу самодостаточных операций с невысокой степенью детализации. Если вызывающий код должен блокировать несколько операций, а не одну, то такой способ неприменим. В этом случае отдельные функции могут быть собраны в блокируемый модуль большего масштаба, работа с которым выполняется при помощи дополнительной (внешней) блокировки. Например, рассмотрим тип, который возвращает итератор, который может стать недействительным перед тем, как вы используете его, или предоставляет алгоритм наподобие
Итак, внутренняя блокировка связана с открытым интерфейсом типа. Она становится применима только тогда,
• Проектирование, не требующее блокировок, включая неизменяемость (объекты, предназначенные только для чтения). Можно разработать типы, для которых блокировка окажется полностью ненужной (см. ссылки). Одним из распространенных примеров являются неизменяемые объекты, которые не требуют блокировки, поскольку они никогда не изменяются. Например, будучи создан, объект неизменяемого строкового типа больше не модифицируется, а все строковые операции приводят к созданию новых строк.
Заметим, что вызывающий код ничего не должен знать о деталях реализации ваших типов (см. рекомендацию 11). Если ваш тип внутренне использует какие-то методики разделения данных (например, копирование при записи), вы не должны нести ответственность за все возможные вопросы безопасности потоков, но обязаны обеспечить корректность работы вызывающего кода при обычной работе — т.е. тип должен быть безопасен в плане многопоточности в той же мере, как если бы он не использовал методики совместного использования данных (см. [Sutter04c]). Как упоминалось, все корректно написанные типы должны позволять работу с различными объектами в разных потоках без синхронизации.
В частности, если вы разрабатываете библиотеку, предназначенную для широкого использования, вы должны предусмотреть безопасность ваших объектов в многопоточных программах, как описано выше, но при этом без дополнительных накладных расходов при работе в однопоточной программе. Например, если вы пишете библиотеку, содержащую тип, использующий копирование при записи, и вы должны обеспечить, как минимум, некоторую внутреннюю блокировку, то предпочтительно разработать ее так, чтобы в однопоточном варианте вашей библиотеки ее не было (обычно для этого используются директивы препроцессора
Если используется несколько блокировок, то избежать взаимоблокировки можно путем их запроса в одинаковом порядке (освобождение блокировок может выполняться в любом порядке). Одно из решений состоит в запросе блокировок в порядке возрастания адресов в памяти, что обеспечивает удобное, однозначное упорядочение в пределах приложения.
[Alexandrescu02a] • [Alexandrescu04] • [Butenhof97] • [Henney00] • [Henney01] • [Meyers04] • [Schmidt01] • [Stroustrup00] §14.9 • [Sutter02] §16 • [Sutter04c]
13. Ресурсы должны быть во владении объектов
Не работайте вручную, если у вас есть мощные инструменты. Идиома С++ "выделение ресурса есть инициализация" (resource acquisition is initialization — RAII) представляет собой мощный инструмент для корректной работы с ресурсами. RAII позволяет компилятору автоматически обеспечить строгую гарантию того, что в других языках надо делать вручную. При выделении ресурса передайте его объекту-владельцу. Никогда не выделяйте несколько ресурсов в одной инструкции.