Discussions related to Visual Prolog
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Suspension and the GUI

Post by Martin Meyer »

Hello Thomas,

in a new GUI project, I insert the following for onFileNew:

Code: Select all

predicates     onFileNew : window::menuItemListener. clauses     onFileNew(_Source, _MenuTag) :-         _Future = pfc\asynchronous\future::submit_unit(workLots),         stdIO::write("submitted").   class predicates     workLots : () suspending. clauses     workLots() :-         std::repeat(),         fail.       workLots().
When I execute the project and click on the "New" icon, the text "submitted" is not displayed, and the task window becomes unresponsive.

However, my intention is to allow the user to continue working with the GUI while workLots runs in the background.

I suspect I need to use a different execution context to achieve the desired behavior, but I cannot figure out how to do it. Please help.
Regards Martin
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

That way it works better in my tests:

Code: Select all

predicates     onFileNew : window::menuItemListener. clauses     onFileNew(_Source, _MenuTag) :-         _Future = pfc\asynchronous\future::submit_unit({  :- workLots_susp(0) }),         stdIO::write("submitted\n").   class predicates     workLots_susp : (integer Cnt) suspending. clauses     workLots_susp(Cnt) :-         stdIO::write(Cnt, "\n"),         pfc\asynchronous\executionContext::yield(),         Cnt1 = if Cnt = upperBound(integer) then 0 else Cnt + 1 end if,         workLots_susp(Cnt1).
So, does workLots_susp actually need to be repeatedly suspended to keep the task window responsive? Is the above pattern (i.e., inserting calls to yield) suitable for use in my "real" applications, or are there other aspects to consider? Do I need to worry about execution contexts, or is that usually unnecessary?
Regards Martin
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

In my real application, the long-running process that I want to run in the background is a call to an Earley parser.

If I would insert calls to yield into the parser's routines, I needed to turn at least the parser's top-level routines into suspending predicates. However, that does not seem elegant to me. Is there a way to keep suspension issues outside of the parser's code while still ensuring the responsiveness of the task window?
Regards Martin
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

I believe I have found a "pattern" that meets my requirements: In below code code, the suspending predicate workLots_susp runs in the background while the task window remains responsive. workLots_susp acts as a wrapper around an ordinary, non-suspending predicate myLongRunning, which among other things interacts with the GUI:

Code: Select all

predicates     onFileNew : window::menuItemListener. clauses     onFileNew(_Source, _MenuTag) :-         _Future = pfc\asynchronous\future::submit_unit({  :- workLots_susp() }, pfc\asynchronous\executionContext_pool::defaultPool),         stdIO::write("submitted\n").   class predicates     workLots_susp : () suspending. clauses     workLots_susp() :-         myLongRunning(0).   class predicates     myLongRunning : (integer Cnt). clauses     myLongRunning(Cnt) :-         if Cnt mod 10000000 = 0 then             stdIO::write(Cnt, "\n")         end if,         Cnt1 = if Cnt = upperBound(integer) then 0 else Cnt + 1 end if,         myLongRunning(Cnt1).
Thus it seems, Dos and Don'ts to keep the task window responsive while running a suspendig predicate in background, are:
  1. Use a thread pool as the execution context, typically defaultPool!
  2. Avoid GUI overload caused by excessively frequent calls to it!
Does my pattern take the right approach, or what should be improved?
Regards Martin
User avatar
Thomas Linder Puls
VIP Member
Posts: 1510
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

Hi Martin,

Suspending predicates are primarily intended for dealing with asynchronous operations, where code needs to "wait" for an asynchronous call to complete.

I/O operations can be performed in two ways:
  • Synchronous: The thread performing the operation waits (blocks) until the operation has completed. Execution then continues with the result available.
  • Asynchronous: The operation is initiated, but the thread that starts it continues doing other work. Once the operation completes, something else must handle the result.
There are several ways to handle the results of asynchronous operations. Suspending predicates allow the programmer to write code as if it were synchronous, while the actual operation is performed asynchronously. When an asynchronous operation is initiated, the predicate suspends. Once the operation has completed, it resumes execution. The compiler generates the special code required to support suspension and resumption, and execution takes place within an execution context.

An execution context has one or more threads that execute submitted code. Once an asynchronous operation is initiated, the predicate is suspended, allowing the thread to execute other work that has been submitted to the same execution context. When the asynchronous operation completes, a thread in the execution context resumes the predicate, continuing from where it was suspended.

We have made it possible to use the GUI thread as an execution context, which is very convenient because the code can update the GUI directly. However, that execution context has only a single thread, the GUI thread. While that thread is executing long-running computations, no other GUI events can be processed. In other words, event handling stops while a suspending predicate is actively executing ordinary code. While the predicate is suspended, however, the GUI thread is free to process other events.

yield is an "artificial" asynchronous operation. It suspends and then immediately resumes execution, temporarily releasing the thread so it can execute other pending work before returning to the code after the yield call.

An executionContext_threadpool is an execution context with a pool of worker threads. Initially, there are no threads running. When a predicate is submitted, or when a suspended predicate resumes, a worker thread may be created if no idle thread is available. Threads that remain idle terminate automatically. Since threads are created and destroyed as needed, work performed in one thread does not block other work from running. If a maximum thread limit is configured, additional work may remain pending until a thread becomes available.

However, you cannot directly perform GUI operations from a suspending predicate running in a thread-pool execution context, because most GUI operations must be executed by the GUI thread. You can use postAction to post work to a window from any thread. If you need a result, you can submit a suspending predicate to the window's execution context and await the result.
Regards Thomas Linder Puls
PDC
User avatar
Thomas Linder Puls
VIP Member
Posts: 1510
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

Alternatively, you can of course just start a thread, and not use the "fancy" suspending predicates at all.

Both methods will work:
  • A certain task can be implemented using a dedicated thread. This can rather simple to deal with.
  • But if you have many tasks, it may end up being be more complex to have dedicated threads. And if the threads can actually be suspended then using a pool seems better.
Regards Thomas Linder Puls
PDC
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

Thomas, thank you very much for the detailed explanation!

In the end you write:
If you need a result, you can submit a suspending predicate to the window's execution context and await the result.
How exactly to do that? Is it supposed to work for example as follows?

Let's say the window is myForm. In the implementation, I add a fact that grabs its execution context:

Code: Select all

implement myForm inherits formWindow     open core, vpiDomains   facts     executionContext : pfc\asynchronous\executionContext := pfc\asynchronous\executionContext::threadExecutionContext.
In the window's interface, I expose the fact via a property:

Code: Select all

interface myForm supports formWindow   properties     executionContext : pfc\asynchronous\executionContext (o).
In the task window, I use the window's executionContext property:

Code: Select all

predicates     onFileNew : window::menuItemListener. clauses     onFileNew(_Source, _MenuTag) :-         MyForm = myForm::display(This),         _Future = pfc\asynchronous\future::submit_unit({  :- workLots_susp(MyForm) }, pfc\asynchronous\executionContext_pool::defaultPool).   class predicates     workLots_susp : (myForm MyForm) suspending. clauses     workLots_susp(MyForm) :-         myLongRunning(MyForm, 0).   class predicates     myLongRunning : (myForm MyForm, integer Cnt). clauses     myLongRunning(MyForm, Cnt) :-         if Cnt mod 10000000 = 0 then             RctFuture = pfc\asynchronous\future::submit({  = MyForm:getOuterRect() }, MyForm:executionContext),             Rct = RctFuture:getResult(),             stdIO::write(Rct, "\n")         end if,         Cnt1 = if Cnt = upperBound(integer) then 0 else Cnt + 1 end if,         myLongRunning(MyForm, Cnt1).
That way it worked in my tests. But is it really correct and best practise?

Is the extra fact/property in myForm actually necessary, or is there a simpler way to retrieve the execution context of myForm?

Should the fact perhaps have attribute immediate to ensure it is provided with the correct context?

I assume the call to stdIO::write does not need to be enclosed in postAction, since I think I remember you once mentioning that writing to the message window is thread-safe. That correct?
Regards Martin
User avatar
Thomas Linder Puls
VIP Member
Posts: 1510
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

There are several things here.

Lets us start with the main question how to get something back from the gui thread. It is correct to submit a suspending function to a gui execution context. And then retrieve the result.

Here you use getResult which means that your worker thread will block and wait for the result.

If myLongRunning was also suspending then you could have awaited the result instead:

Code: Select all

class predicates     myLongRunning : (myForm MyForm, integer Cnt) suspending. clauses     myLongRunning(MyForm, Cnt) :-         if Cnt mod 10000000 = 0 then             RctFuture = pfc\asynchronous\future::submit({  = MyForm:getOuterRect() }, MyForm:executionContext),             Rct = RctFuture:futureExtension::await(),             stdIO::write(Rct, "\n")         end if,         Cnt1 = if Cnt = upperBound(integer) then 0 else Cnt + 1 end if,         myLongRunning(MyForm, Cnt1).
In vip 11 await is stowed away/hidden in futureExtension so you will either have to qualify explicitly (like I have done above) or open futureExtension (which is preferably, I think).

In future versions this will be structured a bit differently, but the idea is the same.

When you use await the execution will suspend until the result is ready instead of blocking. If the gui request is a simple as here there is not much gained in suspending instead of blocking (actually it may be worse) but we are talking principles here.

Next issue is about gui execution contexts. An execution context is very simple, it only have a complete predicate:

Code: Select all

predicates     complete : (runnable Action).
This predicate must execute the Action in the execution context. For a threadpool execution context the Action is put in a queue and eventually a thread in the pool will dequeue it and run it.

A gui execution context will simply use postAction so the Action is executed by the gui thread.

However postAction can only be used on a window that actually exist, and anything that is posted to a window is lost if the window is destroyed before the post event comes through to the window.

In some contexts it may be good if the Action simply disappear when the window is destroyed. But I think it can easily be very complex. The task window is long living and I think it is safest to use that. I didn't want to pollute everything with asynchronous stuff, so windows don't have a executionContext property. But it is very simple to create one from any window using this code (which I think we will add somewhere in the async department):

Code: Select all

class predicates     executionContext : (window Src [this]) -> pfc\asynchronous\executionContext ExecutionContext. clauses     executionContext(Src) =         implement : pfc\asynchronous\executionContext             complete(Action) :-                 Src:postAction(Action).         end implement.
Using this code will control very precisely which window that it the context (and I think you should use the task window).

The final thing is that in this particular example you use a form, and the code will break if the form is closed. I am aware that the example is illustrative, but it also illustrates that you need to consider lifetime a lot. Both lifetime of windows but also of your background tasks.
Regards Thomas Linder Puls
PDC
Martin Meyer
VIP Member
Posts: 372
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

Thanks again, Thomas for the insightful explanation!

You pointed out that
the code will break if the form is closed
That got me thinking of how to abort the execution of a submitted suspending predicate. As first attempt to abort one, I just inserted a call to cancel into the example:

Code: Select all

predicates     onFileNew : window::menuItemListener. clauses     onFileNew(_Source, _MenuTag) :-         MyForm = myForm::display(This),         Future = pfc\asynchronous\future::submit_unit({  :- workLots_susp(MyForm) }, pfc\asynchronous\executionContext_pool::defaultPool),         Future:cancel().    % this is the inserted call to cancel   class predicates     workLots_susp : (myForm MyForm) suspending. clauses     workLots_susp(MyForm) :-         myLongRunning(MyForm, 0).   class predicates     myLongRunning : (myForm MyForm, integer Cnt). clauses     myLongRunning(MyForm, Cnt) :-         if Cnt mod 10000000 = 0 then             RctFuture = pfc\asynchronous\future::submit({  = MyForm:getOuterRect() }, MyForm:executionContext()),             Rct = RctFuture:getResult(),             stdIO::write(Rct, "\n")         end if,         Cnt1 = if Cnt = upperBound(integer) then 0 else Cnt + 1 end if,         myLongRunning(MyForm, Cnt1).   class predicates     executionContext : (window Src [this]) -> pfc\asynchronous\executionContext ExecutionContext. clauses     executionContext(Src) =         implement : pfc\asynchronous\executionContext             complete(Action) :-                 Src:postAction(Action).         end implement.
However, the cancel does not have the effect I expected. The execution of workLots_susp\1 is not aborted but continues.

How can the execution of a submitted suspending predicate actually be aborted?
What purpose does a cancellation context serve, and how can I set one up?
Regards Martin
User avatar
Thomas Linder Puls
VIP Member
Posts: 1510
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

Cancellation is "collaborative". The "task" will not be terminated.

But all asynchronous operations will be cancelled and the suspending predicate will receive an exception from that operation. Predicates like yield will also raise an exception.

For a graceful handling you can check/poll for cancellation using: pfc\asynchronous\executionContext::isCancelled.

You can of course also use your own termination signals instead. Which may give you even more control over the "shutdown".

Cancellation is supposed to handle this scenario well: To present something in a window we need to collect a lot of data from various sources. We collect it in parallel using suspending predicates and asynchronous IO operations. But before things has finished the user closes the window (typical reaction if it takes too long time). On the close we cancel the task we have submitted. And this will automatically cancel all unfinished operations, and the code will receive exceptions which will prevent it from starting next steps in the task.

I will have to admit that much of this is also new land for me. I have implemented the runtime support based in my imagination of how this must work. But I have always been sure that things would need updates when we gain more experience. The http server is implemented using suspending predicates and we use that heavily here. The master/slave principle which we use to launch multiple compilers during build also uses asynchronous operations and suspending predicates.
Regards Thomas Linder Puls
PDC