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

<p>13. Ресурсы должны быть во владении объектов</p>Резюме
Перейти на страницу:

Все книги серии C++ In-Depth

Похожие книги