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, хорошего мало, на мой взгляд.
Комментариев нет:
Отправить комментарий