P1
|
P2
|
for (i = 0; i < 1000; i++)
{
produce_data(&a); send(&a, 1, 2);
}
|
for (i = 0; i < 1000; i++)
{
receive(&a, 1, 1); consume_data(&a);
}
|
В этом фрагменте кода процесс P1 производит 1000 элементов данных, а процесс P2 потребляет их. Предположим, что процесс P2 исполняется существенно медленнее, чем процесс P1. Если в дополнительных буферах отправки и приема достаточно места для размещения всех производимых элементов данных, то оба процесса могут выполняться без проблем. Однако, если свободного пространства в буфере недостаточно, возникает ситуация переполнения буфера. В зависимости от реализации команды send(), она может либо вернуть ошибку вызвавшему процессу, либо заблокировать отправителя до тех пор, пока не будет вызвана соответствующая команда receive(), освобождающая место в буфере. В первом случае ответственность за обработку таких ошибочных ситуаций ложится на разработчика ПО, и в приложении должны быть реализованы дополнительные механизмы синхронизации между отправляющим и принимающим процессами. Во втором случае возможная блокировка отправляющего процесса может приводить к непредсказуемому снижению производительности системы. Поэтому хорошим правилом является написание программ с ограниченными требованиями к размеру буферов.
В то время как буферизация помогает избежать многих тупиковых ситуаций при исполнении процессов, написание кода, приводящего к взаимным блокировкам, все равно остается возможным. Дело в том, что как и в случае взаимодействия без буферизации, вызовы команды receive() по-прежнему являются блокирующими для сохранения семантики передачи сообщений. Поэтому фрагмент кода, представленный ниже,
приведет к взаимной блокировке, т.к. оба процесса будут ожидать поступления данных, но ни один из них не будет их передавать:
P1
|
P2
|
receive(&a, 1, 2);
send(&b, 1, 2);
|
receive(&a, 1, 1);
send(&b, 1, 1);
|
Стоит еще раз отметить, что в данном примере взаимная блокировка вызвана ожиданием завершения команд receive().
Тупиковые ситуации также могут возникать при использовании команды send(), блокирующей отправителя до вызова соответствующей команды receive() в случае если свободного пространства в буфере оказывается недостаточно. Еще раз рассмотрим пример обмена сообщениями между двумя процессами:
P1
|
P2
|
send(&a, 1, 2);
receive(&b, 1, 2);
|
send(&a, 1, 1);
receive(&b, 1, 1);
|
Команда send() вернет управление вызывающему процессу, когда передаваемые данные будут скопированы из памяти процесса-отправителя в дополнительный буфер отправки. Если хотя бы одно сообщение сможет быть скопировано в дополнительный буфер отправки, то представленный выше код выполнится успешно. Если же свободного пространства в обоих буферах окажется недостаточно, возникнет взаимная блокировка процессов. Как и в предыдущем случае блокирующей отправки/ получения без буферизации подобные ситуации циклического ожидания должны быть распознаны и разрешены.
Стоит отметить, что возникновение взаимных блокировок при использовании блокирующих примитивов взаимодействия – явление нередкое, и предотвращение подобного циклического ожидания требует аккуратности и внимательности от разработчиков ПО.
Неблокирующие операции отправки/ получения. При использовании блокирующих примитивов взаимодействия семантика операции передачи данных обеспечивается либо за счет простоя процессов при обмене данными, либо за счет использования дополнительных буферов. Инициируемый такими операциями переход процесса в следующее состояние после отправки или получения сообщения завершается до того, как процессу возвращается управление. Поэтому блокирующие операции send() и receive() можно считать атомарными.
В некоторых случаях ответственность за обеспечение семантики передачи данных можно переложить на разработчика ПО и, тем самым, обеспечить быстрое выполнение операций send() и receive() без дополнительных издержек. Для таких неблокирующих примитивов отправки/ получения возврат управления в исполняемый процесс осуществляется сразу после вызова соответствующей команды и, следовательно, до того, как выполнение семантики передачи данных может быть гарантировано, и до того, как процесс перейдет в состояние после отправки или получения сообщения. Другими словами, вызов команды send() и receive() лишь начинает соответствующую операцию. Поэтому разработчик должен внимательно следить за тем, чтобы его код не изменял отправляемые данные и не использовал принимаемые данные, до завершения команд send() и receive().
Неблокирующие операции отправки/ получения дополняются командами, позволяющими проверить признак их завершения. Сразу после вызова неблокирующей операции send() или receive() исполнение процесса будет безопасным, если оно не зависит от передаваемых или получаемых данных. Позже, процесс сможет проверить завершено ли выполнение неблокирующей операции отправки или приема, и при необходимости дождаться ее завершения. Таким образом, при правильном использовании неблокирующие примитивы позволяют скрыть издержки обмена данными за счет других вычислений.
Как показано на рис. 1.9 существуют механизмы неблокирующей отправки / получения как без буферизации, так и с использованием дополнительных буферов на стороне отправителя и / или на стороне получателя.
Рис. 1.9. Механизмы неблокирующей отправки/ получения; (а) без буферизации, (б) с использованием буферов.
В случае взаимодействия без буферизации, процесс-отправитель направляет получателю запрос на передачу данных и продолжает свою работу. Этот запрос будет ожидать от процесса-получателя готовности к приему данных и соответствующего вызова команды receive(), после чего начнется собственно передача данных. Взаимодействующие процессы смогут узнать об окончании сетевого обмена и, следовательно, о возможности работать с передаваемыми и принимаемыми данными без риска их повреждения с помощью команд, проверяющих признак завершения операций send() или receive(). Эта ситуация иллюстрируется рис. 1.9а.
В случае буферизованного взаимодействия, отправитель инициирует копирование передаваемых данных из своего буфера в дополнительный буфер отправки и сразу продолжает работать. При этом дальнейшая обработка передаваемых данных процессом-отправителем становится безопасной в момент завершения операции копирования. На стороне получателя команда receive() инициирует перемещение данных из дополнительного буфера приема в адресное пространство получателя и возвращает управление в вызвавший процесс. Стоит отметить, что возращение управления произойдет даже в том случае, если данные еще не поступили от отправителя в дополнительный буфер приема. При этом запрос от команды receive() будет ожидать поступления данных. Как видно из рис. 1.9б, использование механизма неблокирующей буферизованной отправки/ получения позволяет уменьшить время, в течение которого работа с передаваемыми и принимаемыми данными является небезопасной. Таким образом, блокирующие примитивы взаимодействия обеспечивают простоту программирования и гарантируют выполнение семантики передачи данных, в то время как неблокирующие операции применимы для увеличения производительности компьютерных систем за счет маскирования издержек обмена данными. Однако в последнем случае разработчикам ПО приходится следить за тем, чтобы приложение не обращалось к передаваемым и принимаемым данным до завершения операций отправки или приема. Поэтому большинство современных библиотек, реализующих операции обмена сообщениями, обычно
предоставляют как блокирующие, так и неблокирующие примитивы.
Do'stlaringiz bilan baham: |