📜 ⬆️ ⬇️

Asynchronous programming - graph editor

Sometimes in the process of describing business logic, it is necessary to compile a graph of asynchronous operations with internal dependencies, i.e. when tasks are performed asynchronously, but some tasks depend on others and thus have to “wait” until it can be started. In this post, I want to show how this problem can be solved by creating a graphical DSL that allows the developer to visually determine the dependency graph.


Nb: English article and source code are here.


Introduction


Generally speaking, domain-specific languages ​​(domain-specific languages, DSL for short) come in three forms. The first type is text DSL, which is defined solely through text and structure, and is associated with a specific process of converting this text into code. The second type is structural DSL, where content is defined using a tree or graph-like editor. I want to discuss the third type - this is a graphical DSL, where the developer works with a graphical editor to create a visual structure, which can later be turned into code.
')
In this article, we will create a simple graphical DSL, which allows the end user to define asynchronous operations that will be organized using Pulse & Wait. To build the attached example, you need Visual Studio 2008 with the Visual Studio SDK. We will use Microsoft DSL Tools (included in the SDK) to create our DSL.

Description of the problem


Since working with Pulse & Wait is difficult, I want to make a graphical DSL that would allow me to determine the sequence of operations that can be organized using the Pulse & Wait mechanism. In particular, I want to be able to drag and drop asynchronous blocks in the editor, as well as the ability to define links between them to form rules for asynchronous, dependent execution.

Create DSL


Before we begin, let me explain the most important points when working with DSL Tools:


To create a DSL project in Visual Studio, select New Project, and then Other Project Types → Extensibility → Domain-Specific Language Designer.




After clicking the OK button, you will be shown a wizard where you can identify some of the features of the DSL you are creating.


When done with the wizard, you get a DSL definition framework. If you have never worked with DSL functionality in Visual Studio, then the screenshot may shock you a bit.




The following elements take part in the DSL editing process:


Let's now delve into the process of creating our DSL.

Arranging Visuals


As I already wrote, the toolbox contains all the elements with which you have to work. These elements are divided into two groups - “logical” and “visual”. The gates are those that define the structure (i.e., domain) in your DSL. Visual elements reflect those rectangles, lines, and similar elements that the user operates on when working with DSL.

The core concept of the DSL logical structure is the domain class. This class can represent anything , depending on what subject area you are working with. As we work with asynchronous operations, one of our domain classes will be called Operation :




A domain class may have properties , i.e. values ​​that the user can set. Our Operation class has Timeout , Name and Description properties that the end user can determine after dragging the instance of the Operation object to his model.

There is a small problem here - in fact, the user is not dragging the domain class directly into its model. Instead, it drags itself into the OperationShape model, which is a visual reflection of Operation . This class is formed from GeometryShape (taken from the same toolbox):




Having defined the domain class Operation as well as its visual representation of the OperationShape , they need to be linked together (if run as it is, nothing will work). For this, the Diagram Element Map element is used. In fact, this thing is the line that connects the two elements, defining the association between them. But even if you add it, still nothing will work.

Relationship between elements


Before we start working with creating toolbox controls for our DSL (which is fun), we need to talk about the relationships between the elements. There are two types of relationships - Embedding Relationship and Reference Relationship. If you use an embedding relationship, element A will be completely enclosed in element B. For example, if I have a swimlane (a large horizontal piece of visual space) and need to embed whole classes in it, then it makes sense to use an embedding relationship. If I just have blocks to attach comments to, the reference relationship will also go.

Let's look at how we will use the elements for our specific task. In the “root” of our mozheli we have an element ExampleModel . I will not even change the name of this element, since he will not be featured in the ultimate DSL. In order to determine if my model contains processes and comments, I draw the embedding relationship lines between the respective classes and get the following picture:




Orange boxes symbolize the relationship, with the names and cardinality of the relationship on both sides. Cardinality is later regulated by the DSL designer, so that the end user cannot break it. As for the relationship, the meaning of these orange boxes is that they allow you to link together different domain classes when editing an already finished DSL.

Note: The DSL designer applies a set of rules to your language, one of which requires that all elements are part of something. This means that all elements must be reduced to a single, “primary” container. (If you recall that DSL == XML, the reason for this requirement should be obvious.)

We used the embedding relationship to tell our DSL that both processes and comments are part of a general model. We can now use the reference relationship to determine that processes can have comments, and that these two elements can be linked.




The dotted line above indicates a reference relationship, i.e. in our case, the operation can simply refer to the comment - and not contain it. Of course, this relationship has its own visual element (the line that connects the operation and comment), which we now talk about.

Toolboxes, finally


Having obtained the logical and visual part of your DSL, you need to give users the ability to drag and drop items from this DSL onto their designer. This is where to start - from the Editor node in DSL Explorer:



To create a new item for the toolbox, right-click on the entire DSL. You will get the following menu:




There are two options - connectors and elements. Connectors are the lines (even perhaps with arrows) that connect elements together. And the elements are block-like structures.

After creating a new item, press F4 and you will see the properties of this item:




What is important here is that several of these properties must be filled in, otherwise DSL will not start. From those that obviously need to be defined - the definition of a domain class that reflects the element, as well as the definition of the icon. (A couple of default icons have already been provided, so if you are too lazy to create your own, you can use ready-made ones.)

Run!


Summarize the DSL creation process:

  1. Made a base DSL using a wizard
  2. Added domain classes representing the concepts we need, such as a process .
  3. The relationships between the doom classes have been added - in our case, they determined that the operations belong to the general model and that they have comments. Also added operations transition between operations, as well as elements of the beginning and end.
  4. Identified the visual elements that our DSL will use.
  5. Associated visual elements with domain classes.
  6. Created toolbox controls and associated them with the corresponding classes.

Our DSL is half ready: we have defined only the visual part. After we have transformed all the templates and launched our language, we can finally start playing with our DSL:




Concepts


For our asynchronous DSL, we defined the following idioms:


Let's look at a real example: the process of eating breakfast (I know, not very clever). To make breakfast, you need to put the kettle, as well as put the bread in the toaster - in any order. While everything is being prepared, I want to get some jam, but only if I have already turned on the toaster. When I got the finished bread and got the jam, I can make a sandwich. And only when both the sandwich and the tea are ready, can I start absorbing the breakfast.

Using our DSL, the whole process can be defined like this:




As you probably already guessed, the bold lines symbolize finish-to-start, and the dotted line, start-to-start.

Transform the model with T4


The visual model of breakfast exists only as DSL, so we need T4 to turn it into a complete code. Fortunately, by the time we have to do the conversion, the model has already been converted to XML format, and all that remains is to go around it and generate what we need.

The production of the final result in T4 moves by several methods, such as WriteLine() (writes a line to the final file) and Push/PopIndent() (keep the number of indents on the stack).

I will not present here the T4 transformation code - it can be downloaded from the link above. Instead, I will show what our DSL will produce from the breakfast definition.

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/>

A lot of code! But this code reflects the structure that we defined. Now it only remains to use the generated structure:

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( null , null );<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/>

The result of calling this code is something like this:

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

Although of course Make tea and Toast bread can appear in a different order.

Conclusion


DSL Tools is a sophisticated but powerful tool. The key feature of this package is the ease of working with the language after it has been defined. Here I could only superficially describe working with DSL Tools, since opportunities and nuances a lot. I hope that this post motivates someone to conduct their own research. â– 

Source: https://habr.com/ru/post/74367/


All Articles