Неблокирующие межпроцессные коммуникации


Содержание

Система обмена сообщениями ZeroMQ

Оригинал: «ZeroMQ» .
Автор: Martin Sústrik, перевод: Н.Ромоданов

24.8. Модель распараллеливания

Одним из требований для ØMQ было возможность использования многоядерных устройств; другими словами, чтобы можно масштабировать пропускную линейно с увеличением числа доступных ядер процессора.

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

Учитывая эти проблемы, мы решили перейти на другую модель. Цель состояла в том, чтобы полностью избежать блокировок и позволить каждому потоку работать на полной скорости. Взаимодействие между потоками было реализовано с помощью асинхронных сообщений (события), которые передаются между потоками. Это, как знают инсайдеры, является классической моделью актера (actor model).

Идея заключалась в том, чтобы запускать на каждом ядре процессора по одному рабочему потоку — наличие двух потоков, совместно использующих то же самое ядро, будет означать лишь большое количество переключений контекста без получения особых преимуществ. Каждый внутренний объект ØMQ, такой, как, скажем, движок TCP, будет тесно связан с конкретным рабочим потоком. Это, в свою очередь, означает, что нет никакой необходимости в критических секциях, взаимоисключаемых событиях (mutexes), семафорах и тому подобном. Кроме того, эти объекты ØMQ не будут перераспределяться между ядрами процессора, так что удастся избежать негативного влияния на производительность, связанного с загрязнением кэша (рис.24.7).

Рис.24.7: Несколько рабочих потоков

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

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

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

Усвоенный урок: Когда стремитесь к экстремальной производительности и масштабируемости, то рассмотрите модель актера; это чуть ли не единственная вариант в подобных случаях. Однако, если вы не пользуетесь специализированной системой, например, Erlang или самой ØMQ, вам придется написать и вручную отладить инфраструктуру большого объема. Кроме того, с самого начала подумайте о процедуре завершения работы системы. Это будет самая сложная часть кода, и если у вас нет четкого представления о том, как ее реализовать, вам, вероятно, следует в первую очередь пересмотреть использование модели актера.

24.9. Неблокирующие алгоритмы

В последнее время в моде стали неблокирующие алгоритмы. Это простые механизмы межпотокового взаимодействия, в которых не используются предоставляемые ядром примитивы синхронизации, такие как взаимоисключаемые события или семафоры; они предпочитают выполнять синхронизацию с использованием атомарных операций процессора, таких как атомное сравнение и своп (CAS). Следует иметь в виду, что они не в буквальном смысле работают без блокировок — в действительности блокировки происходят за кулисами на аппаратном уровне.

ØMQ использует неблокирующую очередь в конвейерных объектах для передачи сообщений между потоками пользователя и рабочими потоками ØMQ. Есть два интересных аспекта, касающихся того, как ØMQ использует неблокирующую очередь.

Во-первых, в каждой очередь есть ровно один поток, который осуществляется запись, и ровно один поток, который осуществляет чтение. Если необходима связь типа «1-N», то создается несколько очередей (рис.24.8). Благодаря такой организации очереди не надо беспокоиться о о синхронизации записи (есть только один поток, осуществляющий запись) или чтения (есть только один поток, осуществляющий чтение), причем очередь может быть реализована очень эффективным способом.

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

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

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

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

Рис.24.9: Неблокирующая очередь

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

Неблокирующие межпроцессные коммуникации

3.3. оЕВМПЛЙТХАЭЙЕ ЛПННХОЙЛБГЙПООЩЕ ПРЕТБГЙЙ

йУРПМШЪПЧБОЙЕ ОЕВМПЛЙТХАЭЙИ ЛПННХОЙЛБГЙПООЩИ ПРЕТБГЙК РПЧЩЫБЕФ ВЕЪПРБУОПУФШ У ФПЮЛЙ ЪТЕОЙС ЧПЪОЙЛОПЧЕОЙС ФХРЙЛПЧЩИ УЙФХБГЙК, Б ФБЛЦЕ НПЦЕФ ХЧЕМЙЮЙФШ УЛПТПУФШ ТБВПФЩ РТПЗТБННЩ ЪБ УЮЕФ УПЧНЕЭЕОЙС ЧЩРПМОЕОЙС ЧЩЮЙУМЙФЕМШОЩИ Й ЛПННХОЙЛБГЙПООЩИ ПРЕТБГЙК. ьФЙ ЪБДБЮЙ ТЕЫБАФУС ТБЪДЕМЕОЙЕН ЛПННХОЙЛБГЙПООЩИ ПРЕТБГЙК ОБ ДЧЕ УФБДЙЙ: ЙОЙГЙЙТПЧБОЙЕ ПРЕТБГЙЙ Й РТПЧЕТЛХ ЪБЧЕТЫЕОЙС ПРЕТБГЙЙ.

оЕВМПЛЙТХАЭЙЕ ПРЕТБГЙЙ ЙУРПМШЪХАФ УРЕГЙБМШОЩК УЛТЩФЩК (opaque) ПВЯЕЛФ «ЪБРТПУ ПВНЕОБ» (request) ДМС УЧСЪЙ НЕЦДХ ЖХОЛГЙСНЙ ПВНЕОБ Й ЖХОЛГЙСНЙ ПРТПУБ ЙИ ЪБЧЕТЫЕОЙС. дМС РТЙЛМБДОЩИ РТПЗТБНН ДПУФХР Л ЬФПНХ ПВЯЕЛФХ ЧПЪНПЦЕО ФПМШЛП ЮЕТЕЪ ЧЩЪПЧЩ MPI-ЖХОЛГЙК. еУМЙ ПРЕТБГЙС ПВНЕОБ ЪБЧЕТЫЕОБ, РПДРТПЗТБННБ РТПЧЕТЛЙ УОЙНБЕФ «ЪБРТПУ ПВНЕОБ», ХУФБОБЧМЙЧБС ЕЗП Ч ЪОБЮЕОЙЕ MPI_REQUEST_NULL. уОСФШ ЪБРТПУ ВЕЪ ПЦЙДБОЙС ЪБЧЕТЫЕОЙС ПРЕТБГЙЙ НПЦОП РПДРТПЗТБННПК MPI_Request_free.

жХОЛГЙС РЕТЕДБЮЙ УППВЭЕОЙС ВЕЪ ВМПЛЙТПЧЛЙ MPI_Isend

C: int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request) FORTRAN: MPI_ISEND(BUF, COUNT, DATATYPE, DEST, TAG, COMM, REQUEST, IERROR) BUF(*) INTEGER COUNT, DATATYPE, DEST, TAG, COMM, REQUEST, IERROR

IN buf — БДТЕУ ОБЮБМБ ТБУРПМПЦЕОЙС РЕТЕДБЧБЕНЩИ ДБООЩИ;
IN count — ЮЙУМП РПУЩМБЕНЩИ ЬМЕНЕОФПЧ;
IN datatype — ФЙР РПУЩМБЕНЩИ ЬМЕНЕОФПЧ;
IN dest — ОПНЕТ РТПГЕУУБ-РПМХЮБФЕМС;
IN tag — ЙДЕОФЙЖЙЛБФПТ УППВЭЕОЙС;
IN comm — ЛПННХОЙЛБФПТ;
OUT request — «ЪБРТПУ ПВНЕОБ».

чПЪЧТБФ ЙЪ РПДРТПЗТБННЩ РТПЙУИПДЙФ ОЕНЕДМЕООП (immediate), ВЕЪ ПЦЙДБОЙС ПЛПОЮБОЙС РЕТЕДБЮЙ ДБООЩИ. ьФЙН ПВЯСУОСЕФУС РТЕЖЙЛУ I Ч ЙНЕОБИ ЖХОЛГЙК. рПЬФПНХ РЕТЕНЕООХА buf РПЧФПТОП ЙУРПМШЪПЧБФШ ОЕМШЪС ДП ФЕИ РПТ, РПЛБ ОЕ ВХДЕФ РПЗБЫЕО «ЪБРТПУ ПВНЕОБ». ьФП НПЦОП УДЕМБФШ У РПНПЭША РПДРТПЗТБНН MPI_Wait ЙМЙ MPI_Test, РЕТЕДБЧ ЙН РБТБНЕФТ request.

жХОЛГЙС РТЙЕНБ УППВЭЕОЙС ВЕЪ ВМПЛЙТПЧЛЙ MPI_Irecv

C: int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request) FORTRAN: MPI_IRECV(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM, REQUEST, IERROR) BUF(*) INTEGER COUNT, DATATYPE, SOURCE, TAG, COMM, REQUEST, IERROR

OUT buf — БДТЕУ ДМС РТЙОЙНБЕНЩИ ДБООЩИ;
IN count — НБЛУЙНБМШОПЕ ЮЙУМП РТЙОЙНБЕНЩИ ЬМЕНЕОФПЧ;
IN datatype — ФЙР ЬМЕНЕОФПЧ РТЙОЙНБЕНПЗП УППВЭЕОЙС;
IN source — ОПНЕТ РТПГЕУУБ-ПФРТБЧЙФЕМС;
IN tag — ЙДЕОФЙЖЙЛБФПТ УППВЭЕОЙС;
IN comm — ЛПННХОЙЛБФПТ;
OUT request — «ЪБРТПУ ПВНЕОБ».

чПЪЧТБФ ЙЪ РПДРТПЗТБННЩ РТПЙУИПДЙФ ОЕНЕДМЕООП, ВЕЪ ПЦЙДБОЙС ПЛПОЮБОЙС РТЙЕНБ ДБООЩИ. пРТЕДЕМЙФШ НПНЕОФ ПЛПОЮБОЙС РТЙЕНБ НПЦОП У РПНПЭША РПДРТПЗТБНН MPI_Wait ЙМЙ MPI_Test У УППФЧЕФУФЧХАЭЙН РБТБНЕФТПН request.

лБЛ Й Ч ВМПЛЙТХАЭЙИ ПРЕТБГЙСИ ЮБУФП ЧПЪОЙЛБЕФ ОЕПВИПДЙНПУФШ ПРТПУБ РБТБНЕФТПЧ РПМХЮЕООПЗП УППВЭЕОЙС ВЕЪ ЕЗП ЖБЛФЙЮЕУЛПЗП ЮФЕОЙС. ьФП ДЕМБЕФУС У РПНПЭША ЖХОЛГЙЙ MPI_Iprobe.

оЕВМПЛЙТХАЭБС ЖХОЛГЙС ЮФЕОЙС РБТБНЕФТПЧ РПМХЮЕООПЗП УППВЭЕОЙС MPI_Iprobe

C: int MPI_Iprobe (int source, int tag, MPI_Comm comm, int *flag, MPI_Status *status) FORTRAN: MPI_IPROBE (SOURCE, TAG, COMM, FLAG, STATUS, IERROR) LOGICAL FLAG INTEGER SOURCE, TAG, COMM, STATUS(MPI_STATUS_SIZE), IERROR

IN source — ОПНЕТ РТПГЕУУБ-ПФРТБЧЙФЕМС;
IN tag — ЙДЕОФЙЖЙЛБФПТ УППВЭЕОЙС;
IN comm — ЛПННХОЙЛБФПТ;
OUT flag — РТЙЪОБЛ ЪБЧЕТЫЕООПУФЙ ПРЕТБГЙЙ;
OUT status — БФТЙВХФЩ ПРТПЫЕООПЗП УППВЭЕОЙС.

еУМЙ flag=true, ФП ПРЕТБГЙС ЪБЧЕТЫЙМБУШ, Й Ч РЕТЕНЕООПК status ОБИПДСФУС БФТЙВХФЩ ЬФПЗП УППВЭЕОЙС.

чПУРПМШЪПЧБФШУС ТЕЪХМШФБФПН ОЕВМПЛЙТХАЭЕК ЛПННХОЙЛБГЙПООПК ПРЕТБГЙЙ ЙМЙ РПЧФПТОП ЙУРПМШЪПЧБФШ ЕЕ РБТБНЕФТЩ НПЦОП ФПМШЛП РПУМЕ ЕЕ РПМОПЗП ЪБЧЕТЫЕОЙС. йНЕЕФУС ДЧБ ФЙРБ ЖХОЛГЙК ЪБЧЕТЫЕОЙС ОЕВМПЛЙТХАЭЙИ ПРЕТБГЙК:

  1. пРЕТБГЙЙ ПЦЙДБОЙС ЪБЧЕТЫЕОЙС УЕНЕКУФЧБ WAIT ВМПЛЙТХАФ ТБВПФХ РТПГЕУУБ ДП РПМОПЗП ЪБЧЕТЫЕОЙС ПРЕТБГЙЙ.
  2. пРЕТБГЙЙ РТПЧЕТЛЙ ЪБЧЕТЫЕОЙС УЕНЕКУФЧБ TEST ЧПЪЧТБЭБАФ ЪОБЮЕОЙС TRUE ЙМЙ FALSE Ч ЪБЧЙУЙНПУФЙ ПФ ФПЗП, ЪБЧЕТЫЙМБУШ ПРЕТБГЙС ЙМЙ ОЕФ. пОЙ ОЕ ВМПЛЙТХАФ ТБВПФХ РТПГЕУУБ Й РПМЕЪОЩ ДМС РТЕДЧБТЙФЕМШОПЗП ПРТЕДЕМЕОЙС ЖБЛФБ ЪБЧЕТЫЕОЙС ПРЕТБГЙЙ.

жХОЛГЙС ПЦЙДБОЙС ЪБЧЕТЫЕОЙС ОЕВМПЛЙТХАЭЕК ПРЕТБГЙЙ MPI_Wait

C: int MPI_Wait(MPI_Request *request, MPI_Status *status) FORTRAN: MPI_WAIT(REQUEST, STATUS, IERROR) INTEGER REQUEST, STATUS(MPI_STATUS_SIZE), IERROR

INOUT request — «ЪБРТПУ ПВНЕОБ»;
OUT status — БФТЙВХФЩ УППВЭЕОЙС.

ьФП ОЕМПЛБМШОБС ВМПЛЙТХАЭБС ПРЕТБГЙС. чПЪЧТБФ РТПЙУИПДЙФ РПУМЕ ЪБЧЕТЫЕОЙС ПРЕТБГЙЙ, УЧСЪБООПК У ЪБРТПУПН request. ч РБТБНЕФТЕ status ЧПЪЧТБЭБЕФУС ЙОЖПТНБГЙС П ЪБЛПОЮЕООПК ПРЕТБГЙЙ.

жХОЛГЙС РТПЧЕТЛЙ ЪБЧЕТЫЕОЙС ОЕВМПЛЙТХАЭЕК ПРЕТБГЙЙ MPI_Test

C: int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status) FORTRAN: MPI_TEST(REQUEST, FLAG, STATUS, IERROR) LOGICAL FLAG INTEGER REQUEST, STATUS(MPI_STATUS_SIZE), IERROR

INOUT request «ЪБРТПУ ПВНЕОБ»;
OUT flag РТЙЪОБЛ ЪБЧЕТЫЕООПУФЙ РТПЧЕТСЕНПК ПРЕТБГЙЙ;
OUT status БФТЙВХФЩ УППВЭЕОЙС, ЕУМЙ ПРЕТБГЙС ЪБЧЕТЫЙМБУШ.

ьФП МПЛБМШОБС ОЕВМПЛЙТХАЭБС ПРЕТБГЙС. еУМЙ УЧСЪБООБС У ЪБРТПУПН request ПРЕТБГЙС ЪБЧЕТЫЕОБ, ЧПЪЧТБЭБЕФУС flag = true, Б status УПДЕТЦЙФ ЙОЖПТНБГЙА П ЪБЧЕТЫЕООПК ПРЕТБГЙЙ. еУМЙ РТПЧЕТСЕНБС ПРЕТБГЙС ОЕ ЪБЧЕТЫЕОБ, ЧПЪЧТБЭБЕФУС flag = false, Б ЪОБЮЕОЙЕ status Ч ЬФПН УМХЮБЕ ОЕ ПРТЕДЕМЕОП.

тБУУНПФТЙН РТЙНЕТ ЙУРПМШЪПЧБОЙС ОЕВМПЛЙТХАЭЙИ ПРЕТБГЙК Й ЖХОЛГЙЙ MPI_Wait.

жХОЛГЙС УОСФЙС ЪБРТПУБ ВЕЪ ПЦЙДБОЙС ЪБЧЕТЫЕОЙС ОЕВМПЛЙТХАЭЕК ПРЕТБГЙЙ MPI_Request_free

C: int MPI_Request_free(MPI_Request *request) FORTRAN: MPI_REQUEST_FREE(REQUEST, IERROR) INTEGER REQUEST, IERROR

INOUT request — ЪБРТПУ УЧСЪЙ.

рБТБНЕФТ request ХУФБОБЧМЙЧБЕФУС Ч ЪОБЮЕОЙЕ MPI_REQUEST_NULL. уЧСЪБООБС У ЬФЙН ЪБРТПУПН ПРЕТБГЙС ОЕ РТЕТЩЧБЕФУС, ПДОБЛП РТПЧЕТЙФШ ЕЕ ЪБЧЕТЫЕОЙЕ У РПНПЭША MPI_Wait ЙМЙ MPI_Test ХЦЕ ОЕМШЪС. дМС РТЕТЩЧБОЙС ЛПННХОЙЛБГЙПООПК ПРЕТБГЙЙ УМЕДХЕФ ЙУРПМШЪПЧБФШ ЖХОЛГЙА MPI_Cancel(MPI_Request *request).

ч MPI ЙНЕЕФУС ОБВПТ РПДРТПЗТБНН ДМС ПДОПЧТЕНЕООПК РТПЧЕТЛЙ ОБ ЪБЧЕТЫЕОЙЕ ОЕУЛПМШЛЙИ ПРЕТБГЙК. вЕЪ РПДТПВОПЗП ПВУХЦДЕОЙС РТЙЧЕДЕН ЙИ РЕТЕЮЕОШ (ФБВМЙГБ 3.3).

фБВМЙГБ 3.3. жХОЛГЙЙ ЛПММЕЛФЙЧОПЗП ЪБЧЕТЫЕОЙС ОЕВМПЛЙТХАЭЙИ ПРЕТБГЙК.

чЩРПМОСЕНБС РТПЧЕТЛБ жХОЛГЙЙ ПЦЙДБОЙС (ВМПЛЙТХАЭЙЕ) жХОЛГЙЙ РТПЧЕТЛЙ (ОЕВМПЛЙТХАЭЙЕ)
ъБЧЕТЫЙМЙУШ ЧУЕ ПРЕТБГЙЙ MPI_Waitall MPI_Testall
ъБЧЕТЫЙМБУШ РП ЛТБКОЕК НЕТЕ ПДОБ ПРЕТБГЙС MPI_Waitany MPI_Testany
ъБЧЕТЫЙМБУШ ПДОБ ЙЪ УРЙУЛБ РТПЧЕТСЕНЩИ MPI_Waitsome MPI_Testsome

лТПНЕ ФПЗП, MPI РПЪЧПМСЕФ ДМС ОЕВМПЛЙТХАЭЙИ ПРЕТБГЙК ЖПТНЙТПЧБФШ ГЕМЩЕ РБЛЕФЩ ЪБРТПУПЧ ОБ ЛПННХОЙЛБГЙПООЩЕ ПРЕТБГЙЙ MPI_Send_init Й MPI_Recv_init, ЛПФПТЩЕ ЪБРХУЛБАФУС ЖХОЛГЙСНЙ MPI_Start ЙМЙ MPI_Startall. рТПЧЕТЛБ ОБ ЪБЧЕТЫЕОЙЕ ЧЩРПМОЕОЙС РТПЙЪЧПДЙФУС ПВЩЮОЩНЙ УТЕДУФЧБНЙ У РПНПЭША ЖХОЛГЙК УЕНЕКУФЧБ WAIT Й TEST.

Межпроцессорное взаимодействие

Читайте также:

  1. N Взаимодействие кислорода с металлами переменной валентности
  2. Белок-белковое взаимодействие
  3. Биологические процессы в океане и их взаимодействие с гидрологическими условиями
  4. Взаимодействие a и b — частиц с веществом
  5. Взаимодействие HTML-страницы с WEB сервером
  6. Взаимодействие административного права с другим отраслями права.
  7. Взаимодействие в процессе программированного обучения
  8. Взаимодействие водорослей и животных
  9. Взаимодействие гос. и рыночного регулир-я экономики.
  10. Взаимодействие грибов с высшими растениями
  11. Взаимодействие грибов с животными.
  12. Взаимодействие двигательных навыков.

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

1.Сигнальные средства обмена информацией – взаимодействие процессов осуществляется путем передачи сигнала посредством специального системного вызова. Степень воздействия на поведение процесса, получившего информацию, минимальна. Процесс может посылать сигналы (табл. 3.4.) только членам своей группы, состоящей из родителя и предков или всей группе за один системный вызов. Сигналы используются и для других целей. Например, если процесс выполняет вычисления с плавающей точкой и непреднамеренно делит число на 0, он получает сигнал SIGFPE (Floating-Point Exception SIGnal – сигнал исключения при выполнении операции с плавающей точкой). Большинство операционных систем кроме сигналов, требуемых стандартом POSIX, используют дополнительные сигналы.

Сигналы, требуемые стандартом POSIX

№п/п Сигнал Причина
SIGABRT Посылается, чтобы прервать процесс и создать дамп памяти (memory dump) – снимок оперативной памяти
SIGALRM Истекло время будильника
SIGFPE Произошла ошибка при выполнении операции с плавающей точкой (например, деление на 0)
SIGHUP Модем повесил трубку на телефонной линии, использовавшейся процессом
SIGILL Пользователь нажал клавишу DEL, чтобы прервать процесс
SIGQUIT Пользователь нажал клавишу, требуя прекращения работы процесса с созданием дампа памяти
SIGKILL Посылается, чтобы уничтожить процесс (не может игнорироваться или перехватываться)
SIGPIPE Процесс пишет в канал, из которого никто не читает
SIGSEGV Процесс обратился к неверному адресу памяти
SIGTERM Вежливая просьба процессу завершить свою работу
SIGUSR1 Может быть определено приложением
SIGUSR2 Может быть определено приложением

Типы сигналов (принято задавать номерами в диапазоне от 1 до 31 включительно или специальными символьными обозначениями, которые начинаются с приставки SIG) и способы их возникновения в системе жестко регламентированы. Процесс может получить сигнал:

— от аппаратного обеспечения (hardware) при возникновении исключительной ситуации;

— от процесса, выполнившего системный вызов передачи сигнала;

— от операционной системы при наступлении некоторых событий;

— от терминала при нажатии определенной комбинации клавиш;

— от системы управления заданиями;

— при выполнении команды kill.

Например, в Linux существуют несколько причин генерации сигналов или ситуаций, в которых отправляются сигналы:

— терминальные прерывания – нажатие пользователем определенной комбинации клавиш вызывают отправку сигнала процессу, связанному с текущим терминалом;

— особые ситуации – при выполнении процесса возникает особая ситуация, например, деление на ноль и процесс получает от ядра соответствующий сигнал;

— межпроцессорное взаимодействие – процесс отправляет специальный сигнал другому процессу с помощью системного вызова kill;

— управление заданиями – командные интерпретаторы, поддерживающие управление заданиями, используют сигналы для манипулирования текущими и фоновыми заданиями;

— установка квот – процесс превышает установленную для него квоту на использования ресурсов и ему отправляется соответствующий сигнал;

— уведомления – процессам посылаются уведомления о готовности устройств или наступлении других событий;

— установка таймера – процессу будет послан сигнал, когда значение таймера станет равным нулю.

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

— принудительно проигнорировать сигнал;

— произвести обработку по умолчанию: проигнорировать, остановить процесс (перевести в состояние ожидания до получения другого специального сигнала), либо завершить работу;

— выполнить обработку сигнала, специфицированную пользователем.

Изменить реакцию процесса на сигнал можно с помощью специальных системных вызовов. Реакция на некоторые сигналы не допускает изменения, поэтому они могут быть обработаны только по умолчанию, например, сигнал SIGKILL обрабатывается только по умолчанию и приводит к завершению процесса. В операционной системе UNIX при системном вызове fork() все установленные реакции на сигналы наследуется порожденным процессом. При системном вызове exec() сохраняются реакции только для тех сигналов, которые игнорировались или обрабатывались по умолчанию. Получение любого сигнала, который до вызова exec() обрабатывался пользователем, приведет к завершению процесса.

2.Разделяемая память (shared memory) – взаимодействие процессов осуществляется посредством совместного использования некоторой области (сегмента) адресного пространства. Созданием разделяемой памяти занимается операционная система, а ее использование для передачи/получения информации осуществляется с помощью средств языков программирования. Проблема совместного обращения двух процессов к одним и тем же данным (синхронизация доступа) решается с помощью семафоров[3].

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

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

Например, в операционной системе Linux механизм разделяемых сегментов памяти обеспечивается четырьмя системными вызовами: shmget, shmctl, shmat, shmdt. Так для создания нового сегмента разделяемой памяти или подключения к существующему сегменту используется функция:

int shmget(key_t key, size_t size, int flag)

key – уникальный ключ, необходимый для создания идентификатора;

size – указывает требуемый размер сегмента в байтах;

flag – комбинация флагов доступа на чтение и запись.


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

void * shmat(int shmid, void *shmaddr, int flag),

shmid – идентификатор разделяемой памяти, возвращенный shmget;

shmaddr – определяет желаемый адрес привязки сегмента (в большинстве случаев система сама выбирает начальный адрес для вызвавшего процесса);

flag – комбинация флагов доступа на чтение и запись.

По умолчанию сегмент подключается для чтения и записи, но в аргументе flag можно указать константу shm_rdonly, которая позволит установить доступ только на чтение. После завершения работы с сегментом его следует отключить вызовом int shmdt(void *shmaddr), который получает в качестве аргумента адрес, возвращенный функцией shmat. Эта функция не удаляет сегмент разделяемой памяти, а только снимает его привязку к процессу.

Для выполнения операций с сегментом разделяемой памяти используется функция:

int shmctl(int shmid, int cmd, struct shmid_ds *buff)

cmd может принимать следующие значения:

IPC_RMID – удаление сегмента с идентификатором shmid;

IPC_SET – установка значений полей структуры shmid_ds;

IPC_STAT – возвращает вызывающему процессу (через аргумент buff) текущее значение структуры shmid_ds для указанного сегмента разделяемой памяти.

Разделяемые сегменты памяти в Unix/Linux не имеют внешних имен. При получении идентификатора сегмента процесс пользуется числовым ключом. Гарантированно уникальный сегмент можно создать с использованием ключа IPC_PRIVATE, но такой ключ не может быть внешним. Поэтому сегменты используются, как правило, родственными процессами, которые имеют возможность передавать друг другу их идентификаторы, например, через наследуемые ресурсы или через параметры вызова дочерней программы. Таким образом, для создания монитора разделяемой памяти из таблицы системных вызовов следует получить адрес обработчика sys_ipc(), перехватить его, а затем, зная, какой из вышеописанных вызовов совершает система, считать соответствующие данные.

3.Канальные средства обмена информацией – взаимодействие процессов осуществляется с помощью специальных системных вызовов через линии связи, предоставленные операционной системой. Объем передаваемой информации в единицу времени ограничивается пропускной способностью канала. Передача информации по линиям связи может потребовать первоначального резервирования линии для процессов, желающих обменяться информацией и для выбора способа адресации. Различают два способа адресации.

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

симметричная прямая адресация – передающий процесс и процесс, принимающий данные, указывают имена своих партнеров по взаимодействию (посторонний процесс не может вмешаться в процедуру общения двух процессов);

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

Способ непрямой адресации. Передающий процесс помещает данные в промежуточный объект хранения (буфер, имеющий адрес[4]), из которого данные могут быть изъяты другим процессом. При этом передающий процесс не знает об идентификаторе процесса, который получит информацию, а принимающий процесс не знает об идентификаторе процесса, который передал информацию.

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

Однонаправленная связь (симплексная связь) – связь, при которой каждый процесс, ассоциированный с ней, может использовать средство связи только для приема информации или только для ее передачи.

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

полудуплексная связь – двунаправленная связь с поочередной передачей информации в разных направлениях;

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

Для передачи данных по каналам связи используется две модели: поток ввода-вывода и сообщения.

1.Межпроцессное взаимодействие посредством потока ввода-вывода – передача данных через pipe (канал, трубу, конвейер), является одним из наиболее простых способов передачи информации между процессами по линиям связи. Pipe служит для организации однонаправленной или симплексной связи. Для использования одного канала в двух направлениях необходимы специальные средства синхронизации процессов. Более простой способ организации двунаправленной связи между родственными процессами заключается в использовании двух pipe.

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

Для организации потокового взаимодействия любых процессов в операционной системе применяется средство связи FIFO (First Input First Output) или именованный канал (Named pipe). FIFO подобен каналу, за одним исключением: данные о расположении FIFO в адресном пространстве ядра и его состоянии процессы могут получать не через родственные связи, а через файловую систему. Для этого при создании именованного канала на диске заводится файл специального типа, обращаясь к которому процессы могут получить интересующую их информацию. Для создания FIFO в операционной системе UNIX используется системный вызов mknod() или функция mkfifo(). В процессе работы они заводятся файл-метку, которая позволяет осуществить реальную организацию FIFO в памяти при его открытии с помощью системного вызова open().

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

В среде операционной системы Microsoft Windows NT каналы типа pipe используются для передачи данных между параллельно работающими процессами и позволяют организовать передачу данных между локальными процессами и между процессами, запущенными на рабочих станциях в сети. Через канал данные передаются только между двумя процессами. Один из процессов создает канал, другой открывает его. После этого оба процесса могут передавать данные через канал в одну или обе стороны, используя для этого функции, предназначенные для работы с файлами: ReadFile и WriteFile. Приложения могут выполнять над каналами Pipe синхронные или асинхронные операции, аналогично операциям, производимым с файлами. В случае использования асинхронных операций дополнительно реализуются механизм синхронизации процессов. Имена каналов в общем случае имеют следующий вид:

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

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

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

2.Межпроцессное взаимодействие путем передачи сообщения. Данный механизм используется в распределенных системах, где не существует памяти, непосредственно доступной процессам, работающим на разных компьютерах. Сообщение – это блок информации, отформатированный процессом-отправителем таким образом, чтобы он был понятен процессу-получателю. На основе механизма передачи сообщений работают все сетевые службы предоставляющие пользователям сети разнообразные услуги: доступ к удаленным файлам, принтерам, почтовым ящикам и пр. Путем сообщений реализуются запросы от клиентов к серверам, например запрос на просмотр содержимого каталога.

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

— адрес — набор символов, уникально определяющих процесс-отправитель и процесс-получатель;

— последовательный номер – номер, который является идентификатором сообщения и используется идентификации потерянных сообщений и дубликатов сообщений в случае отказов в сети;

— структурированная информация – информация, состоящая из поля типа данных, поля длины данных и поля значения данных.

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

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

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

| следующая лекция ==>
Потоки. Реализация мультипрограммирования | Механизмы синхронизации

Дата добавления: 2014-01-07 ; Просмотров: 1561 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

Неблокирующие межпроцессные коммуникации

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

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

Эти особенности учитываются в следующих принципах:

— узлы неизменяемого типа;

— специальные методы управления памятью.

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

Рис. 1.2 Блок-схема простого неблокирующего алгоритма

Принципы неблокирующих алгоритмов

Узлы неизменяемого типа

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

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

Эта парадигма — использование структур данных неизменяемого (immutable) типа. Неизменяемый тип — это такой тип структуры данных, когда данные, входящие в структуру заносятся в нее (изменяются в ней) только однократно — при ее создании. Изменить данные такой структуры непосредственно нельзя, но можно:

1) скопировать структуру;

2) в копии задать необходимые новые значения;

3) далее вместо исходной структуры использовать копию;

4) оригинал уничтожить.

Чтобы выполнение третьего пункта было возможным, необходимо применить следующий принцип — подмену указателей.

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

Итак, после получения копии узла структуры данных, который необходимо изменить, и произведения необходимых согласно алгоритму изменений данных узла, необходимо обеспечить подмену оригинального узла его копией. Это действие производится за счет подмены указателя на оригинальный узел на указатель на копию узла. Фактически, задача изменения произвольной динамической структуры данных (сложная) подменяется на задачу изменения лишь одного указателя на эту структуру (простую). Такая подмена задачи позволяет устранить сам промежуток времени, когда изменяемые данные могут быть не согласованы, — за счет использования атомарных операций, поскольку значение указателя — адрес области памяти — это обычное число, которое можно изменить с помощью единственной процессорной инструкции.

Атомарность означает неделимость операции. Это значит, что ни один поток не может увидеть промежуточное состояние операции, она либо выполняется, либо нет. Рассмотрим пример простой операции инкрементирования значения, описанный в работе [8]:

Даже такой простой код нужно синхронизировать в многопоточной среде. Если посмотреть ассемблерный код второй строки, то мы увидим, что она состоит из трех операций:

1 mov eax, dword ptr [x] ; загрузка текущего значения из памяти в регистр eax

2 add eax, 1 ; инкрементирование значения регистра eax

3 mov dword ptr [x], eax ; запись значения регистра eax обратно в память

Модификация встроенных C++ типов не является атомарной, то есть если два потока одновременно попытаются модифицировать переменную x, мы вполне можем получить ситуацию, где значение x станет 1 после двух инкрементов. Пример показан в таблице 1.1.

Таблица 1.1 Пример неверной модификации переменной х двумя потоками

Ситуации подобные этой, в которых финальный результат зависит от очередности выполнения, называется data race. Можно исправить этот код и сделать его потокобезопасным добавив блокировку перед инкрементом и разблокировку после него, тем самым обеспечивая атомарность операции инкрементирования, но существует другой способ.

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

В связи с этим, кроме атомарных операций чтения и записи, в число атомарных примитивов также должна входить такая операция, как сравнение с обменом или compare-and-swap (CAS), также известная, как compare exchange, или пара операций загрузка с пометкой/попытка записи или load linked/store conditional (LL/SC). Суть операции сравнение с обменом заключается в том, что она атомарно сравнивает значение одного объекта с другим и при удачном сравнении заменяет значение объекта.CAS принимает три аргумента: адрес области памяти, ожидаемое значение по этому адресу и вновь записываемое значение. Если и только если область памяти содержит ожидаемое значение, по этому адресу записывается новое значение. Операцией возвращается булево значение, определяющее произошла ли перезапись значения или нет. Иными словами, CAS(address, expected, new) атомарно выполняет следующую операцию (в псевдокоде):

Суть пары операций загрузка с пометкой/попытка записи аналогична сути операции CAS: LL атомарно сравнивает значение одного объекта с другим и при удачном сравнении SCзаменяет значение объекта.LL принимает один аргумент: адрес области памяти и возвращает ее содержимое. SC принимает два аргумента: адрес области памяти и новое значение. Если ни один другой поток не перезаписывал область памяти по адресу после того, как данный поток считал ее значение с помощью LL, только тогда по этому адресу записывается новое значение. Операция возвращает булево значение, определяющее произошла ли перезапись значения. Дополнительная инструкция validate (VL) принимает один аргумент: адрес области памяти, и возвращает булево значение, определяющее, происходила ли перезапись области памяти со стороны других потоков с того момента времени, как данный поток выполнил LL.

Большинство современных широко распространенных процессорных архитектур поддерживает либо CAS, либо пару LL/SC на выровненных однословных операндах. В некоторых 32-разрядных системах эти операции доступны и для двухсловных операндов (то есть доступна поддержка 64-разрядных инструкций), но на 64-битных архитектурах поддержки операций над двухсловными операндами нет (то есть 128-битные инструкции не поддерживаются). Пара LL/SC обычно используется в RISC-архитектурах (DEC Alpha, MIPS, PowerPC, ARM), тогда как на архитектурах x86 используется CAS в различных ее вариациях.

CAS легко выразить через пару LL/SC следующим образом:

Более подробное описание операций можно найти в работах [2,4].

Специальные методы управления памятью

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

Главную заботу при работе с неблокирующими структурами представляет проблема освобождения памяти, занимаемой удаляемыми узлами. В случае работы со структурой, основанной на блокировках, легко гарантировать, что когда некий поток удаляет узел из динамической структуры, никакой другой поток впоследствии не обратится к памяти этого узла, до тех пор, пока она не будет заново перераспределена. В результате, для удаляющего потока обычно является вполне безопасным освобождение памяти, занимаемой узлом (например, с помощью delete) для последующего произвольного ее выделения в другом потоке (например, с помощью new).

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

Есть несколько способов, которые применяются в различных системах с целью выполнения этой задачи:

— Метод использования специальных тэгов (счетчиков изменений) памяти, основывающийся на ассоциировании специального тега с каждым адресом памяти, который может быть использован в операции;

— Метод неблокирующего подсчета ссылок, основывающийся на включении в структуру динамического узла специального счётчика ссылок;

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

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


Также существует другая связанная с повторным использованием памяти проблема — это так называемая проблема ABA. Ее наличие влияет практически на все неблокирующие алгоритмы. Проблема эта обнаруживается тогда, когда поток читает значение A из некоей разделяемой области памяти, далее другой поток изменяет значение в этой области памяти на B, после чего снова на A. Позднее, когда первый поток проверяет, не изменилось ли значение, например, с помощью CAS, сравнение с сохраненным значением возвращает истину (то есть не изменилось). И поток продолжает выполнение далее, ошибочно считая, что значение не изменялось с момента первого чтения. В результате, поток может повредить структуру данных или вернуть неверный результат, поскольку другой поток мог произвести другие, скрытые изменения, которые первый поток просто не обнаружит.

Проблема ABA является фундаментальной, и должна быть устранена вне зависимости от способа обеспечения повторного использования памяти. Однако методы обеспечения повторного использования памяти (например, сборка мусора или garbage collection (GC)) часто предотвращают возникновение этой проблемы в качестве побочного эффекта, так сказать, без каких бы то ни было дополнительных затрат, но, к сожалению, в некоторых языках нет сборщика мусора.

Процесс коммуникации, его сбои и способы восстановления

Одной из самых сильных идей, изменивших мое мировосприятие, явилось убеждение в том, что абсолютной правды не существует. И тот цвет, который я называю сливовым, для другого человека сливовым не является, а будет фиолетовым или пурпурным. Часто люди, переживающие себя застрявшими в процессе коммуникации, просто смотрят на цифру «6» с разных сторон, и для одного из них она точно выглядит как «9». Поэтому важным аспектом эффективной коммуникации является проверка субъективности восприятия каждой из сторон.

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

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

Что первично, для начала коммуникации? Когда встреча уже состоялась, все условности выполнены, разговор ведется на одном языке, необходимо, чтоб у одного из общающихся родилось что-то, что ему важно донести до другого – интенция, ощущение, импульс, образы, потребность, необходимость, или же то, что было возбуждено в нем вторым собеседником. Далее они претерпевают определенные преобразования с помощью нашей второй сигнальной системы, которая отличает нас от высших млекопитающих. Работа системы — опредмечивать окружающий мир и внутренние ощущения с помощью понятий и слов.

Первый этап коммуникации – сбор образов, ощущений, формирование некоторой идеи, либо совокупности понятий, которые важно донести другому человеку.

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

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

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

На пятом этапе информация достигает мыслительно-аналитических центров головного мозга другого человека, и он начинает процесс раскодировки. Это происходит так: человек, который воспринимает коммуникацию, придает субъективный смысл феноменам, которые он запечатлевает в восприятии. Он формирует значимую для него смысловую целостность, которая учитывает все многогранные особенности воспринятого опыта коммуникации.

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

Для меня, завершением одного цикла коммуникации является зарождение обратной реакции реципиента послания, и шестой этап будет являться номером один обратного цикла коммуникации – процесс повторяется снова.

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

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

Интересно поразмышлять, какого рода сбои могут происходить на каждом из этапов процесса коммуникации?

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

На первом этапе главную роль играет нечувствительность к тому, что со мной происходит: я не слышу, как у меня бьется сердце, я не чувствую, что у меня холодеют руки. Зачастую, мы можем обладать либо селективным (касающимся определенных зон), либо полным отсутствием осознавания ощущений, импульсов и эмоций. Поэтому, когда человек говорит что-то, а потребности опознать не может, закодировать послание может быть сложно. Например, если сексуальность была с детства тотально табуирована и окружена токсическим стыдом, то человеку невозможно осознать свое возбуждение, сформулировать мысль, и обличить ее в слова. Тогда коммуникация «я хочу тебя» не имеет шансов появиться в любой форме. Либо мы можем чувствовать, что с нами происходит, у нас в процессе осознавания проявятся различные ощущения и импульсы, но мы не найдем тех соответствующих понятий, которыми можно закодировать послание. Пучит, подпирает, давит — а любовь ли это? При таком сбое тружно сложить смысловой паззл.

На втором этапе цензура работает вовсю. На пути успешной коммуникации часто стоит стыд и разные блокировки, связанные с этим переживанием. Насколько то, что я собираюсь сказать, вообще можно говорить? Могу ли я осмелиться выбрать именно этот вариант? Могу ли я вообще открыть рот и выразить то, что я уже закодировал? Либо цензура может менять изначально заложенный смысл: хотел предложить жениться, а по факту спросил, почему носки дырявые. На данном этапе часто включается проективный механизм — мы переносим на другого человека свои ожидания и оценки, либо негативный опыт. Руководствуемся в выборе действия тем, что мы о человеке подумали, а не тем, что он реально сказал и сделал. Также в игру могут вступать интроекты – непереваренные убеждения, которые так и не стали до конца нашими: «при старших такое не говорят», «мальчики с девочками не спорят», «даже думать о таком грязно».

На третем этапе может прослеживаться несоответствие того, что мы накодировали, тому, что по факту сказали. Неадекватности здесь могут быть разного рода. Например, обращение слишком обобщенное – «хочу мира во всем мире», а на самом деле послание другому человеку является более личным – «хочу помириться с тобой». Бывает и так, что человек удерживает какой-то личный посыл, а вместо него задает вопрос. Также суть вербальной коммуникации может не соответствовать неверебальным сигналам – «Я люблю тебя», сказанное со сжатыми зубами и напряжением в руках. Нарушение происходит также в случае, если слова окрашиваются интонационными «красками», меняющими смысл. Многие анекдоты про женские комплименты содержат примеры такого сбоя: «Тебе идет твой парик», «зеленый цвет тебе к лицу». Иногда люди не замечают, что в их интонации сквозит обвинение, хоть сами слова – средоточие доброты. Еще одним нарушением на данном этапе может быть отсутствие эмоциональной насыщенности. Случается, человек телесно конгруэнтен, подбирает слова, которые звучат правильно, а люди отвечают: не верю. Например, если безэмоционально говорить «ты – любовь всей моей жизни», и даже стоять на одном колене в эстетически привлекательной позе с букетом цветов.

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

На следующем, пятом этапе, полученные данные попадают в зоны мозга, отвечающие за раскодировку. Тут возможны разные искажения процесса коммуникации. Первое: я, к примеру, вижу какой-то феномен, и автоматически приписываю ему смысл, который был актуален для меня в детстве. Или мне говорят слово, я услышал и даже понял, но оно не рождает во мне отклика, потому что лишено смысловой нагрузки. Я не знаю, какой смысл выберу ему придать. Другая вариация искажения может выражаться в случае, когда какая-либо значимая потребность находится в хронически фрустрированном состоянии. Вы голодали какое-то время или не получали сексуальной разрядки. Тогда смысл любых сообщений, получаемых вами, будет окрашиваться смысловой нагрузкой актуальных для вас потребностей. Можно вспомнить классический гештальтистский пример, в котором есть одно целостное поле – вечеринка в разгаре. Если туда зайдет человек, испытывающий жажду, то первое, что будет для него иметь смысл – это различные напитки, если это будет человек, желающий взаимодействий с противоположным полом, то он первым делом отправится на танцпол, а человек в депрессии скорее обратит внимание на другого, сидящего в углу. Еще к одному из видов искажений приводит такой сценарий: я нахожусь в слиянии с собственными смыслами. Если услышу, к примеру, слово «предательство» в контексте политического рассказа человека о героях-патриотах и предателях, я это слово могу декодировть так, будто речь идет о каком-то болезненном предательстве именно в моей жизни. Это вызывает сильную эмоциональную реакцию, которая может чрезвычайно удивить собеседника. Или слово «правда»: для одних правда — это самое важное в жизни, а для других – газета, в которую заворачивают сало. Так же на даннном этапе возможно очень быстрое приписывание смыслов. Человек еще не успел что-либо внятного сказать, а я уже придал свой смысл его интенции и интенсивно от этого смысла страдаю.

На шестом этапе может теряться возможность отрегировать на то, что мы восприняли, даже если прояснили и распознали смыслы. Я услышал слова «я тебя люблю», но я ничего не чувствую. И другой человек не понимает, что он получает в ответ: «а я тебя нет» или «мне стыдно» или «я не знаю, что с этим делать, я напуган!». Одна из критических для полноценного контакта способностей – это описанная Бьюдженталем доступность: состояние, когда мы открыты тому, чтоб послание другого человека эмоционально влияло на нас. Часто сложности здесь испытывают люди с сильным расщеплением между рациональной и эмоциональной составляющими личности.

Говоря об общих способах улучшения качества коммуникации, выделю важность уточнения. «Когда ты так говоришь, что имеешь в виду?» «Правильно ли я тебя понял в том, что ты. » Для меня данная клиссификация нарушений служит универсальным помогающим ориентиром, и если вы будете знать места нарушения коммуникации, то с большей легкостью сможете отследить, где находитесь, опознаете ли свои потребности, говорите ли про них, насколько прямо о них говорите, насколько осознаете переживание ваших ощущений и эмоций. Соответственно, дальше вы можете видеть феноменологический отклик другого человека и понимать, услышал ли он вообще что-нибудь, при этом автоматически не приписывать своих смыслов его проявлениям.

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

Прямые послания вместо вопросов, также способствует повышению качества коммуникации: «Я хочу этого», «мне нравится это», «мне неприятно это», «Я злюсь на тебя, за то, что…», «я испытываю нежность по отношению к тебе…», «мне отвратительно это», «Ты мне нравишься». Ценным может стать избегание использования противоречивых посланий: «Я, конечно, тебя принимаю, но этого ты не имеешь права делать», «Я очень тебя люблю, но не позволю тебе говорить это», «Я ни в чем тебя не обвиняю, просто можешь быть хоть чуточку сильнее духом».

Одной из самых сильных идей, изменивших мое мировосприятие, явилось убеждение в том, что абсолютной правды не существует. И тот цвет, который я называю сливовым, для другого человека сливовым не является, а будет фиолетовым или пурпурным. Часто люди, переживающие себя застрявшими в процессе коммуникации, просто смотрят на цифру «6» с разных сторон, и для одного из них она точно выглядит как «9». Поэтому важным аспектом эффективной коммуникации является проверка субъективности восприятия каждой из сторон. Часто, единственной точкой согласия мы находим наше несогласие – «Мы согласны с тем, что у нас есть разные взгляды, которые не совпадают, и ни один из них не является абсолютной правдой». При этом важно научиться слышать точку зрения другого просто как мнение, а не как атаку на основы нашего мироздания. Это существенно снизит уровень агрессивности и неприятия позиции другого в контакте.

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

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

И последняя мысль в данной статье, на мой взгляд, способствующая процессу налаживания коммуникации – понимание не означает согласие. Часто люди жалуются и возмущаются: «Он меня не понимает! Говорю ему вынести мусор, а он не хочет!» Ведь понимает, но не хочет. Смешивание этих двух понятий часто ведет к неразрешимым конфликтам. Так что будьте осторожны со смесями, некоторые из них взрывоопасны.

Неблокирующие межпроцессные коммуникации

Wikimedia Foundation . 2010 .

Смотреть что такое «Межпроцессное взаимодействие» в других словарях:

Unix domain socket — (Доменный сокет Unix) или IPC сокет (сокет межпроцессного взаимодействия) конечная точка обмена данными, схожая с Интернет сокетом, но не использующая сетевой протокол для взаимодействия (обмена данными). Он используется в операционных системах,… … Википедия

Сокет (программный интерфейс) — У этого термина существуют и другие значения, см. Сокет. Сокеты (англ. socket углубление, гнездо, разъём) название программного интерфейса для обеспечения обмена данными между процессами. Процессы при таком обмене могут исполняться как на… … Википедия

Именованный канал — … Википедия

mmap — mmap POSIX совместимый системный вызов Unix, позволяющий выполнить отображение файла или устройства на память. Является методом ввода/вывода через отображение файла на память и естественным образом реализует выделение страниц по запросу,… … Википедия

Mailslot — Маилслот один из механизмов межпроцессного взаимодействия, обеспечивающий однонаправленную передачу информации и позволяющий производить широковещательную рассылку сообщений по сети. Реализация Маилслот является клиент серверным интерфейсом.… … Википедия

Сравнение командных оболочек — Подробнее по этой теме см.: Оболочка операционной системы. Командная оболочка это компьютерная программа с интерфейсом командной строки операционной системы. Содержание 1 Общие характеристики 2 Интеракти … Википедия

Windows Script Host — (WSH; первоначально назывался Windows Scripting Host, был переименован ко второму выпуску) компонент Microsoft Windows, предназначенный для запуска сценариев на скриптовых языках JScript и VBScript, а также и на других дополнительно… … Википедия

NDIS — (аббр.. от англ. Network Driver Interface Specification) спецификация интерфейса сетевого драйвера, была разработана совместно фирмами Microsoft и 3Com для сопряжения драйверов сетевых адаптеров с операционной системой. Одна из первых… … Википедия

FIFO — У этого термина существуют и другие значения, см. FIFO и LIFO. FIFO планировщик проц … Википедия

MPI: блокирование против неблокирования

У меня возникают проблемы с пониманием концепции блокирования связи и неблокирования связи в MPI. Каковы различия между этими двумя? Какие преимущества и недостатки?

Блокировка связи осуществляется с помощью MPI_Send() и MPI_Recv() . Эти функции не возвращаются (то есть блокируются) до тех пор, пока связь не будет завершена. Упрощенно это означает, что буфер, переданный в MPI_Send() можно использовать повторно либо потому, что MPI где-то его сохранил, либо потому, что он был получен адресатом. Точно так же MPI_Recv() возвращается, когда приемный буфер был заполнен допустимыми данными.

Напротив, неблокирующая связь осуществляется с использованием MPI_Isend() и MPI_Irecv() . Эти функции возвращаются немедленно (т.е. они не блокируются), даже если связь еще не завершена. Вы должны вызвать MPI_Wait() или MPI_Test() чтобы увидеть, завершена ли связь.

Блокирующая связь используется, когда этого достаточно, поскольку ее несколько проще использовать. Неблокирующая связь используется при необходимости, например, вы можете вызвать MPI_Isend() , выполнить некоторые вычисления, а затем выполнить MPI_Wait() . Это позволяет вычислениям и коммуникациям перекрываться, что обычно приводит к повышению производительности.

Обратите внимание, что коллективное общение (например, все-сокращение) доступно только в его версии блокировки до MPIv2. IIRC, MPIv3 представляет неблокирующую коллективную связь.

Краткий обзор режимов отправки MPI можно посмотреть здесь.

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

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

В отличие от отправки, получение имеет только один режим и может быть блокирующим или неблокирующим.

Прежде чем продолжить, необходимо также уточнить, что я явно упоминаю, какой из них является буфером MPI_Send\Recv, а какой — системным буфером (который является локальным буфером в каждом процессоре, принадлежащем библиотеке MPI, используемой для перемещения данных между рядами связи группа)

БЛОКИРОВКА СВЯЗИ: Блокировка не означает, что сообщение было доставлено получателю/получателю. Это просто означает, что буфер (отправка или получение) доступен для повторного использования. Чтобы повторно использовать буфер, достаточно скопировать информацию в другую область памяти, то есть библиотека может скопировать данные буфера в собственное место памяти в библиотеке, а затем, например, например, MPI_Send может вернуться.

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

1. Стандартный режим

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

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

Синтаксис для стандартной отправки ниже:

2. Буферизованный режим

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

Синтаксис для буфера отправки:

3. Синхронный режим

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

Синтаксис для синхронной отправки:

4. Готовый режим

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

Синтаксис для готовой отправки:

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

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

Давайте посмотрим, что происходит на каждом уровне в приведенном выше примере

Ранг 0 пытается отправить на ранг 1 и ранг 2, и получить от ранг 1 и 3.

Ранг 1 пытается отправить на ранг 0 и ранг 3 и не получает ничего из других рангов

Ранг 2 пытается получить от ранга 0, а затем выполнить некоторую операцию с данными, полученными в recv_buff.

Ранг 3 пытается отправить на ранг 0 и получить с рангом 1

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

В гипотетической ситуации, когда ранг 3 начинает выполнение до ранга 0, он скопирует исходящее сообщение в первом операторе отправки из буфера отправки в системный буфер (в современной системе;)), а затем начнет выполнение своего оператора приема. Как только ранг 0 заканчивает свои два оператора отправки и начинает выполнять свой оператор приема, данные, буферизованные в системе с рангом 3, копируются в буфер приема с рангом 0.

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

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

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

Во время таких блокирующих вызовов происходит то, что вычисления останавливаются до тех пор, пока не будет освобожден заблокированный буфер. Это обычно приводит к потере вычислительных ресурсов, так как Send/Recv обычно копирует данные из одной ячейки памяти в другую ячейку памяти, в то время как регистры в процессоре остаются бездействующими.

НЕБЛОКИРУЮЩАЯ СВЯЗЬ: Для неблокирующей связи приложение создает запрос на обмен данными для отправки и/или приема и возвращает дескриптор, а затем завершается. Это все, что нужно, чтобы гарантировать, что процесс выполняется. Т.е. библиотека MPI уведомляется о том, что операция должна быть выполнена.

Для отправителя это позволяет совмещать вычисления с коммуникацией.

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

Введение в неблокирующие алгоритмы


Смотрите, блоков нет!

Серия контента:

Этот контент является частью # из серии # статей: Теория и практика Java

Этот контент является частью серии: Теория и практика Java

Следите за выходом новых статей этой серии.

Когда к разделяемой переменной обращаются несколько потоков, все они должны использовать синхронизацию, в противном случае может возникнуть нехорошая ситуация. Главным средством синхронизации в языке программирования Java является ключевое слово synchronized (этот способ синхронизации известен также под названием внутренняя блокировка (intrinsic locking)), которое обеспечивает взаимное исключение и гарантирует, что действия одного потока, выполняющего блок synchronized , видимы для остальных потоков, которые будут выполнять блок synchronized , защищенный этой же блокировкой. При правильном применении внутренняя блокировка позволяет сделать наши программы потокозащищенными, но она может быть тяжеловесной операцией при использовании для защиты маленьких участков кода, когда потоки часто соревнуются за блокировку.

В статье «Going atomic» мы рассмотрели атомарные переменные (atomic variable), которые предоставляют атомарные операции чтение-изменение-запись для безопасного изменения разделяемых переменных без блокировок. Атомарные переменные аналогичны по семантике использования памяти переменным volatile, но поскольку они также могут меняться автоматически, их можно использовать в качестве базы для свободных от блокировок параллельных алгоритмов.

Неблокирующий счетчик

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

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

Листинг 1. Потокозащищенный счетчик, использующий синхронизацию

Класс NonblockingCounter в листинге 2 демонстрирует один из простейших неблокирующих алгоритмов: счетчик AtomicInteger , использующий метод compareAndSet() (CAS). Метод compareAndSet() означает «Обновить переменную этим новым значеним, но отказать, если другой поток изменил значение после моего последнего просмотра» (см. в статье «Going atomic» подробное объяснение атомарных переменных и функции сравнить-и-установить).

Листинг 2. Неблокирующий счетчик, использующий CAS

Классы атомарных переменных называются атомарными, потому что они предоставляют мелкомодульные атомарные обновления чисел и объектных ссылок, но они также атомарны и в том смысле, что являются основными строительными блоками для неблокирующих алгоритмов. Неблокирующие алгоритмы являются предметом изучения уже более 20 лет, но стали возможными в языке программирования Java только с появлением Java 5.0.

Современные процессоры имеют специальные инструкции для атомарного обновления разделяемых данных, которые могут обнаружить вмешательство других потоков, и compareAndSet() использует их вместо блокирования. Если все что нам нужно – это инкрементировать счетчик, AtomicInteger предлагает методы для инкрементирования, но они основаны на compareAndSet() , например, NonblockingCounter.increment() .

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

NonblockingCounter — это, возможно, простой пример, но он демонстрирует основную характеристику всех неблокирующих алгоритмов – некоторый шаг алгоритма выполняется гипотетически, с осознанием того, что он должен быть выполнен повторно, если операция CAS закончилась неудачно. Неблокирующие алгоритмы часто называют оптимистическими, потому что они выполняются с предположением о том, что не будет конфликтных ситуаций. Если такая ситуация возникает, они возвращаются на шаг назад и повторяют действие. В случае со счетчиком гипотетическим шагом является операция инкрементирования – она извлекает и добавляет единицу к старому значению в надежде на то, что оно не изменится за время вычисления обновленного значения. Если это не так, она должна повторно извлечь значение и снова выполнить инкрементирование.

Неблокирующий стек

Менее тривиальный пример неблокирующего алгоритма ConcurrentStack приведен в листинге 3. Операции push() и pop() в ConcurrentStack подобны по структуре операции increment() в NonblockingCounter . Они гипотетически выполняют определенную работу и надеются на то, что предполагаемые допущения не будут нарушены, когда придет время «подтвердить» работу. Метод push() просматривает текущий верхний элемент, создает новый элемент, который надо поместить в стек, и, затем, если самый верхний элемент не был изменен со времени последнего просмотра, помещает новый элемент в стек. Если CAS завершается неудачно — значит другой поток изменил стек, поэтому процесс выполняется снова.

Листинг 3. Неблокирующий стек, использующий алгоритм Трайбера (Treiber)

Анализ производительности

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

В условиях жесткой конкуренции (когда множество потоков обращаются к одному месту в памяти) основанные на блокировках алгоритмы начинают предлагать лучшую производительность, чем неблокирующие, поскольку при блокировании потока он прекращает попытки достучаться к ресурсу и терпеливо ждет своей очереди, уменьшая конкуренцию. Однако конкуренция такого высокого уровня – это необычная ситуация, поскольку основную часть времени потоки чередуют локальные вычисления с операциями, требующими разделяемых данных, давая другим потокам шанс использовать эти разделяемые данные. Высокий уровень конкуренции указывает также на то, что нужно подумать об изменении вашего алгоритма в сторону меньшего использования разделяемых данных. Граф в статье «Going atomic» немного сбивал с толку, поскольку анализируемая программа имела настолько нереалистично высокую конкуренцию, что казалось, будто блокировки имеют преимущество даже для небольшого числа потоков.

Неблокирующий связный список

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

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

«Трюк» при создании неблокирующих алгоритмов для не тривиальных структур данных состоит в гарантировании постоянного непротиворечивого состояния структуры данных даже во время между началом изменения данных потоком и его завершением, а также в гарантировании того, чтобы другие потоки могли не только знать, завершил ли первый поток свое обновление или все еще находится в середине этой операции, но также и то, какие операции могли бы потребоваться для завершения обновления, если первый поток куда-то ушел. Если поток начал работу со структурой данных, находящейся в середине операции обновления, он может «помочь» уже выполняющему обновление потоку завершить обновление и затем выполнить свою собственную операцию. Когда первый поток соберется попробовать завершить свое собственное обновление, он узнает, что эта работа больше не нужна и возвратит управление, поскольку CAS обнаружит вмешательство (в данном случае конструктивное вмешательство) помогающего потока.

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

LinkedQueue в листинге 4 демонстрирует операцию вставки для неблокирующего алгоритма очереди Майкла-Скотта (Michael-Scott), реализованного ConcurrentLinkedQueue :

Листинг 4. Вставка в неблокирующем алгоритме очереди Майкла-Скотта

Как и во многих алгоритмах очереди, пустая очередь состоит из одного фиктивного элемента. Указатель «головы» очереди всегда ссылается на фиктивный элемент; Указатель «хвоста» очереди всегда указывает либо на последний элемент, либо на второй от конца элемент. На рисунке 1 изображена очередь с двумя элементами в нормальных условиях:

Рисунок 1. Очередь с двумя элементами в статическом состоянии

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

Очередь всегда находится в одном из двух состояний: нормальном, или статическом (рисунки 1 и 3), или промежуточном (рисунок 2). Очередь находится в статическом состоянии перед операцией вставки и после успешной второй операции CAS (D); она находится в промежуточном состоянии после успешной первой операции CAS (C). В статическом состоянии поле next элемента, на который указывает «хвост» очереди, всегда равно null; в промежуточном состоянии – всегда не null. Любой поток может увидеть, в каком состоянии находится очередь, сравнив значение tail.next с null. В этом состоит суть разрешения потокам помогать другим потокам «завершать» свои операции.

Рисунок 2. Очередь в промежуточном состоянии во время операции вставки после добавления элемента, но перед обновлением указателя на «хвост» очереди

Операция вставки сначала проверяет, находится ли очередь в промежуточном состоянии перед попыткой вставки нового элемента (A), как показано в листинге 4. Если это так, значит какой-то другой поток должен находиться в середине операции вставки элемента между шагами (C) и (D). Вместо ожидания завершения другого потока текущий поток может «помочь» завершить операцию за него посредством перемещения вперед указателя на «хвост» очереди (B). Он продолжает проверку указателя «хвоста» очереди и передвигает его при необходимости до тех пор, пока очередь не перейдет в статическое состояние, после чего он может начать свою собственную операцию вставки.

Первая операция CAS (C) может завершиться неудачно, поскольку два потока могут конкурировать за доступ к текущему последнему элементу очереди; в этой ситуации не выполняется никаких изменений, и данный поток снова читает указатель «хвоста» очереди и пытается выполнить операцию. Если неудачно завершается вторая операция CAS (D), вставляющий поток может не пытаться выполнить ее повторно, поскольку второй поток завершил эту операцию на шаге (B)!

Рисунок 3. Очередь снова находится в статическом состоянии после обновления указателя на «хвост» очереди

Неблокирующие алгоритмы в действии

Если вы углубитесь в дебри JVM и OS, то обнаружите неблокирующие алгоритмы повсеместно. Сборщик мусора использует их для ускорения параллельных операций сборки мусора; планировщик использует их для эффективного планирования потоков и процессов и для реализации внутренней блокировки. В Mustang (Java 6.0) основанный на блокировках алгоритм SynchronousQueue заменен новой неблокирующей версией. Немногие разработчики используют SynchronousQueue напрямую, но она применяется в качестве рабочей очереди для пулов потоков, созданных фабрикой Executors.newCachedThreadPool() . Тесты производительности пула кешированных потоков показывают примерно трехкратное увеличение скорости работы по сравнению с текущей реализацией. Планируются и дальнейшие улучшения в следующей за Mustang версии под кодовым названием Dolphin.

Резюме

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

Ресурсы для скачивания

Похожие темы

  • Оригинал статьи «Java theory and practice: Introduction to nonblocking algorithms».
  • «Going atomic» (developerWorks, Brian Goetz, ноябрь 2004): Описывает атомарные классы переменных, добавленные в Java 5.0 и операцию сравнение-и-замена (CAS).
  • «Масштабируемые синхронные очереди» (ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, William N. Scherer III, Doug Lea, and Michael L. Scott, март 2006): Описывает создание и преимущества по производительности новой реализации SynchronousQueue в Java 6.
  • «Простые, быстрые и практические неблокирующие и блокирующие параллельные очереди» (Maged M. Michael и Michael L. Scott, Symposium on Principles of Distributed Computing, 1996): Подробности создания неблокирующей односвязной очереди, рассмотренной в листинге 4 данной статьи.
  • «Параллельность Java на практике» (Addison-Wesley Professional, Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, Dav >

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

Использование неблокирующих методов синхронизации

Опубликовано: 3 апреля 2012 г.

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

Большинство потоковых механизмов, включая потоковые API Windows* и POSIX*, предоставляют как блокирующие, так и неблокирующие примитивы потоковой синхронизации. По умолчанию обычно используются блокирующие примитивы. Если установка блокировки прошла успешно, то поток получает возможность управлять блокировкой и выполнять код критической секции. Но в случае неудачи происходит операция смены контекста и поток помещается в очередь ожидающих потоков. Операция смены контекста достаточно дорогостоящая и ее следует избегать по следующим причинам:

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

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

Используйте неблокирующие вызовы, чтобы устранить издержки на смену контекста. Неблокирующие системные вызовы обычно начинаются с ключевого слова try .

Если попытка получить блокировку критической секции была успешной, то функция TryEnterCriticalSection возвращает True . Иначе она возвращает False и поток продолжает выполнение своего кода.

void EnterCriticalSection (LPCRITICAL_SECTION cs);
bool TryEnterCriticalSection (LPCRITICAL_SECTION cs);

Вот типичный вариант использования неблокирующего системного вызова:

Потоковая реализация POSIX также предлагает неблокирующие версии примитивов синхронизации mutex, semaphore и переменной condition . Например, блокирующая и неблокирующая версии примитива mutex выглядят так:

int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_try_lock (pthread_mutex_t *mutex);

Также в реализации потоков Windows* можно указать время ожидания для примитивов. В Win32* API существуют системные функции WaitForSingleObject и WaitForMultipleObjects для синхронизации объектов ядра. Поток, вызвавший данные функции, будет ждать сигнала соответствующего объекта ядра или пока не истечет отведенное пользователем время ожидания. Когда обозначенный интервал времени кончается, поток возвращается к исполнению полезной работы.
DWORD WaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds);

В данном коде hHandle является описателем объекта ядра, dwMilliseconds – интервал времени, после которого функция возвращает контроль в случае, если объект ядра сигнал не установил. Значение INFINITE указывает, что поток должен ждать бесконечно. Далее приведен пример кода, демонстрирующий использование данной функции.

Подобным образом функция API WaitForMultipleObjects позволяет потоку ожидать сигнала нескольких объектов ядра.

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

Система обмена сообщениями ZeroMQ

Оригинал: «ZeroMQ» .
Автор: Martin Sústrik, перевод: Н.Ромоданов

24.8. Модель распараллеливания

Одним из требований для ØMQ было возможность использования многоядерных устройств; другими словами, чтобы можно масштабировать пропускную линейно с увеличением числа доступных ядер процессора.

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

Учитывая эти проблемы, мы решили перейти на другую модель. Цель состояла в том, чтобы полностью избежать блокировок и позволить каждому потоку работать на полной скорости. Взаимодействие между потоками было реализовано с помощью асинхронных сообщений (события), которые передаются между потоками. Это, как знают инсайдеры, является классической моделью актера (actor model).

Идея заключалась в том, чтобы запускать на каждом ядре процессора по одному рабочему потоку — наличие двух потоков, совместно использующих то же самое ядро, будет означать лишь большое количество переключений контекста без получения особых преимуществ. Каждый внутренний объект ØMQ, такой, как, скажем, движок TCP, будет тесно связан с конкретным рабочим потоком. Это, в свою очередь, означает, что нет никакой необходимости в критических секциях, взаимоисключаемых событиях (mutexes), семафорах и тому подобном. Кроме того, эти объекты ØMQ не будут перераспределяться между ядрами процессора, так что удастся избежать негативного влияния на производительность, связанного с загрязнением кэша (рис.24.7).

Рис.24.7: Несколько рабочих потоков

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

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

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

Усвоенный урок: Когда стремитесь к экстремальной производительности и масштабируемости, то рассмотрите модель актера; это чуть ли не единственная вариант в подобных случаях. Однако, если вы не пользуетесь специализированной системой, например, Erlang или самой ØMQ, вам придется написать и вручную отладить инфраструктуру большого объема. Кроме того, с самого начала подумайте о процедуре завершения работы системы. Это будет самая сложная часть кода, и если у вас нет четкого представления о том, как ее реализовать, вам, вероятно, следует в первую очередь пересмотреть использование модели актера.

24.9. Неблокирующие алгоритмы

В последнее время в моде стали неблокирующие алгоритмы. Это простые механизмы межпотокового взаимодействия, в которых не используются предоставляемые ядром примитивы синхронизации, такие как взаимоисключаемые события или семафоры; они предпочитают выполнять синхронизацию с использованием атомарных операций процессора, таких как атомное сравнение и своп (CAS). Следует иметь в виду, что они не в буквальном смысле работают без блокировок — в действительности блокировки происходят за кулисами на аппаратном уровне.

ØMQ использует неблокирующую очередь в конвейерных объектах для передачи сообщений между потоками пользователя и рабочими потоками ØMQ. Есть два интересных аспекта, касающихся того, как ØMQ использует неблокирующую очередь.

Во-первых, в каждой очередь есть ровно один поток, который осуществляется запись, и ровно один поток, который осуществляет чтение. Если необходима связь типа «1-N», то создается несколько очередей (рис.24.8). Благодаря такой организации очереди не надо беспокоиться о о синхронизации записи (есть только один поток, осуществляющий запись) или чтения (есть только один поток, осуществляющий чтение), причем очередь может быть реализована очень эффективным способом.

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

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

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

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

Рис.24.9: Неблокирующая очередь

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

Илон Маск рекомендует:  Что такое код ncurses_raw
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL