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>
:
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:
- Implementing a .NET interface from an ABL class.
- Creating an ABL class which inherits from a .NET class.
- 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:
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:
- How to capture the right thread?
- 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
:
task.ContinueWith(
t => OnTaskCompleted(),
TaskScheduler.FromCurrentSynchronizationContext());
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:
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:
SynchronizationContext.SetSynchronizationContext(
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().
v-awaiter:TaskCompleted:Subscribe(Done).
v-awaiter:Await(someObject:DoAsync()).
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:
&SCOPED-DEFINE id {&SEQUENCE}
DEF VAR v-awaiter{&id} AS TaskAwaiter NO-UNDO.
v-awaiter{&id} = NEW TaskAwaiter().
v-awaiter{&id}:TaskCompleted:Subscribe({2}).
v-awaiter{&id}:Await({1}).
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}
Hi Fabian,
ReplyDeleteCan this be used to run ABL methods asynchronously, or only to run .NET methods asynchronously from the ABL?
Sands Casino & Hotel in Kingston
ReplyDeleteView 1xbet korean all information and sign up to our site. Sands 샌즈카지노 Casino & Hotel. Address: 6406 South deccasino Kingston Highway, Kingston, Ontario, Canada. Map.