Роб Кёртен - Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform
Давайте перейдем теперь к потоку-«производителю» и рассмотрим, как он использует библиотеку ждущих блокировок. Вот его полная реализация:
producer() {
while (1) {
// Ждать прерывания от оборудования...
pthread_sleepon_lock();
data_ready = 1;
pthread_sleepon_signal(&data_ready);
pthread_sleepon_unlock();
}
}
Как вы видите, поток-«производитель» также блокирует мутекс, чтобы получить монопольный доступ к флагу data_ready перед его установкой.
Клиента «пробуждает» не установка флага data_ready в единицу (1), а вызов функции pthread_sleepon_signal()!
Давайте рассмотрим происходящее в подробностях. Определим состояния «потребителя» и «производителя» следующим образом:
Состояние Означает CONDVAR ожидание соответствующей ждущей блокировке условной переменной MUTEX ожидание мутекса READY состояние готовности, т.е., готов выполняться или уже выполняется INTERRUPT ожидание прерывания от аппаратных средств Действие Владелец мутекса Состояние «потребителя» Состояние «производителя» «потребитель» блокирует мутекс «потребитель» READY INTERRUPT «потребитель» проверяет флаг data_ready «потребитель» READY INTERRUPT потребитель вызывает функцию pthread_sleepon_wait() «потребитель» READY INTERRUPT функция pthread_sleepon_wait() разблокирует мутекс мутекс свободен READY INTERRUPT функция pthread_sleepon_wait() блокируется мутекс свободен CONDVAR INTERRUPT пауза до прерывания мутекс свободен CONDVAR INTERRUPT аппаратные средства генерируют данные мутекс свободен CONDVAR READY «производитель» блокирует мутекс «производитель» CONDVAR READY «производитель» устанавливает флаг data_ready «производитель» CONDVAR READY «производитель» вызывает pthread_sleepon_signal() «производитель» CONDVAR READY «потребитель» «пробуждается», функция pthread_sleepon_wait() пытается заблокировать мутекс «производитель» MUTEX READY «производитель» разблокирует мутекс мутекс свободен MUTEX READY «потребитель» получает мутекс «потребитель» READY READY «потребитель» обрабатывает данные «потребитель» READY READY «производитель» ждет новых данных от аппаратуры «потребитель» READY INTERRUPT пауза («потребитель» обрабатывает полученные данные) «потребитель» READY INTERRUPT «потребитель» завершает обработку и разблокирует мутекс мутекс свободен READY INTERRUPT «потребитель» возвращается в начало цикла и блокирует мутекс «потребитель» READY INTERRUPTПоследняя строка в таблице повторяет первую — мы совершили один полный цикл.
Каково назначение флага data_ready? Он служит для двух целей:
• Он является флагом состояния — посредником между «потребителем» и «производителем», указывающим на состояние системы. Если флаг установлен в состояние 1, это означает, что данные доступны для обработки; если этот флаг установлено в состояние 0, это означает, что данных нет, и поток-потребитель должен быть заблокирован.
• Он выполняет функцию «места, где происходит синхронизация со ждущей блокировкой». Более формально говоря, адрес переменной data_ready используется как уникальный идентификатор объекта, по которому осуществляется ждущая блокировка. Мы запросто могли бы применить «(void*)12345» вместо «&data_ready» — библиотеке ждущих блокировок все равно, что это за идентификатор, лишь бы он был уникален и корректно использовался. Использование же в качестве идентификатора адреса переменной есть надежный способ сгенерировать уникальный номер, поскольку не бывает же двух переменных с одинаковым адресом!
• К обсуждению различий между функциями pthread_sleepon_signal() и pthread_sleepon_broadcast() мы еще вернемся в разговоре об условных переменных.
Условные переменные
Условные переменные (или «condvars») очень похожи на ждущие блокировки, которые мы рассматривали выше. В действительности, ждущие блокировки — это надстройка над механизмом условных переменных, и именно поэтому в таблице, иллюстрировавшей использование ждущих блокировок, у нас встречалось состояние CONDVAR. Функция pthread_cond_wait() точно так же освобождает мутекс, ждет, а затем повторно блокирует мутекс, аналогично функции pthread_sleepon_wait().
Давайте опустим вступление и обратимся к нашему примеру о «производителе» и «потребителе» из раздела о ждущих блокировках, но вместо ждущих блокировок будем использовать условные переменные. А затем уже обсудим вызовы.
/*
* cp1.c
*/
#include <stdio.h>
#include <pthread.h>
int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;
void* consumer(void *notused){
printf("Это поток-потребитель...n");
while (1) {
pthread_mutex_lock(&mutex);
while (!data_ready) {
pthread_cond_wait(&condvar, &mutex);
}
// Обработать данные
printf("Потребитель: получил данные от производителяn");
data_ready = 0;
pthread_cond_signal(&condvar);
pthread_mutex_unlock(&mutex);
}
}
void* producer (void *notused) {
printf("Это поток-производитель...n");
while (1) {
// Получить данные от оборудования
// (мы имитируем это при помощи sleep(1))
sleep(1);
printf("Производитель: получил данные от h/wn");
pthread_mutex_lock(&mutex);
while (data_ready) {
pthread_cond_wait(&condvar, &mutex);
}
data_ready = 1;
pthread_cond_signal(&condvar);
pthread_mutex_unlock(&mutex);
}
}
main() {
printf(
"Начало примера с производителем и потребителем...n");
// Создать поток-производитель и поток-потребитель
pthread_create(NULL, NULL, producer, NULL);
pthread_create(NULL, NULL, consumer, NULL);
// Дать потокам немного повыполняться
sleep(20);
}
Этот пример в значительной степени похож на программу с применением ждущей блокировки, с небольшими отличиями (мы добавили несколько вызовов printf(), а также функцию main(), чтобы программа могла работать!) Первое отличие, которое бросается в глаза, — здесь использован новый тип данных, pthread_cond_t. Это просто декларация для условной переменной; мы назвали нашу условную переменную condvar.