Критические разделяемые ресурсы.

Версия 2.1.0

© Copyright 2006 - 2008
Грибов Игорь,
e-mail: gribov@depni.sinp.msu.ru

Оглавление.

Изменения в версиях.
Критические ресурсы.
Семафоры.
Пример использования критических ресурсов.
Не атомарные семафоры.
Пример с не атомарными семафорами.

Изменения в версиях.

Полная версия раздела задается в виде: версия.подверсия.выпуск.

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

Версия 1.1.0. В главе «Пример использования критических ресурсов», в функции write_data(dt), показана важность атомарного поведения семафоров.

Версия 1.2.0. В главу «Семафоры» добавлен абзац о полной семафорной операции в многопоточной среде.

Версия 2.0.0. Добавлены главы «не атомарные семафоры» и «пример с не атомарными семафорами».

Версия 2.1.0. Внесены изменения в код и описание функции semaphore_example() в главе «Семафоры» .


Критические ресурсы.

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

Определим два уровня собственной сигналобезопасности программ.

  1. Безусловная сигналобезопасность. Обеспечивается повторно-входимыми программными компонентами, обращение к которым может происходить в любой момент времени без каких-либо конфликтов. Такие программы должны выполняться независимо от любой другой программы, включая самою себя. Это достигается несколькими способами:

  1. Транзакционная сигналобезопасность. Программные компоненты с транзакционной сигналобезопасностью могут вызываться в любой момент времени, но без повторной входимости. То есть выполнение программы должно полностью завершаться до ее очередного вызова. Транзакционно безопасная программа зависит только от самой себя, но независима от любой другой программы. Например, операции чтения и записи кольцевого буфера являются безусловно сигналобезопасными друг относительно друга, поскольку манипуляции головой и хвостом буфера независимы. Чтение буфера может быть прервано операцией записи и наоборот. Однако, ни чтение, ни запись сами по себе не являются повторно входимыми и должны рассматриваться как критические секции. См. Кольцевой буфер (FIFO).

Если программные компоненты зависят как от самих себя, так и от других программ, они не являются сигналобезопасными. Например, операции чтения и записи разделяемой памяти не являются сигналобезопасными: их наложение и на самих себя и друг на друга может привести к искажениям данных. Такие программы следует рассматривать как критические разделяемые ресурсы и применять внешние методы их защиты: семафоры или взаимные исключения (mutexes).


Семафоры.

Семафоры были введены в эксплуатацию Эдсжером Дейкстрой еще в 1965 году и с тех пор являются базовым средством управления доступом к критическим ресурсам. Алгоритм использования семафоров выглядит так:




Следует особо отметить, что операции занятия и освобождения ресурса (P и V соответственно) должны быть атомарными, то есть всегда выполняться до конца, не прерываясь другими событиями и операторами программы. В практическом программировании P операция может быть реализована атомарным инкрементом или декрементом некоторой переменной, а V – соответственно ее атомарным декрементом либо инкрементом. Для однопроцессорных систем атомарность может быть гарантирована запретом всех прерываний процессора; для SMP (симметричных мультипроцессорных) систем используются специальные операции, обеспечивающие защиту ресурса от доступа со стороны всех процессоров системы. Подчеркнем - нет никаких гарантий того, что собственно инкрементно-декрементные операторы, записанные в на языке высокого уровня, действительно транслируются в код, исполняемый процессором атомарно. Прояснение этого вопроса требует особого изучения и внимания: анализа ассемблерного кода транслятора и даже системы команд процессора. В качестве альтернативы можно ограничиться однопоточной программной средой и согласиться с тем, что последовательность даже корректного доступа к разделяемым ресурсам может нарушаться. К счастью, для многих практических приложений оба эти условия вполне приемлемы.

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




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

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

Если же программная среда является многопоточной, когда последовательность выполнения программ определяется “внешней силой” (диспетчером), способной приостановить выполнение одного потока и запустить исполнение другого, последствия могут стать просто катастрофическими. Предположим, что некоторый программный поток приступил к выполнению критической секции, но диспетчер решил приостановить работу этого потока именно точке А или Б (см. рисунок). И - надо же такому случиться - переключился на выполнение другого потока, где производится вызов той же самой критической секции. А в этом втором потоке как раз при выполнении операторов критической секции (доступ к ней еще не был блокирован) диспетчер вдруг решает вернуться к продолжению первого потока. Завершив выполнение не атомарной семафорной операции, итогом которой будет вывод о том, что критическая секция свободна, диспетчер вновь приступит к выполнению ее кода. Поведение системы становится непредсказуемым: под управлением диспетчера выполняются различные сегмены кода одной и той же критической секции. Еще одним следствием не атомарности семафоров в многопоточной среде может стать постоянное блокирование доступа к ресурсу вследствие несогласованности итоговых значений семафорной переменной в налагающихся друг на друга P и V операциях.

Несмотря на всю важность вышеописанных проблем, в дальнейшем рассмотрении и примерах будем полагать, что операции инкремента и декремента семафора выполняются атомарно. Итак, операция занятия ресурса будет производиться инкрементом переменной-семафора: sem++, а его освобождения – декрементом: sem--. В открытом состоянии семафор имеет значение минус 1, а доступ к ресурсу осуществляется только при нулевом значении семафора. Вот как семафорные операции будут выглядеть в примерах программ:

typedef short int16;

int16 sem;

void semaphore_example(void)         // se_0
{
   sem++;                            // se_2
   if (sem == 0) {                   // se_3
//    Работа с критическим ресурсом
   } else if (sem == 1) {            // se_5
      write_status(RESOURCE_BUSY, "semaphore_example()");
   }
   sem--;                            // se_8
}

void open_semaphore(void)
{
   sem = -1;   // Семафор открыт
}

В приведенном примере функция semaphore_example() является сигналобезопасной, то есть может вызываться любым событием в системе. Переменная sem обеспечивает исключительный доступ к операторам разделяемого ресурса. Кроме того, регистрация статуса занятости ресурса write_status(...) позволяет зафиксировать сам факт наложения вызова semaphore_example(). Конечно, функция регистрации также должна быть реализована сигналобезопасно. Дополнительная проверка значения семафора [se_5] позволяет избежать излишних повторных вызовов функции регистрации и снизить общую нагрузку на систему. Это особенно важно, когда semaphore_example() является периодической функцией, например, обработчиком прерывания таймера, а в системе происходят серьезные сбои, способные поглотить все ресурсы (буфера, возможные тайм-ауты и т.п) функции write_status(...).

Обратите внимание, что попытка захвата ресурса [se_2] должна обязательно предшествовать проверке его занятости [se_3]. В противном случае, если проверка занятости осуществляется до попытки захвата, между этими операторами возникает интервал, аналогичный точке A или Б на приведенном выше рисунке со всеми указанными последствиями. Для того, чтобы избежать подобных недоразумений во всех программах семафорная инкрементная операция будет записываться отдельным операндом, а не внедряться в качестве пре-инкрементной в оператор проверки условия занятости ресурса. См. также Против микро-оптимизации.

В многопоточной среде атомарной должна являться полная семафорная операция, включающая в себя как попытку захвата ресурса [se_2], так и проверку его занятости [se_3] и, возможно, последующего освобождения [se_8]. В противном случае могут возникать ситуации, когда свободный ресурс не достанется ни одному потоку. Допустим, что один из них выполнил атомарную инкрементную операцию [se_2], но не успел проверить условие [se_3], оказавшись вытесненным другим потоком, также желающим получить доступ к критическому ресурсу. А этот второй поток, завершив операцию [se_2] и убедившись в занятости семафора [se_3], вынужден был до его освобождения [se_8] вновь уступить ресурс первому потоку. Проверив условие [se_3], первый поток также принимает решение о занятости вполне свободного ресурса. И хотя такие случаи обычно не приводят к последствиям катастрофического характера, подобного рода проблем лучше избегать. Особенно важным это становится в многопроцессорных или многоядерных системах, когда борьба за критические ресурсы существенно обостряется и вероятность проявления подобных ситуаций оказывается вполне значимой.

Отметим, что в силу локально-исторической традиции операция занятия ресурса производится инкрементом семафора, а его освобождение – декрементом. В открытом состоянии значение семафора отрицательно. Такие манипуляции противофазны стандарту POSIX.1B и оригинальному описанию Э. Дейкстры. Однако, это не должно вызывать ощутимых проблем, поскольку все семафорные операции записываются в виде явных манипуляций с переменной, а не путем обращения к семафорным функциям (sem_wait(...), sem_post(...) и др.).


Пример использования критических ресурсов.




Предположим, что нужно обеспечить последовательный вывод асинхронно возникающих данных (события 1, 2, 3, 4 на рисунке). Поскольку канал вывода может единовременно принять только один набор данных, необходимо их промежуточное хранение. Значит, нужно реализовать некоторый буфер, причем таким образом, чтобы процесс записи данных обладал свойством повторной входимости, то есть обеспечивал обслуживание налагающихся друг на друга требований записи (события 2 и 3 на рисунке). Покажем на примере программы возможную организацию такого буфера и алгоритмы сигналобезопасного вывода данных.

#define CACHE_SIZE  4   // Размер сигналобезопасного буфера
#define ERROR       -1  // Код ошибки
#define OK          1   // Код нормального завершения

typedef char            int8;
typedef unsigned char   unsigned8;
typedef short           int16;
typedef unsigned short  unsigned16;
typedef int             int32;
typedef unsigned int    unsigned32;

typedef struct {
   unsigned8 data_1;
   unsigned16 data_2;
   int32 data_3;
} data;   // Структура выводимых данных

typedef struct {
   int16 busy;
   data dt;
} cachedata;   // Структура данных буфера – дополнена семафором busy

cachedata cache[CACHE_SIZE];   // Буфер: небольшой массив структур данных
int16 sem_send;                // Семафор отправки данных

int16 data_out(data *dt)
{
   return OK;
   return ERROR;
}

Функция отправки data_out(dt)осуществляет непосредственный вывод данных в гнездо, регистр, порт и т.п. Эта функция должна поддерживать лишь транзакционную сигналобезопасность и возвращать код успешного завершения OK либо ошибки ERROR.

void send_data(void)   // sd_0
{
   unsigned16 cnt;

   sem_send++;   // sd_4
   if (sem_send == 0) {
      for (cnt = 0; cnt < CACHE_SIZE; cnt++) {      // sd_6
         if (cache[cnt].busy >= 0) {                // sd_7 
            if (data_out(&cache[cnt].dt) == OK) {   // sd_8
               cache[cnt].busy = -1;                // sd_9
            } else {
               break;                               // sd_11
            }
         }
      }
   }
   sem_send--;   // sd_16
}

Функция send_data()осуществляет вывод данных из буфера. Поскольку она работает с разделяемыми ресурсами (вывод данных [sd_8] и освобождение буфера [sd_9]), повторно-входимое обращение к циклу вывода [sd_6] не допустимо. Безусловная сигналобезопасность функции поддерживается блокирующим семафором sem_send [sd_4], [sd_16], который также используется для закрытия доступа к буферу на время выполнения критической секции записи данных в write_data(dt). При выводе данных производится линейный просмотр буфера (цикл [sd_6]) и при обнаружении заполненного элемента [sd_7] предпринимается попытка отправки данных [sd_8]. Если она завершается успешно, соответствующий элемент буфера освобождается [sd_9]. При неудачной отправке данных вывод остальных элементов не производится [sd_11]. В качестве флага заполненности буфера используется семафор сигналобезопасного доступа к соответствующему элементу. При освобождении буфера этот семафор устанавливается в открытое состояние явно – операцией присваивания [sd_9]. Такой алгоритм удобен, когда открытие семафора осуществляется отдельной функцией, причем возможно повторно-входимое обращение к этой операции, способное привести к постоянному блокированию ресурса (значение семафора становится меньше минус 1). Повышается и прозрачность кода, хотя в данном примере для открытия семафора вполне можно использовать штатную декрементную операцию.

int16 write_data(data *dt)  // wd_0
{
   unsigned16 cnt;

   sem_send++;                      // wd_4
   for (cnt = 0; cnt < CACHE_SIZE; cnt++) {   // wd_5
      cache[cnt].busy++;            // wd_6
      if (cache[cnt].busy == 0) {   // wd_7
         cache[cnt].dt = *dt;       // wd_8
         sem_send--;                // wd_9 
         send_data();               // wd_10
         return OK;                 // wd_11
      }
      cache[cnt].busy--;            // wd_13
   }
   sem_send--;
   send_data();                     // wd_16
   return ERROR;                    // wd_17  
}

Повторно-входимая функция записи данных write_data(dt) производит заполнение буфера. Доступ к его элементам осуществляется сигналобезопасно с использованием семафора [wd_6], [wd_7], [wd_13]. Функция осуществляет линейный просмотр элементов буфера (цикл [wd_5]) и при обнаружении свободного элемента [wd_7] заполняет его данными [wd_8], оставляя семафор в закрытом состоянии. После заполнения элемента буфера осуществляется попытка вывода данных [wd_10] и возвращается код нормального завершения OK. Если в буфере не обнаружено ни одного свободного элемента, также производится вывод данных [wd_16], но возвращается код ошибки ERROR.

Особое внимание следует обратить на использование в функции write_data(dt) семафора блокирования вывода данных sem_send. Поскольку вызов функции вывода может осуществляться асинхронно по отношению к функции записи, нужно предотвратить некорректное использование разделяемых ресурсов. Так, после выполнения попытки захвата элемента буфера [wd_6] и до фактической записи данных [wd_8] этот элемент не может считаться готовым для вывода, несмотря на значение флага busy, соответствующее заполненному элементу. Кроме того, если вызов send_data() произойдет непосредственно до семафорной операции [wd_13], значение семафора станет меньше минус 1 и данный элемент будет постоянно блокирован.

Необходимо также отметить важность атомарного поведения семафора [wd_6] при попытке захвата свободного буфера данных. Предположим, что инкрементная операция [wd_6] выполняется не атомарно, например с размещением семафора в некотором регистре. Тогда, при возникновение повторной попытки захвата во время не завершенной семафорной операции будет получен доступ к соответствующему буферу и произведено его заполнение данными. Из-за предшествующего двухкратного инкремента семафора sem_send в [wd_4] этот буфер не будет освобожден при последующем выводе данных [wd_9], [wd_10]. А возврат к до-обработке не атомарной семафорной операции приведет еще раз к выполнению условия [wd_7] и занесению новых данных в тот же самый буфер. Таким образом, записанные ранее данные будут безвозвратно утеряны.

void periodic(void)
{
   send_data();
}

Поскольку любая отправка данных с помощью функции data_out(dt) может завершиться безуспешно, следует возобновлять попытки вывода путем периодического вызова функции send_data(). В противном случае уже записанные данные могут “зависнуть” в буфере на неопределенной время – до очередного обращения к функции write_data(dt). Поскольку send_data() является безусловно сигналобезопасной, ее вызовы могут производиться асинхронно, например, по сигналу или прерыванию таймера.

void init_io(void)   // in_0
{
   unsigned16 cnt;

   sem_send = 0;    // in_4
   for (cnt = 0; cnt < CACHE_SIZE; cnt++) cache[cnt].busy = -1;   // in_5
   sem_send = -1;   // in_6
}

Функция (пере)инициализации init_io() открывает семафоры буферов данных [in_5] и вывода данных [in_6]. На время переинициализации вывод данных блокируется семафором sem_send [in_4]. Вызов init_io() должен выполняться только извне по отношению к функциям записи и вывода данных, например, из монитора прикладной программы. Данные, записанные во время прохождения переинициализации могут быть утеряны.

Практическая и полнофункциональная - с поддержкой приоритетов сообщений - программа сигналобезопасного вывода данных приведена в разделе "Процесс пошел".


Не атомарные семафоры.

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




На рисунке приведен алгоритм двухуровневого семафора. Он предполагает использование обычного счетного семафора (возможно, не атомарного) и дополнительной логической переменной – флага, указывающего на занятость ресурса. Для примера считается, что занятие семафора производится за два этапа, хотя их может быть и больше. Когда один из вызовов алгоритма впервые полностью завершит установ не атомарного семафора (до этапа 2), последующие попытки захвата ресурса блокируются условным оператором (этап 3). Таким образом, именно этот вызов осуществит занятие ресурса, выполнив последовательно и непрерывно этапы 5..9. Если попытка захвата не атомарного семафора прерывается на этапе 1, при возврате к прерванному коду этот семафор окажется в открытом состоянии. Тогда повторный захват ресурса будет предотвращен флагом занятости (этап 6). Отметим, что алгоритм не должен осуществлять несвоевременного освобождения семафора при обнаружении занятости ресурса на этапе 6.

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

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


Пример с не атомарными семафорами.

Программа выполняет те же функции, что и пример использования критических ресурсов. Здесь, однако, допускается применение не атомарных семафорных операций в однопоточных приложениях. Кроме того, явно выделены критические секции семафоров, дающие возможность применять код и в многопоточной среде. Для этого используются макросы CRITICAL_BEGIN и CRITICAL_END. В контекстно последовательных программах, к которым относится большинство микроконтроллерных приложений, эти макросы могут оставаться пустыми.

#define CACHE_SIZE  4
#define ERROR       -1  // Код ошибки
#define OK          1   // Код нормального завершения

typedef char            int8;
typedef unsigned char   unsigned8;
typedef short           int16;
typedef unsigned short  unsigned16;
typedef int             int32;
typedef unsigned int    unsigned32;

#define CRITICAL_BEGIN
#define CRITICAL_END

Макросы для выделения критических секций семафоров позволяют использовать код в многопоточной среде. В контекстно последовательных программах могут оставаться пустыми. Некоторые операционные системы содержат библиотеки для организации атомарных семафоров и предоставляют соответствующий набор интерфейсов прикладного программирования (API). Использование таких библиотек может оказаться более эффективным по сравнению со “скобками” выделения критических секций. Для этого потребуется некоторая модификация программного кода которая, при понимании сущности семафорных операций, не должна оказаться слишком трудоемкой.

typedef struct {
   unsigned8 data_1;
   unsigned16 data_2;
   int32 data_3;
} data;   // Структура выводимых данных

typedef struct {
   int16 busy;
   unsigned8 capture;
   data dt;
} cachedata;   // Структура данных буфера –
               // дополнена семафором busy (возможно, не атомарным)
               // и флагом занятости ресурса capture

cachedata cache[CACHE_SIZE];   // Буфер: небольшой массив структур данных
int16 sem_send;                // Семафор отправки данных. Может быть не атомарным.

int16 data_out(data *dt)
{
   return OK;
   return ERROR;
}

void send_data_non_atomic(void)   // sdna_0
{
   unsigned16 cnt;

   CRITICAL_BEGIN         // sdna_4
   sem_send++;            // sdna_5
   if (sem_send == 0) {   // sdna_6
      CRITICAL_END        // sdna_7
      for (cnt = 0; cnt < CACHE_SIZE; cnt++) {      // sdna_8
         if (cache[cnt].busy >= 0) {
            if (data_out(&cache[cnt].dt) == OK) {
               cache[cnt].capture = 0;              // sdna_11
               cache[cnt].busy = -1;                // sdna_12
            } else {
               break;
            }
         }
      }
      CRITICAL_BEGIN      // sdna_18
   }
   sem_send--;            // sdna_20
   CRITICAL_END           // sdna_21
}

Функция send_data_non_atomic()осуществляет вывод данных из буфера. Сигналобезопасность функции поддерживается семафором sem_send [sdna_5], [sdna_6], [sdna_20]. Этот семафор также обеспечивает взаимное исключение операций освобождения буфера [sdna_11], [sdna_12] и его захвата в функции записи данных write_data_non_atomic(dt). В однопоточном приложении атомарность манипуляций с sem_send не обязательна, поскольку все операции вывода данных производятся в едином последовательно исполняемом программном цикле [sdna_8]. Для многопоточных приложений семафорные операции заключаются в “скобки” макросов критических секций CRITICAL_BEGIN и CRITICAL_END. Эти макросы обеспечивают атомарность полных семафорных операций, то есть попытку захвата семафора и проверку его занятости [sdna_5], [sdna_6] с возможным освобождением занятого семафора [sdna_20]. Таким образом, исключаются ситуации, когда свободный ресурс не достается ни одному потоку.

int16 write_data_non_atomic(data *dt)   // wdna_0
{
   unsigned16 cnt;

   CRITICAL_BEGIN
   sem_send++;                            // wdna_5
   CRITICAL_END
   for (cnt = 0; cnt < CACHE_SIZE; cnt++) {
      CRITICAL_BEGIN
      cache[cnt].busy++;                  // wdna_9
      if (cache[cnt].busy == 0) {         // wdna_10
         CRITICAL_END
         if (cache[cnt].capture == 0) {   // wdna_12
            cache[cnt].capture = 1;       // wdna_13
            cache[cnt].dt = *dt;
            CRITICAL_BEGIN
            sem_send--;                   // wdna_16
            CRITICAL_END
            send_data_non_atomic();
            return OK;
         }
      } else {
         cache[cnt].busy--;               // wdna_22
         CRITICAL_END
      }
   }
   CRITICAL_BEGIN
   sem_send--;                            // wdna_27
   CRITICAL_END
   send_data_non_atomic();
   return ERROR;
}

Повторно-входимая функция записи данных write_data_non_atomic(dt) производит заполнение буфера. Доступ к его элементам осуществляется по алгоритму главы не атомарные семафоры. Семафорная операция [wdna_9] соответствует этапам 0..2 (см. рисунок в главе не атомарные семафоры). Проверка состояния семафора [wdna_10], отвечает этапам 2, 3 и 5, а его возможное освобождение [wdna_22] этапам 3 и 4. Оператор [wdna_12] проверяет фактическую занятость ресурса (этапы 5, 6, 7), а [wdna_13] устанавливает флаг занятости. Семафор sem_send [wdna_5], [wdna_16], [wdna_27] обеспечивает взаимно исключающее выполнение операций захвата и освобождения буфера. В однопоточном приложении операции с этим семафором могут быть не атомарным. Для многопоточной версии семафорные операции заключаются в макросы критических секций CRITICAL_BEGIN и CRITICAL_END, которые обеспечивают как атомарность полных семафорных операций, так и согласованность итоговых значений семафорных переменных.

void periodic(void)
{
   send_data_non_atomic();
}       

void init_io(void)
{
   unsigned16 cnt;

   sem_send = 0;
   for (cnt = 0; cnt < CACHE_SIZE; cnt++) {
      cache[cnt].capture = 0;
      cache[cnt].busy = -1;
   }
   sem_send = -1;
}