Pull to refresh

Асинхронное программирование — редактор графов

Reading time 15 min
Views 3.5K
Иногда в процессе описания бизнес-логики, необходимо составить граф асинхронных операций с внутренними зависимостями, т.е. когда задачи выполняются асинхронно, но некоторые задачи зависят от других и тем самым вынуждены «ждать» пока из можно будет запустить. В этом посте я хочу показать как эту проблему можно решить путем создания графической DSL, которая позволит разработчику визуально определить граф зависимостей.


N.b.: статья на английском и исходный код находятся тут


Введение


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

В этой статье мы создадим простой графический DSL, что позволяет конечному пользователю определить асинхронные операций, которые будут организованы с помощью механизма Pulse & Wait. Чтобы собрать прилагаемый пример, вам потребуется Visual Studio 2008 с Visual Studio SDK. Мы будем использовать инструменты Microsoft DSL Tools (входят в состав SDK) для создания наших DSL.

Описание проблемы


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

Создание DSL


Прежде чем мы начнем, позвольте мне изложить наиболее важные моменты при работе с DSL Tools:

  • В DSL Tools, графические DSL сами сделаны с помощью графического DSL. На первый взгляд это может показаться запутанным, но, в принципе, вы должны понимать, что большинство наших асинхронных DSL (которую я называю здесь AsyncDsl) будут разработаны с использованием визуальных элементов – не с языком программирования. Конечно, за кулисами будет много кода, но мы не будем с ним часто сталкиваться.
  • DSL инструменты широко используют технологию T4. Наша графическая DSL в действительности является только визуальным представлением XML, и Т4 превращает этот XML в код. Таким образом, когда вы редактируете визуальные элементы с DSL Tools, вы действительно редактируете XML.
  • Ваш DSL по-прежнему создается с использованием C#, и он компилируются. Вы можете расширить его с помощью частичных (partial) классов и т.п., что позволит вашему DSL вести себя определенным образом. Мы не будем делать ничего подобного в данной статье.
  • Cоздание DSL с помощью инструментов DSL Tools касается только визуальной части – той части, которая позволяет пользователю визуально создавать XML модели. Та часть, которая превращает ее в код простого текста – отдельная проблемы, с которой мы встретимся позже.

Чтобы создать DSL-проект в Visual Studio, выберите New Project, а потом Other Project Types → Extensibility → Domain-Specific Language Designer.




После нажатия кнопки OK, вам будет показан визард, где вы сможете определить некоторые особенности DSL которую вы создаете.

  • На первой странице, кроме определения имени вашего языка, вы также можете выбрать начальный шаблон. Этот шаблон определяет, какими начальными возможностями обладает DSL имеет – например, выбирая Task Flow, вы определяете, что изначальные элементы DSL будут относиться к схемо-подобным (flowchart) структурам. Независимо от того, какой шаблон вы выберете на данном этапе, вы всега сможете переопределить поведение вашей DSL путем удаления первочино сгенерированных элементов.
  • Вторая страница позволяет вам выбрать файловое расширение (extension) для вашего DSL. Это расширение будет фигурировать в тех местах, где вы будете вставлять вашу DSL в ваши собственные проекта. Помимо расширения, визард генерирует иконку.
  • Третья страница позволяет задать некоторые строки, которые определяют ваше DSL, таких как имя продукта которому ваш DSL принадлежит.
  • Четвертая страница фактически заставляет вас подписать вашу сборку – либо уже существующим, либо новым ключем.

Когда закончите работу с мастером, вы получите каркас определения DSL. Если вы никогда не работали с DSL-функционалом в Visual Studio, то скриншот может вас немного шокировать.




В процессе редактирования DSL принимают участие следующие элементы:

  • Dsl Designer Toolbox. Эта панель содержит все элементы, с которыми вы будете работать при проектировании ваших DSL. Используются эти элементы так же как например в WinForms – берете элемент и перетаскиваете его в окно редактора (то центральное окно со странными боксами).
  • Сам DSL-дизайнер. Вообще-то это файл с расширением .dsl, но как видите, редактор мегавизуальный – как я уже сказал, DSLи сами по себе строятся с помощью других DSL. У этого DSL две части – с левой стороны находятся классы и отношения между ними, а в правой – визуальные элементы, т.е. визуальные отражения DSL-концептов, с которыми будет работать конечный пользователь. Тем самым, можно представить себе DSL так: справа находится визуализация, слева – логика.
  • Solution Explorer. При создании DSL, вы получите два проекта – один, определяющий DSL который вы делаете, и другой, который определяет редактор компонентов, связанных с DSL. Мы еще поговорим об этом позже – сейчас важно лишь отметить одну единственную кнопку, которая трансформирует все шаблоны:


    Это очень важная кнопка. Как я уже говорил, DSLи – это XML-спецификации, которые трансформируются в код. Это означает, что для того чтобы обновить определение нашего DSL (а определение – это тоже DSL), нужно трансформировать все шаблоны в C#. Кнопка выше именно это и делает. Поэтому если у вас вдруг в конечной DSL не отражаются какие-то изменения, которые вы уверены что сделали – значит вы забыли нажать на эту кнопку.
  • The DSL explorer. Это совершенно новая панель в студии, которыя представляет ваш DSL в форме дерева, и выглядит примерно так:



    Это дерево инкапсулирует многие структурные аспекты DSL. Важно отметить, что некоторые узлы дерева имеют свой набор свойств, которые можно увидеть, нажав F4.

  • Страницы для редактированя свойств (property pages) существуют как для элементов дерева DSL Explorer, так и для визуальных элементов в редакторе DSL. Некоторые элементы DSL можно редактировать прямо «в редакторе» – например, вы можете определить форму отношений (one-to-many, many-to-many, и т.д.) между двумя элементами, не открывая при этом панель свойств. Как это удобней делать – решать вам.

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

Арранжировка визуальных элементов


Как я уже писал, тулбокс содержит все элементы, с которыми вам предстоит работать. Эти элементы разделены на две группы – «логическую» и «визуальную». Логические элементы – это те, которые определяют структуру (т.е. домен) в вашем DSL. Визуальные элементы отражают те прямоугольники, линии, и пободные элементы, которыми пользователь оперирует при работе с DSL.

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




У доменного класса могут быть свойства, т.е. значения, которые пользователь может устанавливать. У нашего класса Operation есть свойства Timeout, Name и Description которые конечный пользователь сможет определить после того, как перетащит инстанс объекта Operation к себе в модель.

Тут небольшая проблемка – на самом деле пользователь не перетаскивает к себе в модель доменный класс напрямую. Вместо этого, он перетаскивает себе в модель OperationShape, который является визуальным отражением Operation. Этот класс формируется из GeometryShape (взято из того же тулбокса):




Определив доменный класс Operation а также его визуальное представление OperationShape, их нужно связать вместе (если запустить как есть, ничего работать не будет). Для этого используется элемент Diagram Element Map. Фактически, эта штука – линия, которая соединяет два элемента, определяя ассоциацию между ними. Но даже если ее добавить, все равно пока ничего работать не будет.

Отношения между элементами


Прежде чем мы начнем работать с созданием тулбокс-контролов для нашей DSL (а это весело), нужно поговорить об отношениях (relationships) между элементами. Есть два типа отношений – Embedding Relationship и Reference Relationship. Если вы используете embedding relationship, элемент А будет полностью заключен в состав элемента В. Например, если у меня есть swimlane (большой горизонтальный кусок визуального пространства), и в него нужно вставлять целые классы, то имеет смысл использовать embedding relationship. Если же у меня просто есть блоки к которым нужно прицеплять комментарии, пойдет и reference relationship.

Давайте посмотрим на то, как мы будет использовать элементы для нашей конкретной задачи. В «корне» нашей можели у нас фигурирует элемент ExampleModel. Я даже имя этого элемента менять не буду, т.к. он не будет фигурировать в конечной DSL. Для того, чтобы определить то, что моя модель содержит процессы и комментарии, я рисую линии embedding relationship между соответствующими классами и получаю следующую картину:




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

Внимание: дизайнер DSL применяет к вашему языке ряд правил, одно из которых требует, что все элементы являются частью чего-то. Это означает, что все элементы должны сводиться к одному, «конревому» контейнеру. (Если вспомнить, что DSL == XML, причина этого требования должна быть очевидна.)

Мы воспользовались embedding relationship для того чтобы сказать нашей DSL, что и процессы и комментарии являются частью общей модели. Теперь мы можем использовать reference relationship чтобы определить, что у процессов могут быть комментарии, и что эти два элемента можно связывать.




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

Тулбоксы, наконец-то


Получив логическую и визуальную часть вашего DSL, нужно дать пользователям возможность перетаскивать элементы из этой DSL на их дизайнер. Вот с чего нужно начинать – с узла Editor в DSL Explorer:



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




Тут две опции – коннекторы и элементы. Коннекторы – это линии (даже возможно со стрелочками) которые соединяют вместе элементы. А элементы – это блокообразные структуры.

После создания нового элемента, нажмите F4 и вы увидите свойства этого элемента:




Что тут важно, так это то, что несколько из этих свойств нужно обязательно заполнить – иначе DSL не запустится. Из тех что очевидно нужно определить – определение доменного класса, который отражает элемент, а также определение иконки. (Пара дефолтных иконок уже предоставлена, так что если лень создавать свои, можно воспользоваться готовыми.)

Запускаем!


Прорезюмируем процесс создания DSL:

  1. Сделали базовую DSL с использованием визарда
  2. Добавили доменные классы представляющие нужные нам концепции, такие как процесc.
  3. Добавили отношения между дуоменными классами – в нашем случае, определили тот факт, что операции пренадлежат общей модели, и что у них есть комментарии. Также добавили операции перехода между операциями, а также элементы начала и конца.
  4. Определили визуальные элементы, которые будет использовать наша DSL.
  5. Связали визуальные элементы с доменными классами.
  6. Создали тулбокс-контролы и связали их с соответствующими классами.

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




Концепции


Для нашей асинхронной DSL, мы определили следующие идиомы:

  • Operation
    Это наш unit of work, например ‘приготовить чай’. Мы подразумеваем, что операция может пройти без каких-либо сбоев.
  • Process
    Процесс – это последовательность операций в графе. Единственная причина по которой мы добавили это элемент – это для того, чтобы иметь возможность держать несколько графов операций в одном классе.
  • Start и Finish
    Процесс должен где-то начинаться и заканчиваться, поэтому мы создали два элемента чтобы маркировать состояния начала и конца.
  • Finish-to-start transition
    Этот переход определяет, что операция может быть запущена только после того, как другая операция завершена.
  • Start-to-start transition
    Этот переход определяет что операция может начаться только тогда, когда другая операция была запущена, и не раньше.

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

Воспользовавшись нашей DSL, весь процесс можно определить вот так:




Как вы уже наверное догадались, жирные линии символизируют finish-to-start, а пунктирная – start-to-start.

Трансформируем модель с помощью Т4


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

Производство конечного результата в Т4 движится несколькими методами, такими как WriteLine() (пишет строку в кончный файл) и Push/PopIndent() (держат в стеке кол-во отступов).

Я не буду тут представлять код трансформации Т4 – его можно скачать по ссылке выше. Вместо этого, я покажу что произведет наша DSL из определения завтрака.

namespace Debugging<br/>
{<br/>
  using System.Threading;<br/>
  partial class Breakfast<br/>
  {<br/>
    private readonly object MakeSandwichLock = new object();<br/>
    private readonly object EatBreakfastLock = new object();<br/>
    private readonly object GetJamLock = new object();<br/>
    private bool MakeTeaIsDone;<br/>
    private bool ToastBreadIsDone;<br/>
    private bool GetJamIsDone;<br/>
    private bool MakeSandwichIsDone;<br/>
    private bool MakeTeaStarted;<br/>
    private bool ToastBreadStarted;<br/>
    private bool GetJamStarted;<br/>
    private bool MakeSandwichStarted;<br/>
    protected internal void MakeTea()<br/>
    {<br/>
      MakeTeaImpl();<br/>
      lock(EatBreakfastLock)<br/>
      {<br/>
        MakeTeaIsDone = true;<br/>
        Monitor.PulseAll(EatBreakfastLock);<br/>
      }<br/>
    }<br/>
    protected internal void ToastBread()<br/>
    {<br/>
      lock(GetJamLock)<br/>
      {<br/>
        ToastBreadIsDone = true;<br/>
        Monitor.PulseAll(GetJamLock);<br/>
      }<br/>
      ToastBreadImpl();<br/>
      lock(MakeSandwichLock)<br/>
      {<br/>
        ToastBreadIsDone = true;<br/>
        Monitor.PulseAll(MakeSandwichLock);<br/>
      }<br/>
    }<br/>
    protected internal void GetJam()<br/>
    {<br/>
      lock(GetJamLock)<br/>
        if(!(ToastBreadStarted))<br/>
          Monitor.Wait(GetJamLock);<br/>
      GetJamImpl();<br/>
      lock(MakeSandwichLock)<br/>
      {<br/>
        GetJamIsDone = true;<br/>
        Monitor.PulseAll(MakeSandwichLock);<br/>
      }<br/>
    }<br/>
    protected internal void MakeSandwich()<br/>
    {<br/>
      lock(MakeSandwichLock)<br/>
        if(!(ToastBreadIsDone && GetJamIsDone))<br/>
          Monitor.Wait(MakeSandwichLock);<br/>
      MakeSandwichImpl();<br/>
      lock(EatBreakfastLock)<br/>
      {<br/>
        MakeSandwichIsDone = true;<br/>
        Monitor.PulseAll(EatBreakfastLock);<br/>
      }<br/>
    }<br/>
    protected internal void EatBreakfast()<br/>
    {<br/>
      lock(EatBreakfastLock)<br/>
        if(!(MakeTeaIsDone && MakeSandwichIsDone))<br/>
          Monitor.Wait(EatBreakfastLock);<br/>
      EatBreakfastImpl();<br/>
    }<br/>
  }<br/>
}<br/>

Немало кода! Но зато этот код отражает ту структуру, которую мы определили. Теперь остается только воспользоваться сгенерированной структурой:

namespace Debugging<br/>
{<br/>
  partial class Breakfast<br/>
  {<br/>
    AutoResetEvent eatHandle = new AutoResetEvent(false);<br/>
    Random rand = new Random();<br/>
    public void Prepare()<br/>
    {<br/>
      ThreadStart[] ops = new ThreadStart[] {<br/>
        MakeTea,<br/>
        GetJam,<br/>
        ToastBread,<br/>
        MakeSandwich,<br/>
        EatBreakfast };<br/>
      foreach (ThreadStart op in ops)<br/>
        op.BeginInvoke(nullnull);<br/>
      eatHandle.WaitOne();<br/>
    }<br/>
    private int RandomInterval<br/>
    {<br/>
      get<br/>
      {<br/>
        return (1 + rand.Next() % 10) * 100;<br/>
      }<br/>
    }<br/>
    public void MakeTeaImpl()<br/>
    {<br/>
      Thread.Sleep(RandomInterval);<br/>
      Console.WriteLine("Make tea");<br/>
    }<br/>
    public void ToastBreadImpl()<br/>
    {<br/>
      Thread.Sleep(RandomInterval);<br/>
      Console.WriteLine("Toast bread");<br/>
    }<br/>
    public void GetJamImpl()<br/>
    {<br/>
      Thread.Sleep(RandomInterval);<br/>
      Console.WriteLine("Get jam");<br/>
    }<br/>
    public void MakeSandwichImpl()<br/>
    {<br/>
      Thread.Sleep(RandomInterval);<br/>
      Console.WriteLine("Make sandwich");<br/>
    }<br/>
    public void EatBreakfastImpl()<br/>
    {<br/>
      Thread.Sleep(RandomInterval);<br/>
      Console.WriteLine("Eat breakfast");<br/>
      eatHandle.Set();<br/>
    }<br/>
  }<br/>
}<br/>

Результат вызова этого кода примерно такой:

Make tea
Toast bread
Get jam
Make sandwich
Eat breakfast
All done

Хотя конечно же Make tea и Toast bread могут фигурировать и в другом порядке.

Заключение


DSL Tools – это сложный, но мощный инструментарий. Ключевая характеристика этого пакета – простота работы с языком после того, как он был определен. Тут я смог только поверхностно описать работу с DSL Tools, т.к. возможностей и ньюансов очень много. Надеюсь что это пост мотивирует кого-то на проведение собственных исследований. ■
Tags:
Hubs:
+24
Comments 12
Comments Comments 12

Articles