Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform
Шрифт:
В этом
Это в полной мере отражает второй вариант ответа на наш вопрос «а почему они ждут?» Так как все потоки все ждут соблюдения различных условий (поток thread1 ждет, пока значение x не станет меньше или равно 7, или пока значение у не станет равным 15, поток thread2 ждет, пока значение x не станет простым числом, а поток thread3 ждет, пока x не станет равным у), у нас нет никакого выбора, кроме как «разбудить» все потоки «одновременно».
Ждущие блокировки имеют одно основное преимущество в сравнении с условными переменными. Предположим, что вам надо синхронизировать множество объектов. Используя условные переменные, вы бы ассоциировали с каждым объектом отдельную условную переменную — если бы у вас было M объектов, вы, скорее всего, определили бы M условных переменных. При применении же ждущих блокировок соответствующие им условные переменные создаются динамически по мере постановки потоков на ожидание, поэтому в этом случае на M объектов и N блокированных потоков у вас было бы максимум N, а не M условных переменных.
Однако, условные переменные более универсальны, чем ждущие блокировки, и вот почему:
1. Ждущие блокировки в любом случае основаны на условных переменных.
2. Мутексы ждущих блокировок скрыты в библиотеке; условные переменные позволяют вам задавать его явно.
Первый пункт сам по себе достаточно убедителен. :-) Второй, однако, имеет еще и практический смысл. Когда мутекс скрыт в библиотеке, это означает, что он может быть только один на процесс, независимо от числа потоков в этом процессе или от количества переменных. Это может быть сильно ограничивающим фактором, особенно если принять во внимание, что вам придется использовать один-единственный мутекс для синхронизации доступа всех имеющихся потоков в процессе ко всем нужным им переменным!
Намного лучшая схема состоит в применении нескольких мутексов — по одному на каждый набор данных — и явно сопоставлять им условные переменные по мере необходимости. Как мощь, так и опасность этого подхода заключаются в том, что ни на этапе компиляции, ни на этапе выполнения не будет производиться никаких проверок, и вам придется самим следить за:
• блокировкой мутексов перед доступом к соответствующим переменным;
• применением правильного мутекса для каждой переменной;
• применением правильной условной переменной для соответствующих мутекса и переменной (данных).
Самый простой путь решения этих проблем — грамотно проектировать и тщательно проверять, а также заимствовать приемы объектно-ориентированного программирования (например, встраивать мутексы в структуры данных, создавать для обращения к структурам данных специализированные подпрограммы, и т.д.). Разумеется, то, в какой степени вы примените первый, второй, или оба варианта, будет зависеть не только от вашего стиля программирования, но и от требований производительности.
Ключевыми моментами при использовании условных переменных являются:
1. Мутексы следует использовать для проверки и изменения переменных.
2. Условные переменные следует использовать в качестве «точки встречи».
Ниже представлена иллюстрация этого:
Связь мутексов и условных переменных по схеме «один к одному»
Одно интересное замечание. Поскольку никаких проверок не выполняется, вы можете, например, связать один набор переменных с мутексом «MutexABC», другой — с мутексом «MutexDEF», и сопоставить обоим наборам переменных одну и ту же условную переменную «CondvarABCDEF»:
Связь мутексов и условных переменных по схеме «один ко многим».
Это весьма полезное свойство. Поскольку мутекс должен использоваться для «проверки и изменения» всегда, это подразумевает, что я должен буду выбрать правильный мутекс всякий раз, когда мне понадобится доступ к некоей переменной. Вполне логично — если я, скажем, проверяю переменную «С», то, очевидно, мне потребуется заблокировать мутекс «MutexABC». А что если я хочу изменить переменную «E»? Хорошо, перед этим я должен буду захватить мутекс «MutexDEF». Затем я ее изменяю и сообщаю об этом другим потокам через условную переменную «CondvarABCDEF», после чего освобождаю мутекс.
А теперь смотрите, что происходит. Толпа потоков, ждавших на условии «CondvarABCDEF», вдруг резко «просыпается» (по функции pthread_cond_wait). Их функции ожидания немедленно пытаются повторно захватить мутекс. Критическим моментом здесь является то, что мутексов два. (В зависимости от того, изменения какой переменной поток ждал, его функция ожидания попытается захватить либо MutexABC, либо MutexDEF — прим. ред.) Это означает, что в SMP-системе возникли бы две конкурирующие очереди потоков, и в каждой потоки будут проверять как бы независимые переменные, используя при этом независимые мутексы. Круто, да?
Дополнительные сервисы QNX/Neutrino
QNX/Neutrino позволяет делать еще ряд изящных вещей. POSIX утверждает, что с мутексом должны работать потоки одного и того же процесса, но позволяет в соответствующей реализации эту концепцию расширять. В QNX/Neutrino это расширение сводится к тому, что мутекс может использоваться потоками различных процессов. Чтобы понять, почему это работает, вспомните: то, что мы рассматриваем как «операционную систему», реально состоит из двух частей — ядра, которое занимается диспетчеризацией, и администратора процессов, который, наряду со всем остальным, заботится о защите памяти и «процессах». Мутекс — всего-навсего объект синхронизации потоков. Поскольку ядро работает только с потоками, то реально ему все равно, какие потоки работают в каких процессах, это уже забота администратора.