Листинг 4.10. Скелет программы реализации модели «изготовитель-потребитель»
pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER
pthread_t Thread[2]
Queue
// initial thread
{
pthread_create(&(Thread[1]...producer...);
pthread_create(&(Thread[2]...consumer...);
//...
}
void *producer(void *X)
{
loop
perform work
pthread_mutex_lock(&Mutex)
enqueue data
pthread_mutex_unlock(&Mutex)
signal consumer
//...
until done
}
void *consumer(void *X)
{
loop
suspend until signaled
loop while(Data Queue not empty)
pthread_mutex_lock(&Mutex)
dequeue data
pthread_mutex_unlock(&Mutex)
perform work
end loop
until done
}
//
В листинге 4.9 начальный поток создает оба потока: «изготовителя» и «потребителя». Поток- «изготовитель» содержит цикл, в котором после выполнения некоторых действий блокируется мьютекс для совместно используемой очереди, чтобы поместить в нее подготовленные для потребителя данные. После этого «изготовитель» деблокирует мьютекс и посылает сигнал потоку- «потребителю» о том, что ожидаемые им данные уже находятся в очереди. Поток- «изготовитель» выполняет итерации цикла до тех пор, пока не будет выполне н а вся работа. Поток- «потребитель» также выполняет цикл, в котором он приостанавливается до тех пор, пока не получит сигнал. Во внутреннем цикле поток- «потребитель» обрабатывает все данные до тех пор, пока не опустеет очередь. Он блокирует мьютекс для разде л яемой очереди перед извлечением из нее данных и деблокирует мьютекс после этого. Затем он выполняет обработку извлеченных данных. В программе 4.6 поток-«потребитель» помещает свои результаты в файл. Вместо файла может быть использована другая структура данных. Зачастую потоки-«потребители» играют две роли: как потребителя, так и изготовителя. Сначала возможно «исполнение» роли потребителя необработанных данных, подготовленных потоком-«изготовителем», а затем поток играет роль «изготовителя», когда он обрабатывает данные, сохраняемые в другой совместно используемой очереди, «потребляемой» другим потоком.
Создание многопоточных объектов
Модели делегирования, равноправных потоков, конвейера и типа «изготовитель» - «потребитель» предлагают деление программы на несколько потоков с помощью функций. При использовании объектов функции-члены могут создавать потоки выполнения нескольких задач. Потоки используются для выполнения кода от имени объекта: посредством отдельных функций и функций-членов.
В любом случае потоки объявляются в рамках объекта и создаются одной из функций-членов (например, конструктором). Потоки могут затем выполнять некоторые независимые функции (функции, определенные вне объекта), которые вызывают функции-члены глобальных объектов. Это — один из способов создания многопоточного объекта. Пример многопоточного объекта представлен в листинге 4.10.
// Листинг 4.11 . Объявление и определение многопоточного
// объекта
#include
#include
#include
void *taskl(void *);
void *task2(void *);
class multithreaded_object {
pthread_t Threadl,
Thread2; public:
multithreaded_object(void);
int cl(void);
int c2(void);
//.. .
);
multithreaded_object::multithreaded_object(void) {
//. . .
pthread_create(&Threadl, NULL, taskl, NULL); pthread_create(&Thread2 , NULL, task2 , NULL);
pthread_join(Threadl, NULL);
pthread_join(Thread2 , NULL);
//. . .
}
int multithreaded_object::cl(void) {
// Выполнение действий,
return(1);
}
int multithreaded_object::c2(void) {
// Выполнение действий,
return(1);
}
multithreaded_object MObj;
void *taskl(void *) {
//...
MObj.cl() ; return(NULL) ;
}
void *task2(void *) {
//...
M0bj.c2(); return(NULL) ;
}
В листинге 4.11 в классе multithread_object объявляются два потока. Они создаются и присоединяются к основному потоку в конструкторе этого класса. Поток Thread1 выполняет функцию task1 (), а поток Thread2 — функцию task2 (). Функции taskl () и task2 () затем вызывают функции-члены глобального объекта MObj.
В последовательной программе всю нагрузку можно разделить между отдельными подпрограммами таким образом, чтобы выполнение очередной подпрограммы было возможно только после завершения предыдущей. Существует и другая организация программ, когда, например, вся работа выполняется в виде мини-программ в рамках основной программы, причем эти мини-программы выполняются параллельно основной. Такие мини-программы могут быть реализованы как процессы или потоки. Если в реализации используются процессы, то каждый процесс должен иметь собственное адресное пространство, а если процессы должны взаимодействовать между собой, то такая реализация требует обеспечения механизма межпроцессного взаимодействия. Для потоков, разделяющих адресное пространство одного процесса, не нужны специальные методы взаимодействия. Но для защиты совместно используемой памяти (чтобы не допустить возникновения условий «гонок») необходимо использоватьтакие механизмы синхронизации, как мьютексы.
Читать дальше