Discussions related to Visual Prolog
Martin Meyer
VIP Member
Posts: 375
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: 375
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: 375
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: 375
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: 1513
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: 1513
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: 375
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: 1513
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: 375
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: 1513
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
Martin Meyer
VIP Member
Posts: 375
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

I tried to create an example that adheres to what you said. However, I did not succeed. Here is one of my attempts in which closing a form window is intended to cancel the wait for the result of a calculation:

Code: Select all

predicates     onShow : window::showListener. clauses     onShow(_Source, _Data) :-         _ = future::submit_unit({  :- displayComputationResult_susp() }, executionContext_pool::defaultPool).   facts     waitSetFuture : future{integer ComputationResult} := erroneous.   predicates     displayComputationResult_susp : () suspending. clauses     displayComputationResult_susp() :-         WaitSet = waitSet::new(),         waitSetFuture := WaitSet:submit(myComputation_susp, executionContext_pool::defaultPool),         ComputationResult = waitSetFuture:futureExtension::await(),         stdIO::writeF("Computation result is %d\n", ComputationResult).   predicates     myComputation_susp : () -> integer ComputationResult suspending. clauses     myComputation_susp() = myComputation().   predicates     myComputation : () -> integer ComputationResult. clauses     myComputation() = ComputationResult :-         stdIO::write(">>> myComputation starts\n"),         programControl::sleep(10000),         ComputationResult = 123,         stdIO::write(">>> myComputation ended\n").   predicates     onDestroy : window::destroyListener. clauses     onDestroy(_Source) :-         stdIO::write(">>> Issuing waitSetFuture:cancel\n"),         waitSetFuture:cancel().
The ordinary, non-suspending predicate myComputation performs some lengthy computation.

myComputation is wrapped in suspending predicate myComputation_susp.

Predicate displayComputationResult_susp calls myComputation_susp, awaits the result, and finally displays it. However, if the form window is closed before the result has been calculated, the predicate shall abort waiting for the result.

The idea is that the waitSetFuture:cancel, invoked within the destroy listener, causes futureExtension::await to terminate with an exception, thereby preventing the calculation result from being output. In reality, however, the call to waitSetFuture:cancel does not have the expected effect: Waiting is not canceled and the computation result is output nonetheless.

What is wrong with my code, how to do it correctly?
Regards Martin
User avatar
Thomas Linder Puls
VIP Member
Posts: 1513
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

I believe your code is fine, but unfortunately the promise class has a buggy handling of cancelation. We will have to correct it.
Regards Thomas Linder Puls
PDC
Martin Meyer
VIP Member
Posts: 375
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

I have not made extensive use of multi-threading so far, but I believe I can see that suspending predicates offer great potential. The topic is but complex, and there are lots of details that I, and perhaps other users, have not grasped yet. Adding demos to the VIP examples would be beneficial.

As you have explained, cancellation does not terminate a task. Predicate thread:terminate is not really suitable for terminating a thread either, because -as stated in the discription- it does not deallocate the thread's initial stack. I have no idea about the issues involved in deallocating the initial stack, but it would be useful to have a simple way to terminate a thread resp. the execution of a suspending predicate.

One thing I have been looking for, but did not see in the PFC, is the possibility of submitting predicates that initially suspend, waiting to potentially be executed at a later time.

Domain futureDomains::result{A} offers two functor alternatives, i.e. success_(A) and error_(exception::traceId). If the domain would get a third alternative, such as intermediate_(A, future), it could allow suspending predicates to return an intermediate result along with a future that could be used to continue execution of the predicate. With a fourth alternative failed_ the domain could support suspending predicates of all predicate modes. I know of course that the challenge is not to extend the domain, but suspending predicates' functionality - maybe that is not feasible at all.

I came across a Wikipedia article titled "Continuation-passing style" (I am sure you know much more about all that than written in the article). I noticed that suspending predicates implement parts of the CPS concept described there. I think the missing piece to fully support it, is reflection. This would be if a future exposed a structure enabling code inspection and maybe modification. Perhaps the debug information could be used to generate such code structure. However, I suspect that introducing reflection would be an even greater challenge.

Thank you very much for supporting me!
Regards Martin
User avatar
Thomas Linder Puls
VIP Member
Posts: 1513
Joined: 28 Feb 2000 0:01

Re: Suspension and the GUI

Post by Thomas Linder Puls »

Adding demos is a good idea. Notice also that we already have two substantial real usages to draw them from: the http server is implemented using suspending predicates, and the master/slave principle that launches multiple compilers during a build uses asynchronous operations and suspending predicates.

But first: suspending predicates do not tie to a thread concept. A suspending predicate is a "job" submitted to an executionContext; the thread(s) there execute many such jobs, and a single job can jump between several threads. So terminating a thread is not the right operation at all, since it kills a part of the executionContext rather than the job. If that context is the GUI context, you have killed the GUI thread.

Killing a dedicated thread running a "classical routine" is equally bad: it can leave critical regions locked, plus the initial-stack problem you mention. A dramatic case is killing the thread that owns the critical region used by the memory allocator/garbage collector. Within milliseconds every other thread in the program is blocked.

What you actually want is to stop the execution of a suspending predicate, and that is what cancellation does; my previous description may have been misleading here. Cancellation does not abort the job at an arbitrary instruction, but it does end the job's work: unfinished asynchronous operations are cancelled and raise exceptions in your predicate, yield likewise raises, and these unwind the predicate through normal exception handling, so cleanup runs and the job finishes with an error. Only a long computation containing no suspension points needs isCancelled polling, since there is nothing there to raise in.

An "initially suspended predicate" I read as a lazy one, not started until someone awaits the result. That is reasonable, and it can be built as a library facility on what we have, without compiler or runtime changes.

A clarification on modes: suspending predicates can already be nondeterm today. What is not nondeterm is the future/promise bridge back into normal predicates. A future carries a single completion, so a call producing several solutions has no way to deliver them across that bridge.

For determ this is easily handled: the bridge predicate is a procedure returning an optional{_}. So no failed_ alternative in result{A} is needed for that.

For several solutions, and this is also my answer to intermediate_(A, future), the natural approach is not to make the bridge nondeterm but to let the nondeterm calls stay in the suspending world and insert the solutions into a queue that something else fetches from. What you are describing with intermediate_ is an asynchronous stream of values, and a queue/channel gives you that today without any language change. I would not put it into result{A} in any case: that domain is the completion type of one future, and every existing consumer switches over its alternatives, so an alternative that can never occur for an ordinary future would force them all to handle an impossible case.

On continuation-passing style: the missing piece is not reflection but first-class continuations, i.e. capturing the continuation as a value that can be stored, passed around and resumed more than once. A future is already a continuation, but an opaque one that is resumed once, which is also why it delivers a single result. Reflection is an unrelated thing: Scheme has full continuations and essentially no reflection, Java the reverse. Debug information would not help either, as it describes source structure rather than live runtime state.

Finally, awaiting a future from a normal thread blocks that thread until the result is ready. That may be necessary at the top level, but it is better if you can avoid waiting on the result and instead let the suspending "job" handle the result in some "asynchronous" way (putting it into a queue, posting actions to a window, or something like that).

P.S. if you find this text different (and especially longer) than what I usually write, then it is because it has been "blown-up" by an AI :-).
Regards Thomas Linder Puls
PDC
Martin Meyer
VIP Member
Posts: 375
Joined: 14 Nov 2002 0:01

Re: Suspension and the GUI

Post by Martin Meyer »

Wow, great answer! It provides me with insights. Thanks again!

Long text style is fine. It treats me newbie to suspending predicates more gentle.
Regards Martin