February 9, 2014

Consuming Task-based Asynchronous Methods from OpenEdge ABL Part 1

The Task-based Asynchronous Pattern (TAP) was introduced in .NET 4.0 as part of the Task Parallel Library (TPL), which was one of the key features in this release. Methods implementing the TAP pattern can be recognized by their return type System.Threading.Tasks.Task and an "Async" suffix on the method name, if they follow the convention. Tasks are a great abstraction for the complexities of multithreading and asynchrony. Compared with the older patterns in the .NET Framework (EAP and APM) they are a lot easier to use, require less code and are more powerful.

Why should I care as an OpenEdge developer?

Because of the growing importance of multithreading and asynchrony tasks are becoming a substantial part of .NET, which makes them harder and harder to avoid. If we can't use tasks as OpenEdge developers we could only access a shrinking part of .NET via the CLR bridge. Tasks are also a great way to expose self written multithreaded C# code to the single threaded world of OpenEdge.

Let's try it

.NET 4.0 libraries can be referenced from OpenEdge 11.0 onwards. I assume that everything in this post is true for OE 11.0 to 11.3, and I checked the documentation from 11.0 and 11.3 to ensure that, but I only have 11.1 available for testing.

To keep things simple in the first part I will concentrate on tasks without results and leave Task<T> for the next part. For illustration purposes I will use a fictive method with the following signature: public Task DoAsync().

Our goal for this post is to invoke the DoAsync method from ABL, and invoke a method named Done (in the same class) when the task is completed.

In C# (.NET 4.0) it is done like this:

DoAsync().ContinueWith(t => Done());

Invoking the DoAsync method from ABL is no problem, but we don't know when the task has finished. So we need the ContinueWith method as in the C# example. Code completion in OpenEdge Developer Studio lists some overloads for this method. The simplest requires one parameter of type Action<Task>: ContinueWith overloads from OpenEdge Developer Studio

Although generic .NET classes can be instantiated in ABL, and this one looks like a generic class, it will not work with this one because Action is a delegate and not a class.

The OpenEdge documentation (GUI for .NET Programming > Limitations of support for .NET classes) says:

You cannot use an instance of System.Delegate or any delegate type derived from it. However, when you implement a .NET abstract or interface event in ABL, you must make reference to a delegate type in order to specify the event signature.

What is a delegate?

I like to describe delegates as method pointer objects. Or more precise from the msdn documentation:

A delegate in C# is similar to a function pointer in C or C++. Using a delegate allows the programmer to encapsulate a reference to a method inside a delegate object. The delegate object can then be passed to code which can call the referenced method, without having to know at compile time which method will be invoked. Unlike function pointers in C or C++, delegates are object-oriented, type-safe, and secure.

What can we do now?

Now we know what a delegate is and that we need one to use the ContinueWith method. But using delegates is not possible in ABL.

So we need some kind of adapter that allows us to create a .NET delegate which points to ABL code.

As we have seen in the example it is very easy to create a delegate in C#. But how can we invoke ABL code from .NET?

There are three different possibilities:

  1. Implementing a .NET interface from an ABL class.
  2. Creating an ABL class which inherits from a .NET class.
  3. Subscribing to a .NET event in ABL.

Solutions 1 and 2 share a great disadvantage as they dictate the method name, and therefore do not allow more than one method per class. That's why we will take the event which is the most flexible option.

This would lead us to a delegate-event adapter. Even though this is an interesting topic, and would solve our problem, I want to keep things simple and focus on Tasks this time. Thus we will be able to optimize our solution for tasks which will be easer to implement and - more important - easier to use.

Less abstract we need something that takes a task, exposes an event and publishes that event when the task is completed.

The TaskAwaiter

public class TaskAwaiter
    public event EventHandler TaskCompleted;

    public void Await(Task task)
        task.ContinueWith(t => OnTaskCompleted());

    private void OnTaskCompleted()
        var handler = TaskCompleted;
        if (handler != null)
            TaskCompleted(this, EventArgs.Empty);

This little C# class meets the above requirements. The Await method takes a task and registers a continuation task, which publishes the TaskCompleted event by invoking the TaskCompleted method.

If you are curious why I used the EventHandler delegate with it's two parameters seeming quite useless in this case: There is a reason for that. .Net events used from ABL must have the Object, EventArgs signature (of course it's possible to subclass EventArgs). The ABL compiler will not complain about other event signatures, but at runtime your subscribers won't get invoked.

Threading Issues

As implied by the caption the TaskAwaiter is not ready yet. Running the above version will present the following error message: ABL wrong thread error

The problem is that the continuation task, which publishes the event we subscribed to in ABL, is not executed on the ABL thread. This is because we used the one parameter version of ContinueWith. We just specified the task to run (invoke TaskCompleted), but not how. That's why the task was executed on a thread pool thread and not on the ABL thread, which invoked the Await method.

Invoking on the right thread

So we need to add more parameters to the ContinueWith method in order to run the continuation on the ABL thread. This raises two questions:

  1. How to capture the right thread?
  2. How to pass that information to ContinueWith?

We know that ContinueWith is called on the ABL thread so we could capture the thread via Thread.CurrentThread. However, this has no value because it is not possible to execute code on an existing thread. The only way is to already have code running in the target thread which allows other threads to communicate with it.

In case of a .NET UI thread the work has already been done. Windows Forms offers Control.Invoke and WPF Dispatcher.Invoke to execute code on the UI thread.

Simply put, if you invoke one of these methods, a window message is sent to the corresponding window, which then gets processed by the message loop running in the UI thread, which will invoke the desired code.

This explains why we can't use Thread.CurrentThread, but how do we capture the UI thread instead? We need to use SynchronizationContext.Current, which returns the SynchronizationContext that is attached to the current thread.

A SynchronzationContext is an abstraction of a thread that has a mechanism which allows other threads to execute code on it. There are implementations of SynchronzationContext for WinForms and WPF based on Control.Invoke and Dispatcher.Invoke.

Now the second question: How do we pass the captured SynchronzationContext to ContinueWith?

Although the method has some overloads, no one accepts a SynchronzationContext. The appropriate variant in this case takes a TaskScheduler as second parameter.

A TaskScheduler is responsible for executing tasks. It controls when and how (on which thread) a task is run. This sounds as we need a TaskScheduler that executes our task using the captured SynchronizationContext.

Fortunately we don't need to write it ourselves, because it's available already. The static method TaskScheduler.FromCurrentSynchronizationContext returns a TaskScheduler that captures the current SynchronizationContext and uses it to execute the tasks.

So, all we have to do is to pass a second parameter to ContinueWith:

    t => OnTaskCompleted(),

Getting a SynchronizationContext

If run in an OpenEdge application that has a .NET UI, the fixed TaskAwaiter will just work. If it's invoked before the .NET UI gets created or in an old Progress GUI application, you will see the following error message: SynchronizationContext error

Taken literally, I understand that the current thread has a SynchronizationContext, but it can not be used for task scheduling. In fact, SynchronizationContext.Current is returning null, which is the default behavior for a thread.

So, why does the UI thread have a SynchronizationContext after UI creation? The first Control instance creates a SynchronizationContext and attaches it to the current thread. To solve our problem in a Progress GUI application, we can do this by ourselves:

    NEW WindowsFormsSynchronizationContext()).

Creating a Control - for example NEW Form() - would have the same effect, but I think although it's less code, it would look very strange because the intent is not very clear.

Until now I have not investigated using tasks with character or non UI OpenEdge applications (e.g. AppServer).

Using the TaskAwaiter

DEF VAR v-awaiter AS TaskAwaiter NO-UNDO.

v-awaiter = NEW TaskAwaiter().

As you see, using the TaskAwaiter from ABL is pretty easy. We are creating an instance of the TaskAwaiter, subscribe the Done method to the TaskCompleted event and pass the task, which the DoAsync method is returning, to the Await method.

The signature of the Done method:

METHOD PRIVATE VOID Done(sender AS System.Object, args AS System.EventArgs)

Using the TaskAwaiter with less Code

Compared to the one liner in C# this 4 lines contain a lot of boilerplate code, which makes it harder for the reader to see the relevant parts. Thus, I decided to create the following small include:


DEF VAR v-awaiter{&id} AS TaskAwaiter NO-UNDO.

v-awaiter{&id} = NEW TaskAwaiter().

Thats the same code as above except that I am using the built-in preprocessor name &SEQUENCE to make the variable name unique. Without it the include could be used only once per scope.

To make it look more like a statement I omitted the usual .i extension and just named the include file await. Using the include we get the final line of code:

{await someObject:DoAsync() Done}

1 comment:

  1. Hi Fabian,
    Can this be used to run ABL methods asynchronously, or only to run .NET methods asynchronously from the ABL?