Читаем без скачивания Системное программирование в среде Windows - Джонсон Харт
Шрифт:
Интервал:
Закладка:
Мы часто будем говорить о блокировании и разблокировании объектов CS, а вхождение в CS будет означать то же, что и блокирование CS.
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
Поток, владеющий объектом CS, может повторно войти в этот же CS без его блокирования; это означает, что объекты CRITICAL_SECTION являются рекурсивными (recursive). Поддерживается счетчик вхождений в объект CS, и поэтому поток должен покинуть данный CS столько раз, сколько было вхождений в него, чтобы разблокировать этот объект для других потоков. Эта возможность может оказаться полезной для реализации рекурсивных функций и обеспечения безопасного многопоточного выполнения функций общих (разделяемых) библиотек.
Выход из объекта CS, которым данный поток не владеет, может привести к непредсказуемым результатам, включая блокирование самого потока.
Для возврата из функции EnterCriticalSection не существует конечного интервала ожидания; другие потоки будут блокированы на неопределенное время, пока поток, владеющий объектом CS, не покинет его. Однако, используя функцию TryEnterCriticalSection, можно тестировать (опросить) CS, чтобы проверить, не владеет ли им другой поток.
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
Возврат функцией TryEnterCriticalSection значения True означает, что вызывающий поток приобрел права владения критическим участком кода, тогда как возврат значения False говорит о том, что данный критический участок кода уже принадлежит другого потока.
Объекты CRITICAL_SECTION обладают тем преимуществом, что они не являются объектами ядра и поддерживаются в пользовательском пространстве. Обычно, но не всегда, это приводит к дополнительному улучшению показателей производительности. К обсуждению аспектов производительности мы вернемся после того, как ознакомимся с объектами синхронизации, относящимися к ядру.
Настройка спин-счетчика
Обычно, если в результате выполнения функции EnterCriticalSection поток обнаруживает, что объект CS уже принадлежит другому потоку, он входит в ядро и остается блокированным до тех пор, пока не освободится объект CRITICAL_SECTION, что требует определенного времени. Однако в SMP-системах вы можете потребовать, чтобы поток повторил попытку завладеть объектом CS, прежде чем блокироваться, поскольку существует вероятность того, что поток, владеющий CS, выполняется на другом процессоре и в любой момент может освободить CS. Это может оказаться полезным для повышения производительности, если между потоками наблюдается высокая состязательность за право владения единственным объектом CRITICAL_SECTION. Влияние упомянутых факторов на производительность обсуждается далее в этой и последующих главах.
Для настройки счетчика занятости, или спин-счетчика (spin-count), предназначены две функции, одна из которых, SetCriticalSectionSpinCount, обеспечивает динамическую настройку счетчика, а вторая, InitializeCritical-SectionAndSpinCount, выступает в качестве замены функции Initialize-CriticalSection. Настройка спин-счетчика рассматривается в главе 9.
Использование объектов CRITICAL_SECTION для защиты разделяемыхпеременных
Использование объектов CRITICAL_SECTION не вызывает сложностей, и одним из наиболее распространенных способов их применения является обеспечение доступа потоков к разделяемым глобальным переменным. Рассмотрим, например, многопоточный сервер (аналогичный представленному на рис. 7.1), в котором необходимо вести учет следующих статистических данных:
• Общее количество полученных запросов.
• Общее количество отправленных ответов.
• Количество запросов, обрабатываемых в настоящее время всеми потоками сервера.
Поскольку переменные счетчиков являются глобальными переменными процесса, нельзя допустить того, чтобы одновременно два потока изменяли их значения. Один из методов обеспечения этого, базирующийся на применении объектов CRITICAL_SECTION, иллюстрирует схема, показанная ниже на рис. 8.2. Использование объектов CRITICAL_SECTION демонстрируется на примере программы 8.1, представляющей намного более простую систему, чем серверная.
Объекты CS могут привлекаться для решения задач, аналогичных той, которую иллюстрирует рис. 8.1, где два потока увеличивают значение одной и той же переменной. Приведенный ниже фрагмент кода обеспечивает нечто большее, нежели простое увеличение переменной, поскольку для этого достаточно было бы воспользоваться функциями взаимоблокировки. Обратите внимание на спецификатор volatile, предотвращающий размещение текущего значения переменной оптимизирующим компилятором в регистре, а не в ячейке памяти, отведенной для хранения переменной. Кроме того, в этом примере используется промежуточная переменная; этот необязательный элемент снижает эффективность программы, однако позволяет более отчетливо продемонстрировать, каким образом решается задача, иллюстрируемая рис. 8.1.
CRITICAL_SECTION cs1;
volatile DWORD N = 0, М;
/* N — глобальная переменная, разделяемая всеми потоками. */
InitializeCriticalSection (&cs1);
…
EnterCriticalSection (&cs1);
if (N < N_MAX) { M = N; M += 1; N = M; }
LeaveCriticalSection (&cs1);
…
DeleteCriticalSection (&cs1);
На рис. 8.2 представлена одна из возможных последовательностей выполнения программы для случая, изображенного на рис. 8.1, и продемонстрировано, каким образом объекты CS упрощают решение проблемы синхронизации.
Программа 8.1 демонстрирует, насколько полезными могут быть объекты CS.
Пример: простая система "производитель/потребитель"
Программа 8.1 иллюстрирует, насколько полезными могут быть объекты CS. Кроме того, эта программа демонстрирует, как создаются защищенные структуры данных для хранения состояний объектов, и знакомит с понятием инварианта (invariant) — свойства состояния объекта, относительно которого гарантируется (путем соответствующей реализации программы), что оно будет истинным за пределами критического участка кода.
Рис. 8.2. Разделение общей памяти синхронизированными потоками
Описание задачи приводится ниже.
• Имеются два потока, производитель (producer) и потребитель (consumer), работающие в полностью асинхронном режиме.
• Производитель периодически создает сообщения, содержащие таблицу чисел, например, таблицу биржевых котировок, которая периодически обновляется.
• По требованию пользователя потребитель отображает текущие данные. Требуется, чтобы отображаемые данные представляли собой самый последний полный набор данных, но никакие данные не должны отображаться дважды.
• Данные не должны отображаться в те промежутки времени, когда они обновляются производителем; устаревшие данные также не должны отображаться. Обратите внимание на то, что многие сообщения вообще никогда не используются и, таким образом, "теряются". Этот пример является частным случаем конвейерной модели, в которой данные передаются из одного потока в другой.
• В качестве средства контроля целостности данных производитель вычисляет простую контрольную сумму[28] данных таблицы, которая далее сравнивается с аналогичной суммой, вычисленной потребителем, дабы удостовериться в том, что данные не были повреждены при их передаче из одного потока в другой. Данные, полученные при обращении к таблице в моменты ее обновления, будут недействительными; использование объектов CS гарантирует, что этого никогда не произойдет. Инвариантом блока сообщения (message block invariant) является корректность контрольной суммы для содержимого текущего сообщения.
• Обоими потоками поддерживается статистика суммарного количества отправленных, полученных и утерянных сообщений.
Программа 8.1.simplePC: простая система "производитель/потребитель"/* Глава 8. simplePC.с */
/* Поддерживает два потока — производителя и потребителя. */
/* Производитель периодически создает буферные данные с контрольными */
/* суммами, или "блоки сообщений", отображаемые потребителем по запросу */
/* пользователя. */
#include "EvryThng.h"
#include <time.h>
#define DATA_SIZE 256
typedef struct msg_block_tag { /* Блок сообщения. */
volatile DWORD f_ready, f_stop; /* Флаги готовности и прекращения сообщений. */
volatile DWORD sequence; /* Порядковый номер блока сообщения. */
volatile DWORD nCons, nLost;
time_t timestamp;
CRITICAL_SECTION mguard; /* Структура защиты блока сообщения. */
DWORD checksum; /* Контрольная сумма содержимого сообщения. */
DWORD data[DATA_SIZE]; /* Содержимое сообщения. */
} MSG_BLOCK;