вторник, 30 июня 2009 г.

[comp.concurrency] Совсем коротко о SCOOP

SCOOP, или Simple Concurrent Object-Oriented Programming, – это надстройка над языком Eiffel для конкурентного и распределенного программирования. Разговоры о SCOOP идут уже очень давно. В толмуде Бертрана Мейера об Eiffel SCOOP посвящена изрядная часть. Но до реальной эксплуатации SCOOP-а дело, как я понимаю, так и не дошло и в природе существует только его прототип.

Вспомнить о SCOOP меня заставила недавняя статья “SCOOP: Concurrent Programming Made Easy”. В ней дается короткий, но хороший обзор технологии SCOOP. И, самое важное, в ней был описан механизм дуэлей, о котором я раньше не слышал (или пропустил мимо ушей). Ниже следует очень свободный пересказ содержимого упомянутой статьи. Если кто-то заинтересуется подробностями – пожалуйста к первоисточнику :)

Итак, смысл SCOOP сводится к тому, что объекты разрешается распределять по процессам. Процессом может быть другая нить в том же приложении, другой процесс на этой же машине или же другой процесс на другой машине в сети. Для того, чтобы один процесс мог ссылаться на объекты в другом процессе, вводится новый тип ссылок – separate. Например, метод some_feature получает два аргумента: первый нормальный, а второй – ссылку на объект из другого процесса:

some_feature(a: LOCAL_TYPE; b: separate GLOBAL_TYPE) is
...
end

В принципе, ссылка на любой тип может быть объявлена как separate. Хотя не для всех типов это может иметь смысл. Можно так же описывать классы, помеченные как separate – это будет означать, что ссылки на такие классы будут автоматически считаться separate, а все объекты это типа будут т.н. separate object.

А дальше начинается особая SCOOP-овская магия :)

Все обращения к separate-объекту будут ставиться в FIFO-очередь. За этим будет следить среда исполнения Eiffel-SCOOP. Т.е. separate-объекты автоматически получают внутренний монитор (мутекс) и заботиться внутри separate-объекта о защите от многопоточного доступа к его данным не нужно.

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

class SAMPLE
feature
  -- Вот у нас ссылка на separate-объект.
  mailslot: separate MAILSLOT
  -- А вот в этом методе мы попытаемся обратиться
  -- к separate-объекту.
  produce_messages is
  do
    ...
    -- Это у нас не получится, поскольку mailslot
    -- не является агрументом produce_messages.
    mailslot.store (msg) -- ОШИБКА!
    -- Зато вот так можно.
    store_message (mailslot, msg)
  end
  -- Этот метод может обращаться к mailslot,
  -- поскольку он получает его в качестве аргумента.
  store_message(to: separate MAILSLOT,
    what: MSG) is
  do
    to.store (what)
   end
end

Такая политика связана с механизмом контроля за целостностью объектов в SCOOP. Поскольку SCOOP сам разруливает синхронизацию доступа к объектам, то ему нужно знать, в какой момент нужно захватывать монитор объекта. Таким моментом как раз является вызов метода, в который аргументом передается separate-объект. (Мой склероз может мне изменять, но мне кажется, что таким образом SCOOP может отслеживать вложенные вызовы функций с передачей того же самого объекта – он повторно уже не будет захватываться). Так же такая политика, по мнению разработчиков SCOOP, позволяет избегать тупиковых ситуаций.

Если SCOOP-процессы работают в рамках одного приложения, то обращение к separate-объекту будет происходить просто как вызов метода с необходимыми блокировками. Но если SCOOP-процессы разнесены по разным физическим процессам или даже узлам сети, то SCOOP берет на себя задачу создания proxy-объектов и маршалинг/демаршалинг вызовов.

А дальше начинается самое интересное. Ведь Eiffel – это язык, построенный на использовании Design By Contract. И оказывается, что в SCOOP контракты задействованы достаточно оригинальным образом. В частности, предусловия играют роль condition variable. Т.е. предикаты, которые записываются в секции require делятся на две категории: те, которые можно проверить немедленно и те, которые зависят от внешних условий. Например, приведенный выше метод store_message может быть записан с предусловиями:

store_message(to: separate MAILSLOT,
  what: MSG) is
require
  -- Это условие вычисляется сразу.
  mailslot_specified: to /= Void
  -- А вот это может проверяться неоднократно.
  mailslot_has_free_space: to.free_ceils > 0
do
  to.store (what)
end

Так вот, когда в программе произойдет вызов store_message, то среда исполнения проверит, что возвращает метод free_ceils у separate-объекта. И если это значение равно нулю, то значит предусловия не выполнены и нужно уснуть, пока они не будут выполнены.

Никаких condition variable в SCOOP не видно. Зато на предусловиях их можно имитировать. Не понятно правда, как сделать ожидание, ограниченное по времени (что-то типа cond_timedwait).

Еще одна черта SCOOP – распараллеливание вычислений. Тут, однако, нужно вспомнить про Command Query Separation Principle. Команда должна модифицировать состояние объекта, но не должна возвращать результата. А запрос не должен модифицировать объект, но должен возвращать результат. По сути, запрос должен быть функцией без побочных эффектов.

Итак, допустим, у нас есть ссылка на separate-объект A. Мы можем вызывать несколько команд у него. Это не может быть распараллелено, т.к. все команды будут поставлены в FIFO очередь объекта A. Аналогично и с запросами к объекту A.

Но вот если у нас есть ссылки на разные separate-объекты A, B и C, то обращения к ним могут распараллеливаться. Точкой синхронизации будет первое обращение с запросом к объекту. Например:

some_feature(
  a: separate A;
  b: separate B;
  c: separate C) is
do
  a.do_something
  b.do_something
  c.do_something
  -- Все три задействованные выше команды могут
  -- выполняться параллельно. Зато здесь произойдет
  -- синхронизация действий над объектами a и b.
  if a.result /= Void and b.result /= Void then
    -- До сих пор вычисления для c могли идти независимо.
    -- Но сейчас произойдет синхронизация.
    a.result.attach (c.result)
  end
end

А теперь пара слов о дуэлях (в упомянутой статье им уделено мало внимания, к сожалению). Итак, есть объект, который называется holder. Он владеет неким separate-объектом b, поскольку успел войти в какой-то свой метод r(b) (т.е. b передан в r как аргумент). И тут на арену выходит другой объект, называемый challenger, которому так же нужен объект b. Но объект b сейчас захвачен holder-ом и challenger-у нужно ожидать завершения r. Если же такое ожидание нежелательно, то можно применять дуэли.

Объект holder может использовать два специальных вызова: yeild (который говорит, что holder готов отдать владение объектом challenger-у) и retain (который говорит, что holder не желает отдавать владение). У объекта challenger-а есть набор из трех методов: demand (потребовать немедленное владение объектом), insist (попробовать получить владение объектом) и wait_turn (ожидать хода от holder-а). Поведение holder-а и challenger-а в этих условиях демонстрируется следующей таблицей:

 

wait_turn

demand

insist

retain

challenger ждетИсключение у challenger-аchallenger ждет

yeild

challenger ждетИсключение у holder-аИсключение у holder-а

Т.е., происходить может следующее. Объект holder захватывает какой-то объект и начинает длительную операцию. Где-то на ее середине holder понимает, что критически-важные действия, способные нарушить инварианты объектов, он уже выполнил. Поэтому, если кто-то нуждается в захваченном объекте, то holder может отдать ему владение. Для этого holder вызывает метод yeild. Если после этого challenger вызовет demand или insist, то challenger получит владение над объектом, а у holder-а возникнет исключение.

Если же holder не хочет отдавать владение объектом, то он вызывает retain (или ничего не вызывает, т.к. политика retain используется по умолчанию). И если challenger обращается к demand, то у holder-а ничего не происходит, зато у challenger-а выскакивает исключение. Если же challenger вызывает insist, то challenger засыпает до освобождения объекта.

Как я понимаю, механизм дуэлей нужен для использования SCOOP в real-time проектах. Тогда какой-нибудь низкоприоритетный holder сможет работать с неким важным объектом лишь до тех пор, пока не возникнет более высокоприоритетное событие, обработкой которого занимается challenger. Объект challenger вызывает demand, у holder-а выскакивает исключение, а chellenger получает в свое распоряжение нужный ему объект. Правда, я сомневаюсь, что разработка корректного кода с использованием дуэлей будет простой – уж очень внимательно нужно расставлять в коде retain/yeild и demand/insist. Да и получить в произвольный момент времени исключение, как следствие yeild/demand, хорошего мало, на мой взгляд.

Комментариев нет: