Второе введем условную переменную. Переменные и условный оператор. Условные переменные

Условную переменную можно использовать для реализации отношений синхронизации, о которых упоминалось выше: старт-старт (CC), финиш-старт (ФС), старт-финиш (СФ) и финиш-финиш (ФФ). Эти отношения могут существовать между потоками одного или различных процессов. В листингах 5.4 и 5.5 представлены примеры реализации ФС- и ФФ-отношений синхронизации. В каждом примере определено два мьютекса. Один мьютекс используется для синхронизации доступа к общим данным, а другой - для синхронизации выполнения кода.

// Листинг 5.4. ФС-отношения синхронизации между

// двумя потоками

pthread_t ThreadA,ThreadB;

pthread_cond_t Event;

void * worker1 (void *X) {

for(int Count = l;Count

pthread_mutex_lock(&Mutex);

pthread_mutex_unlock(&Mutex);

if(Number == 50){

pthread_cond_signal(&Event);

void * worker2 (void *X) {

pthread_mutex_lock(&EventMutex);

pthread_cond_wait(&Event,&EventMutex);

for(int Count = 1;Count

pthread_mutex_lock(&Mutex);

Number = Number + 20;

pthread_mutex_unlock(&Mutex);

cout « «Выполнение функции worker2 завершено.» « endl; return(0);

int main (int argc, char *argv) {

pthread_mutex_init(&Mutex,NULL);

pthread_mutex_init(&EventMutex,NULL);

pthread_cond_init(&Event, NULL);

pthread_create(&ThreadA, NULL, workerl, NULL);

pthread_create(&ThreadB, NULL, worker2 , NULL);

В листинге 5.4 показан пример реализации ФС-отношений синхронизации. Поток ThreadA не может завершиться до тех пор, пока не стартует поток ThreadB. Если значение переменной Number станет равным 50, поток ThreadA сигнализирует о этом потоку ThreadB. Теперь он может продолжать выполнение до самого конца Поток ThreadB не может начать выполнение до тех пор, пока не получит сигнал от потока ThreadA. Поток ThreadB использует объект EventMutex вместе с условной переменной Event. Объект Mutex используется для синхронизации доступа для записи значения разделяемой переменной Number. Для синхронизации различных событий и доступа к критическим разделам задача может использовать несколько мьютексов.

Пример реализации ФФ-отношений синхронизации показан в листинге 5.5.

// Листинг 5.5. ФФ-отношения синхронизации между // двумя потоками

pthread_t ThreadA, ThreadB ;

pthread_mutex_t Mutex, EventMutex;

pthread_cond_t Event;

void *workerl(void *X) {

for(int Count = l;Count

pthread_mu tex_l ock (&Mutex);

pthread_mutex_unlосk(&Mutex);

cout « «workerl: число равно "

pthread_mutex_lock(&EventMutex) ,-

cout « «Функция workerl в состоянии ожидания. " « endl;

pthread_cond_wait (&Event, &EventMutex) ;

pthread_mutex_unlock(&EventMutex);

void *worker2 (void *X) {

for(int Count = l;Count

pthread_mutex_lock(&Mutex) ;

Number = Number * 2 ;

pthread_mutex_unlock(&Mutex) ;

cout « «worker2: число равно " « Number « endl;

pthread_cond_signal (&Event) ;

cout « «Функция worker2 послала сигнал " « endl; return(0);

int main(int argc, char *argv) {

pthread_mutex_init (&Mutex,NULL) ;

pthread_mutex_init (&EventMutex,NULL) ;

pthread_cond_init (&Event, NULL) ;

pthread_create(&ThreadA, NULL,workerl, NULL);

pthread_create (&ThreadB, NULL, worker2, NULL) ;

В листинге 5.5 поток ThreadA не может завершиться до тех пор, пока не завершится поток ThreadB. Поток ThreadA должен выполнить цикл 10 раз, а ThreadB - 100. Поток ThreadA завершит выполнение своих итераций раньше ThreadB, но будет ожидать до тех пор, пока поток ThreadB не просигналит о своем завершении.

CC- и СФ-отношения синхронизации невозможно реализовать подобным образом. Эти методы используются для синхронизации пор я дка выполнени я процессов.

Для того чтобы иметь возможность реализовать логику в программе используются условные операторы. Умозрительно эти операторы можно представить в виде узловых пунктов, достигая которых программа делает выбор по какому из возможных направлений двигаться дальше. Например, требуется определить, содержит ли некоторая переменная arg положительное или отрицательное число и вывести соответствующее сообщение на экран. Для этого можно воспользоваться оператором if (если), который и выполняет подобные проверки.

В самом простом случае синтаксис данного оператора if следующий:

if (выражение)

Если значение параметра «выражение» равно «истинно», выполняется оператор, иначе он пропускается программой. Следует отметить, что «выражение» является условным выражением, в котором выполняется проверка некоторого условия. В табл. 2.1 представлены варианты простых логических выражений оператора if.

Таблица 2.1. Простые логические выражения

Приведем пример использования оператора ветвления if. Следующая программа позволяет определять знак введенной переменной.

Листинг 2.1. Первая программа определения знака введенного числа.

#include
int main()
{
float x;
printf(“Введите число: ”);
scanf(“%f”,&x);
if(x >= 0)

Return 0;
}

Анализ приведенного текста программы показывает, что два условных оператора можно заменить одним, используя конструкцию

if (выражение)

которая интерпретируется таким образом. Если «выражение» истинно, то выполняется «оператор1», иначе выполняется «оператор2». Перепишем ранее приведенный пример определение знака числа с использованием данной конструкции.

Листинг 2.2. Вторая программа определения знака введенного числа.

#include
int main()
{
float x;
printf(“Введите число: ”);
scanf(“%f”,&x);
if(x printf(“Введенное число %f является отрицательным.\n”, x);
else
printf(“Введенное число %f является неотрицательным.\n”, x);

Return 0;
}

В представленных примерах после операторов if и else стоит всего одна функция printf(). В случаях, когда при выполнении какого-либо условия необходимо записать более одного оператора, необходимо использовать фигурные скобки, т.е. использовать конструкцию вида

if (выражение)
{

}
else
{

Следует отметить, что после ключевого слова else формально можно поставить еще один оператор условия if, в результате получим еще более гибкую конструкцию условных переходов:

if(выражение1)
else if(выражение2)
else

На листинге 2.3 представлена программа, реализующая последнюю конструкцию условных переходов.

Листинг 2.3. Третья программа определения знака введенного числа.

#include
int main()
{
float x;
printf(“Введите число: ”);
scanf(“%f”,&x);
if(x printf(“Введенное число %f является отрицательным.\n”, x);
else if(x > 0)
printf(“Введенное число %f является положительным.\n”, x);
else
printf(“Введенное число %f является неотрицательным.\n”,x);

Return 0;
}

До сих пор рассматривались простые условия типа x && - логическое И
|| - логическое ИЛИ
! – логическое НЕТ

На основе этих трех логических операций можно сформировать более сложные условия. Например, если имеются три переменные exp1, exp2 и exp3, то они могут составлять логические конструкции, представленные в табл. 2.2.

Таблица 2.2. Пример составных логических выражений

Подобно операциям умножения и сложения в математике, логические операции И ИЛИ НЕТ, также имеют свои приоритеты. Самый высокий приоритет имеет операция НЕТ, т.е. такая операция выполняется в первую очередь. Более низкий приоритет у операции И, и наконец самый малый приоритет у операции ИЛИ. Данные приоритеты необходимо учитывать, при составлении сложных условий. Например, условие

if(4 6 || 5 проверяется таким образом. Если 4 6 ИЛИ 5 if(4 6 || 5 Условная операция if облегчает написание программ, в которых необходимо производить выбор между небольшим числом возможных вариантов. Однако иногда в программе необходимо осуществить выбор одного варианта из множества возможных. Формально для этого можно воспользоваться конструкцией if else if … else. Однако во многих случаях оказывается более удобным применять оператор switch языка С++. Синтаксис данного оператора следующий:

switch(переменная)
{
case константа1:

Case константа2:

...
default:

Данный оператор последовательно проверяет на равенство переменной константам, стоящим после ключевого слова case. Если ни одна из констант не равна значению переменно, то выполняются операторы, находящиеся после слова default. Оператор switch имеет следующую особенность. Допустим, значение переменной равно значению константы1 и выполняются операторы, стоящие после первого ключевого слова case. После этого выполнение программы продолжится проверкой переменной на равенство константы2, что часто приводит к неоправданным затратам ресурсов ЭВМ. Во избежание такой ситуации следует использовать оператор break для перехода программы к следующему оператору после switch.

На листинге 2.4 представлен пример программирования условного оператора switch.

Листинг 2.4. Пример использования оператора switch.

#include
int main()
{
int x;
printf(“Введите число: ”);
scanf(“%d”,&x);
switch(x)
{
case 1: printf(“Введено число 1\n”);break;
case 2: printf(“Введено число 2\n”); break;
default: printf(“Введено другое число\n”);
}
char ch;
printf(“Введите символ: ”);
scanf(“%с”,&сh);
switch(ch)
{
case ‘a’ : printf(“Введен символ а\n”); break;
case ‘b’ : printf(“Введен символ b\n”); break;
default: printf(“Введен другой символ\n”);
}
return 0;
}

Данный пример демонстрирует два разных варианта использования оператора switch. В первом случае выполняется анализ введенной цифры, во втором – анализ введенного символа. Следует отметить, что данный оператор может производить выбор только на основании равенства своего аргумента одному из перечисленных значений case, т.е. проверка выражений типа x

Условные переменные

Условная переменная представляет собой семафор, используемый для сигнализации о событии, которое произошло. Сигнала о том, что произошло некоторое событие, может ожидать один или несколько процессов (или потоков) от других процессов или потоков. Следует понимать различие между условными переменными и рассмотренными выше мьютексными семафорами. Назначение мьютексного семафора и блокировок чтения-записи - синхронизировать доступ к данным, в то время как условные переменные обычно используются для синхронизации последовательности операций. По этому поводу в своей книге UNIX Network Programming прекрасно высказался Ричард Стивенс (W. Richard Stevens): « Мьютексы нужно использовать для блокирования, а не для ожидания ».

В листинге 4.6 поток-«потребитель» содержал цикл:

15 while(TextFiles.empty())

Поток-«потребитель» выполнял итерации цикла до тех пор, пока в очереди TextFiles были элементы. Этот цикл можно заменить условной пере м енной. Поток-«изготовитель» сигналом уведомляет потребителя о том, что в очередь помещены элементы. Поток-«потребитель» может ожидать до тех пор, пока не получит сигнал, а затем перейдет к обработке очереди.

Условная переменная имеет тип pthread_cond_t. Ниже перечислены типы операций, которые может она выполнять:

Инициализация;

Разрушение;

Ожидание;

Ожидание с ограничением по времени;

Адресная сигнализация;

Всеобщая сигнализация;

Операции инициализации и разрушения выполняются условными переменными подобно аналогичным операциям других мьютексов. Функции класса pthread_cond_t, которые реализуют эти операции, перечислены в табл. 5.7.

Таблица 5.7. Функции класса pthread_cond_t, которые реализуют операции условных переменных

Условные переменные используются совместно с мьютексами. При попытке заблокировать мьютекс поток или процесс будет заблокирован до тех пор, пока мьютекс не освободится. После разблокирования поток или процесс получит мьютекс и продолжит свою работу. При использовании условной переменной ее необходимо связать с мьютексом.

pthread_mutex_lock(&Mutex) ;

pthread_cond_wait(&EventMutex, &Mutex);

pthread_mutex_unlock(&Mutex) ;

Итак, некоторая задача делает попытку заблокировать мьютекс. Если мьютекс уже заблокирован, то эта задача блокируется. После разблокирования задача освободит мьютекс Mutex и при этом будет ожидать сигнала для условной переменной EventMutex . Если мьютекс не заблокирован, задача будет ожидать сигнала неограниченно долго. При ожидании с ограничением по времени задача будет ожидать сигнала в течение заданного интервала времени. Если это время истечет до получения задачей сигнала, функция возвратит код ошибки. Затем задача вновь затребует мьютекс.

Выполняя адресную сигнализацию, задача уведомляет другой поток или процесс о том, что произошло некоторое событие. Если задача ожидает сигнала для заданной условной переменной, эта задача будет разблокирована и получит мьютекс. Если сразу несколько задач ожидают сигнала для заданной условной переменной, то разблокирована будет только одна из них. Остальные задачи будут ожидать в очереди, и их разблокирование будет происходить в соответствии с используемой стратегией планирования. При выполнении операции всеобщей сигнализации уведомление получат все задачи, ожидающие сигнала для заданной условной переменной. При разблокировании нескольких задач они будут состязаться за право владения мьютексом в соответствии с используемой стратегией планирования. В отличие от операции ожидания, задача, выполняющая операцию сигнализации, не предъявляет прав на владение мьютексом, хотя это и следовало бы сделать.

Условная переменная также имеет атрибутный объект, функции которого перечислены в табл. 5.8.

Таблица 5.8. Функции доступа к атрибутному объекту для условной переменной типа pthread_cond_t


Int pthread_condattr_init (pthread_condattr_t * attr) Инициализирует атрибутный объект условной переменной, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определенных реализацией;

Int pthread_condattr_destroy (pthread_condattr_t * attr) ; Разрушает атрибутный объект условной переменной, заданный параметром attr. Этот объект можно инициализировать повторно, вы-звав функцию pthread_condattr_init ()

Int pthread_condattr_setpshared (pthread_condattr_t * attr,int pshared);

Int pthread_condattr_getpshared (const pthread_condattr_t * restrict attr, int *restrict pshared); Устанавливает или возвращает атрибут process-shared атрибутного объекта условной переменной, заданного параметром attr. Параметр pshared может содержать следующие значения:

PTHREAD_PROCESS_SHARED (разрешает блокировку чтения-записи, разделяемую любыми потоками, которые имеют доступ к памяти, выделенной для этой условной переменной, даже если потоки принадлежат различным процессам);

PTHREAD_PROCESS_PRIVATE (Условная Переменная разделяется между потоками одного процесса)

Int pthread_condattr_setclock (pthread_condattr_t * attr, clockid_t clock_id);

Int pthread_condattr_getclock (const pthread_condattr_t * restrict attr, clockid_t * restrict clock_id); Устанавливает или возвращает атрибут clock атрибутного объекта условной переменной, заданного параметром attr . Атрибут clock представляет собой идентификатор часов, используемых для измерения лимита времени в функции pthread_cond_timedwait (). По умолчанию для атрибута clock используется идентификатор системных часов

Условные переменные

Условная переменная (condvar - сокращение от condition variable) используется для блокировки потока по какому-либо условию во время выполнения критической секции кода. Условие может быть сколь угодно сложным и не зависит от условной переменной. Однако условная переменная всегда должна использоваться совместно с мьютексом для проверки условия.

Условные переменные поддерживают следующие функции:

· ожидание условной переменной (wait) (pthread_cond_wait() );

· единичная разблокировка потока (signal) (pthread_cond_signal() )

· множественная разблокировка потока (broadcast) (pthread_cond_broadcast() ),

Приведем пример типичного использования условной переменной:

pthread_mutex_lock (&m) ;-…

while (!arbitrary condition) {

pthread_cond_wait(&cv, &m);

pthread_mutex_unlock (&m) ;

В этом примере захват мьютекса происходит до проверки условия. Таким образом, проверяемое условие применяется только к текущему потоку. Пока данное условие является истинным, эта секция кода блокируется на вызове ожидания до тех пор, пока какой-либо другой поток не выполнит операцию единичной или множественной разблокировки потока по условной переменной.

Цикл while в приведенном примере требуется по двум причинам. Во-первых, стандарты posix не гарантируют отсутствие ложных пробуждений (например, в многопроцессорных системах). Во-вторых, если другой поток изменяет условие, необходимо заново выполнить его проверку, чтобы убедиться, что изменение соответствует принятым критериям. При блокировании ожидающего потока связанный с условной переменной мутекс атомарно освобождается функцией pthread_cond_wait() для того, чтобы другой поток мог войти в критическую секцию программного кода.

Поток, который выполняет единичную разблокировку потока, разблокирует поток с наивысшим приоритетом, который стоит в очереди на условной переменной. Операция множественной разблокировки потока разблокирует все потоки, стоящие в очереди на условной переменной. Связанный с условной переменной мутекс освобождается атомарно разблокированным потоком с наивысшим приоритетом. После обработки критической секции кода этот поток должен освободить мутекс.

Другой вид операции ожидания условной переменной (pthread__cond_timedwair() ) позволяет установить таймаут. По окончании этого периода ожидающий поток может быть разблокирован.

Барьеры

Барьер - это механизм синхронизации, который позволяет скоординировать работу нескольких взаимодействующих потоков таким образом, чтобы каждый из них остановился в заданной точке в ожидании остальных потоков, прежде чем продолжить свою работу.

В отличие от функции pthreadjoin() , при которой поток ожидает завершения другого потока, барьер заставляет потоки встретиться в определенной точке. После того как заданное количество потоков достигает установленного барьера, все эти потоки разблокируются и продолжат свою работу. Барьер создается с помощью функции pthread_barrier_init():

#include

pthread_barrier_init (pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count);

В результате выполнения этого кода создается барьер по заданному адресу (указатель на барьер находится в аргументе barrier) и с атрибутами, установ­ленными аргументом attr. Аргумент count задает количество потоков, кото­рые должны вызвать функцию pthread_barrier_wait().

После создания барьера каждый поток вызывает функцию pthread_barrier_wait (), тем самым сообщая о завершения этого действия:

#include

int pthread_barrier_wait (pthread_barrier_t "barrier};

Когда поток вызывает функцию pthread_barrier_wait(), он блокируется до тех пор, пока то число потоков, которое было задано функцией pthread_barrier_init() , не вызовет функцию pthread_jbarrier_wait() и, соответст­венно, не заблокируется. После того как заданное количество потоков вы­зовет функцию pthread_barrier_wait() , все они разблокируются одновременно .

#include

#include

#include

#include

pthread_barrier_t barrier; // объект синхронизации типа "барьер"

main () // игнорировать аргументы

time_t now;// создать барьер со значением счетчика 3

pthread_barrier_init (&barrier, NULL, 3); // стартовать два потока - threadl и thread2

pthread_create (NOLL, NOLL, threadl, NULL); // потоки threadl и thread2 выполняются

pthread_create (NDLL, NDLL, thread2, NDLL);// ожидание завершения

printf ("main() waiting for barrier at %s", ctime (&now));

pthread_barrier_wait (&barrier);// после этого момента все три потока завершены

printf ("barrier in mainO done at %s", ctime (&now));

threadl (void *not used)

time (&now); // выполнение вычислений

printf ("threadl starting at %s", ctime (&now));// пауза

pthread_barrier_wait (&barrier) ;// после этого момента все три потока завершены

printf ("barrier in threadl{) done at %s", ctime (&now)) ;

thread2 (void *not__used)

time (&now) ; // выполнение вычислений

printf ("thread2 starting at %s", ctime (&now));// пауза

pthread_barrier_wait (&barrier) ;

// после этого момента все три потока завершены

printf {"barrier in thread2() done at %s", ctime (&now));

В примере из листинга основной поток создает барьер, после запуска которого начинается подсчет количества потоков, заблокированных на барьере для синхронизации. В данном случае количество синхронизируемых потоков задается равным 3: поток main(), поток thread1() и поток thread2().

Запускаются потоки thread1() и thread2(). Для наглядности в потоке задается пауза, чтобы имитировать процесс вычислений. Для выполнения синхрони­зации основной поток блокируется на барьере и ожидает разблокировки, которая произойдет после того, как остальные два потока не присоединятся к нему на этом барьере.



Ждущие блокировки

Ждущие блокировки (sleepon locks) работают аналогично условным пере­менным, за исключением некоторых деталей. Как и условные переменные, ждущие блокировки (pthread_sleepon_lock() ) могут использоваться для бло­кировки потока до тех пор, пока условие не станет истинным (аналогично изменению значения ячейки памяти). Но в отличие от условных перемен­ных (которые должны существовать для каждого проверяемого условия), ждущие блокировки применяются к одному мm.тексу и динамически созда­ваемой условной переменной независимо от количества проверяемых усло­вий. Максимальное число условных переменных в конечном итоге равно максимальному числу блокированных потоков.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: