Trampolines and asynchronous execution in Visual FoxPro

Josip Zohil, Koper, Slovenija, july 2013

 

In Visual FoxPro we mostly run sequential code. Sometimes we run asynchronous code using multithreading or other techniques. Asynchronous safe multithread VFP code is difficult to write and debug especially when running multiple concurrent processes. On the other side, long running task implemented in sequential VFP code can block the user screen. In this article I shall show you how to download in parallel multiple WEB resources (hundreds and more) (WEB pages, files, pictures) in  asynchronous mode with responsive screen, using VFP code and a program composition known as a trampoline.

In the last years many programming languages have introduced coroutines (JavaScript, Pyton, C#, GoLang-goroutines). Can we use this programming technique in VFP?

XMLHTTP to download a WEB resource

 

A XMLHTTP object is simple to use, for example:

loRequest= Createobject('MsXml2.XmlHttp')

loRequest.Open("GET",lcUrl[j],.T.)  && async .T.

loRequest.Send()

 

The second command "Open" has a third parameter .T.; the request will be asynchronous. In other words we shall not wait for a response immediately, but we shall continue with the program execution and we shall catch the response later.

Download ten files in parallel

 

We shall create a program to download ten files in parallel, with a possibility to cancel its execution, a time out and an asynchronous execution. We shall use the normal VFP code and compose it, mostly using a do while loop.

*The initialization phase

We create three arrays (or  collections):

n=10  && number of downloads

DIMENSION lorequest[n,4] ,lcurl[n],lccallback[n]

A) an array of four columns:

1) Requests objects,

2) Object created and/or consumed,

3) The eventual error text,

4) In the fourth column we store a request start time in seconds (to calculate the eventual time out).

B) An array of urls.

C) An array of callback functions, that will be executed, when the response will arrive.

 

Function Initialize()

*The URLs to download are:

lcurl[9]="http://server.xxxx.si/yyyyy/potnik.txt" && not existent,timeout

lcurl[2]="http://ceur-ws.org/Vol-584/paper9.pdf"

lcurl[3]="http://www.zipeg.com/zipeg_win.exe"

lcurl[4]="http://www.zipeg.com/zipeg_win.exe"

lcurl[5]="http://www.zipeg.com/zipeg_win.exe"

lcurl[6]="http://ceur-ws.org/Vol-584/paper9.pdf"

lcurl[7]="http://ceur-ws.org/Vol-584/paper9.pdf"

lcurl[8]="http://ceur-ws.org/Vol-584/paper9.pdf"

lcurl[1]="https://www.google.si/#q=%22doevents%22+%2B+%22foxpro%22&ei=8-nBUaLdIceMtAbUr4CwDg&start=10&sa=N&bav=on.2,or.r_qf.&bvm=bv.48175248,d.Yms&fp=cbd62ca8d49537c3&biw=1181&bih=773" && error

lcurl[10]="http://www.zipeg.com/zipeg_win.exe"

*We put the callback function into an array:

FOR m=1 TO n

lccallback[m]="callback()"  && all request have the same callback function

endfor

endfunc

 

The url lcurl[9] does not exist, the XMLHTTP timeout will generate an error. Also the url lcurl[1] will generate an error.

 

All requsts will use the same callback function, that writes the respose body to a file:

FUNCTION callback()  && write a response body to a file

lch=Fcreate("D:/moj/vfpremote1/aaa.txt",0) && all threads are writing to the same file (no locking!!!)

Fclose(lch)

lch=Fopen("D:/moj/vfpremote1/aaa.txt",11)

Fwrite(lch,lorequest[i,1].ResponseBody)

Fclose(lch)

ENDFUNC

 

Theoretically, the ten writes to a file AAA. text can be concurrent and the programmer has to solve the shared access problem (using locking or some other technique). In our trampoline (coroutines) method we have not concurrency problems as all processes execute sequential, all writes are sequential, so there is no shared access and no need for locks.

 

We shall write all the downloads into the same file D:/moj/vfpremote1/aaa.txt to demonstrate, there will be no concurrent access to this file.

 

Download protocol

 

In our example we have to coordinate ten groups of parallel processes:

1) Http requests,

2) Http responses,

3) Bind the requests with the responses (callbacks).

The first two groups of problems we shall resolve using normal VFP do while loops (one request after the other).

The third group of coordination is special:

We are executing asynchronous: we send a request to the WEB server and does not wait for a response, we are monitoring the reaction from the WEB server request: waiting for a response. When it arrives, we consume it using a callback. We have to make the first decision: when to start monitoring. For example, we can run the requests, in this order: req1, req2, req3 ... reqn. They can arrive after a few seconds or minutes in another order: req3,req1, reqn ... req2. We shall start monitoring using this protocol: Start the first request, start monitoring for "its" response, start the second request, start monitoring eventualy for the first and second response and so on. We shall stop monitoring:

- in case of a cancel,

- in case all responses (results or errors) have arrived.

 

Synchronous, asynchronous execution and coroutines

 

After sending  the request (SEND command):

1) In synchronous execution we runs a request and afer that the program blocks waiting for a response, it blocks.

2)In asynchronous execution we runs a request and after that we continue with the program execution. We have more coordination freedom (and problems) on how (and when) to accept the response. For example, if we have a program (function, routine) to send a request and another to receive the response, we have to coordinate the two programs (routines). This coordination is known as coroutine programming.

We call the request and receive functions cooperative because they get scheduled by a routine (scheduler), they just run for a while, then suspend (pause). When a coroutine suspends, other coroutines get to run. When a coroutine gets control back, it continues running from where it left off last time.

In VFP we coordinate generators.

Generators

 

Let us explain generators with some examples (See also: Closures and generators in VFP).

 

1)Function gencounter()

i=i+1

endfunc

i=1

gencounter()   && first call

?i   && display 1

gencounter()  && second call

?i   && display 2

 

The function gencounter is a

a) Closure (it takes the value for x from its environment).

b) It returns nothing (it only executes),

c) After the first call it exits (stop). On the second call  it resumed (new entry point) where it stops the first call. It resumed (entry point) with  data saved on a first entry. We can see it as a function with multiple entry and exit  points (yield in JavaScript, Pyton and C#).

 

When a function gencounter is called, it always starts its execution with its first statement, however, the restarting point is where gencounter was suspended previously. It takes a previously saved state!!!

With appropriate management (coordination) of this function we can "simulate" it has multiple entry and exit points.

Generator lets you write a piece of code generating a sequence of results, pausing its execution for each result and restarting from its paused state to compute the next result.

 

Coroutine and trampoline

 

The idea of coroutines is to suspend the current program to give the control to another program, until that other routine gives the control back. Normally a program has one entry point (its start statement) and one exit point (the last executed statement, mostly a return). You can coordinate generators to simulate multiple entry and exit points. For example, in manipulating the data tables , we use commands like these:

 

program coordinator()

select 0

use table1

first()

go bottom

select 0

use table2

skip

first()     && we are at the bottom of table1

go top

second()

skip

first()

go top

endfunc

 

Function first()

select table1

endfunc

Function second()

select table2

endfunc

 

This program coordinates two work areas of two open tables. We are jumping between work areas using the functions first() and second(), entering and leaving  a work area multiple times. We have entered the work area table1 three times and exited it two times. When we return back to table 1 (first()), it preserves its record position (at the top).

We can read the function coordinator in this way: we call a function first(), after that it pauses (suspends), we call the function second(). When it stops, we call first() and it resume from the record position it was before the pause.

 The functions first() and second() return nothing, they only change state (work area).

The idea is to jump between programs (routines) the same way as we jump between tables (when we reenter the program it preserve its state: it saves the control state). Functions (routines) that have multiple entry and exit point are called coroutines. In VFP you can create programs with these characteristics using the command doevents, creating generators and managing them using trampolines (functions to coordinate and/or dispatch generators).

Generators are not able to suspend and resume. They are like a command select (it can switch between work areas multiple times, but it needs a manager (coordinator) such as program coordinator(). Inside it the work area bounce between table1 and table2: The program coordinator() coordinate this bouncing (it is a trampoline).

The same way as we coordinate the work areas we can coordinate generators: the coordination coroutine is called trampoline.

 

FUNCTION genforce(m)

*force a generator to execute m times

i=0

j=0

DO WHILE .t.

j=j+1

gencounter()

IF j>=m

EXIT

endif

enddo

endfunc

 

Mostly we use generators by forcing them, for example executing them m times. The function genforce is an example of calling (forcing) gencounter m times.

 

FUNCTION trampoline()

*coordinate execution of two coroutins and state

*we push on a trampoline two coroutins and coordinate theirs execution

*the state is managed without programmer intervention.

i=0

gencounter()  && start executing, activates a generator, 0-1

* gencounter is suspended, we can switch to other computation, call other functions, control is returned to the trampoline

*state, i is "hidden"

y=3+2  && other computation (can be ignored)

genforce(5)   && pass control to genforce() 0-5

* inside genforce is gencouter (another coroutine)

z=y**2   && other computation (can be ignored)

gencounter()  && pass control back to gencounter(),5-6

endfunc

 

We coordinate generators by "pushing them on a trampoline". The function trampoline coordinates the execution of two coroutines: gencounter and genforce. It starts with i=0, call gencounter(),(stops gencounter()) pass control to genforce(5) and after that resumes gencounter(). The trampoline is a caller that calls repeatedly to activate each generator. Behind the scene it manages also state (i). It generates i=1, after that start with i=0 and get i=5 and the last gencounter increase i=5 to 6.

You have to observe how gencounter change state: it changes state S->S1, suspends, resume and change state S1->S2 and so on.

Inside a coroutine genforce there is another generator gencounter. Normally, a function that receives execution control keeps it until it returns. But with the trampoline and generators coordinations, the execution control can be given up in the middle of the routine and later restored.

 

Note. Genforce(m) is a composition of multiple call to a generator. This is a block of code we force to execute without suspension. It is a block of code that is running on the "computer" for a time slice!!! In this time interval we can't suspend id. For large n we create multiple chunks of this big computation, for example, we create two do while loops, one inside the other. After each genforce we can suspend or switch program flow.

Genforce(m) can be interpreted also in this way: We jump (bounce) on a trampoline, every jump is higher; extremely high jumps can distruct the jumper or the trampoline. The bounce of the coroutine genforce(m)  on a trampoline depend also from m: it must be bounded by a "reasonable" number.

XMLHTTP component

 

For every web request we shall create an XMLHTTP object and obtain an array of ten objects. The function (coroutine) genrequest() will create this object:

 

Function genrequest()

*generate a request and pass control to receivedatatill

j=j+1  && j is taken from the environement

k=k+1  && k is taken from the environement

      loRequest[j,1] = Createobject('MsXml2.XmlHttp')

      loRequest[j,1] .Open("GET",lcurl[j],.T.)  && async .f.

      loRequest[j,4]=Int(Seconds())  && to calculate timeout

      loRequest[j,1].Send()

      loRequest[j,2]=.T.  && request initiate

receivedatatill()

If j=N  && sent all requests

      gen=.T.

Else

      gen=.F.  && continue

Endif

Endfunc

 

A function genrequest() is a generator. It:

a) creates a XMLHTTP object, put its reference and other state data in the loRequest array. Genrequest is a generator, it produces a lorequest object, sends a request in an asynchronous way  and updates state (j=j+1,k=k+1, gen).

b) Try to receive responses from the web using the function receivedatatill(). It eventually receives (consume) the responses.

c) Update the state variables j,k, gen and con.

The function genrequest is a generator and a consumer. It produces requests and eventually consumes the received responses. After each generated request it passes control (call) to the function receivedatatill(), executes it and return control to genrequest.

We are controlling ours' program executions using a procedural programming style (not an event driven).

We have two coroutines (coordinated routines): genrequest() and receivedatatill(). The function genrequest has an additional exit and entry point: it passes execution (exit point) to the function receivedatatill and when it finished execution the genrequest coroutine resumes (entry point).

We shall present later the management of the coroutine genrequest.

Though the processes generated by the functions genrequest and receivedatatill are concurrent, they cannot cooperate. Their execution is thread safe. There can be no race conditions nor deadlocks. That in turn means we need no locking, transactions, nor other similar blocking mechanism.

The producer coroutine (genrequest) yields values which the consumer coroutine (receivedatatill) awaits to process. It is easy to coordinate the two coroutines, because their two suspension points (exit of a generator) are similar.

 

FUNCTION receivedatatill()

i=0

Do While  Not lcstop && a routine to wait responses or a cancel message

      i=i+1      && next bounce (on a trampoline)

      If loRequest[i,2]

*we do some work, then yield the asynchronous actions back to the program flow controller

            GetData()   && if possible consume the response

            If Not con

                  loRequest[i,2]=.F.  && responses consumed

                  loRequest[i,1].abort()

            Endif

      Endif

      If i>=j Or k<=0  && try to receive all i<j - number of requests sent

            Exit

      Endif

Enddo

ENDFUNC

The function  receivedatatill() loop to receive the responses for sent (i<j) and not received (loRequest[i,2]=.F.) responses. The function getdata() consume the received responses. If it receives a stop message it stop execution.

State

 

In all presented functions we are managing state (lorequest, lcurl, j, k, gen, con), but without explicit passing of state objects and variables. All the functions are without parameters. We pass them implicitly (behind the scene) using the closure technique and generators.

Note. Normally, for example in multithreading code, a callback function has two arguments, one of them is a state object (see C#).

The producer produces data, pushing it on the "environment", and the consumer consumes it, shifting it off the "environment" again. The state can be an array, a collection, a table, a variable or all of these.

The state variables are like "hidden" global variables: we can access them from "everywhere" and we can "hide" them inside the functions (coroutines). Because only one coroutine is active  at specific points, there are no share state between the coroutins. VFP code is safe.

Note. In VFP we mostly manage state using tables: save, read, modify.

 

Receiving data

 

We are running asynchronous code; we are waiting for a response that will arrive "in the future". This process is coordinated using the function receivedata:

 

FUNCTION receivedata()

Activate Screen

      i=0

      Do While .T.  && try for all connections, the trampoline inside

            i=i+1

            Activate Screen

            If lorequest[i,2]  && not consumed connection (response)

* a jump to catch a response

                  GetData()     && receive and possible save data   (consumer)

                  If Not con       && received data for connection number i

* on successful jump

                        lorequest[i,2]=.F.  && connection consumed

                        lorequest[i,1].abort() && abort the xmlhttp object

                  Endif

            Endif

*suspend, after each getdata transfer control to a VFP event loop

            DoEvents  && consume events in the VFP event loop

*resume

            If i>=N Or K<=0

                  Exit

            Endif

            If lcstop  && catch the stop message

                  Unload(lcstop)

                  Return

            Endif

      ENDDO

      endfunc

 

In a loop, the function receivedata:

1) receive data, for not consumed requests (lorequest[i,2]=.T.) passing control to the function getdata,

2) suspend execution and pass control to the VFP event loop (doevents),

3) stop execution (lcstop=.T.).

This function is a coordinator: it suspends, resume and stop  program execution. Normally, we can't suspend and resume a generator with code "inside it". In such cases, we create a new program (routine, trampolin) that resume and suspend a generator.

 

To catch a response we create a function getdata(), that will monitor a response:

 

Function GetData()  && a generator (consumer)

con=.T.  && "return" value

*timeout

If Seconds()-loRequest[i,4]>timeouts  &&timeout 2 seconds

      loRequest[i,3]= " Timeout"+Str(timeouts)+ lcurl[i]

      Activate Screen

      ?i,loRequest[i,3]

      k=k-1  && decrease the number of requests to consume

      con=.F.

Endif

*readystate

If loRequest[i,1].ReadyState = cnReadyStateComplete And loRequest[i,2]

*suspend

      DoEvents  &&  jump to the event queue and return back

*resume

*your typical function will have both a normal and an exceptional continuation (callback)

      If loRequest[i,1].Status=200  && catch errors

*run a callback

            Evaluate(lccallback[i]) && call a callback

            Activate Screen

            ?loRequest[i,1].Status,"false", "k:",k,"i:",i,lcurl[i]

      Else

*error

            loRequest[i,3]= Str(loRequest[i,1].Status)+" "+loRequest[i,1].statusText && put the error message in the array

            Activate Screen

            ?i,loRequest[i,3],lcurl[i]

      Endif

      k=k-1

      con=.F.

Endif

*DOEVENTS  && suspend and resume (consume the events in the VFP event loop)

Endfunc

 

The function getdata is a generator function: it takes the variables from its environment, modify them (change state) and exits. It does not return anything!!! On the next call, it starts where it "suspends" (it has a new entry point). Its manager bounces it on the trampoline (We shall present this later).

The function getdata() monitors the

1)timeout (SECONDS()-lorequest[i,4]>timeouts ) ,

2) ready state (lorequest[i,1].ReadyState = cnReadyStateComplete And lorequest[i,2]) for active request objects (lorequest[i,2]=.T.),

3) errors (lorequest[i,1].status=200)

and

4) pass execution (exit point) to VFP event loop using the command doevents. When the VFP consumes the events in its event queue it return control to the function getdata (entry point). The function getdata is a coordinator: it coordinates the process of receiving data and the execution of the VFP event loop.

For each state variable i (request id) it modifies appropreatly the state variables (lorequest, j,k,gen,con).

Note. Getdata() has multiple exit point (timeout, readystate, errors). For example on readystate=4 we have a continuation to be executed (callback). Theoretically this callback can be another trampoline (for example to write to a local file in asynchronous way).

 

Doevents

 

The command DOEVENTS suspend the execution of the function getdata, pass control to VFP to consume the events in the VFP event loop. After that it returns control to the function getdata(). It has an exit and entry point. When it exits it "save" state and it resume from the saved state.

The command DOEVENTS stop "the bounce on the trampoline" and after a time slice resumes. The responsiveness of the VFP screen depend on the frequency of call to DOEVENTS. Let us say this in other words: The frequency of call to doevents depend on the time consumed to execute the getdata() function. This time interval has to be short: we have to manage short running tasks.

Note. A long running VFP SQL query on a fast network is CPU bounded, so we can't suspend and resume it: the VFP application become unresponsive.   

 

Generator and doevents

 

The function (coroutine) getdata has two suspension points:

1) doevents suspend its execution (exit point), pass control to the VFP event loop and return back to resume getdata execution (entry point) . It has an additional exit and entry point.

2)The getdata function is a generator. It runs once and generate a new state. On the next execution it takes the generated state. We can see this as a new entry point (with new values). So we say the function getdata has multiple entry and exit points.

We can manage this function using its entry and exit points. Most entries and exit points the function has more manageable it is.

Note. You can look at the function's (coroutine) suspensions as its logic. When you add it a new suspension, you add it a new point where you can inject new logic (for example, change the program flow). In event driven programming you use the commands BINDEVENT and RAISEEVENT to change the program flow. In croutines programming you use suspension points, generators and trampoline to manage (and change) the program flow. Why we do this? Using events is difficult to manage asynchronous execution, it is easier using cooroutines, generators and trampolines. 

Suspensions and resumptions are a series of alternating bounces and pauses, during the pauses the trampoline turns control over to us. The coroutine getdata don't block and wait for the response to become available. It  allow other processes (threads) to run on the CPU. As a result, a single machine can handle millions of connections and/or coroutines. The computer is unresponsive "because of blocks and not execution".

 

Coordinator - trampoline

 

We coordinate all presented functions using a function (coroutine) CLICK(). It is a click method of a command1 button on a VFP form:

Function Click()

*program flow controller,  trampolines handler, dispatcher

*initialization

Activate Screen

Thisform.stop=.F.

Set Proc To factory Additive

N=10

Dimension lorequest[n,4] ,lcurl[n],lccallback[n] && an array of urls and callbacks

*object,active,response,timeout,url

initialize()

timeouts=5

cnReadyStateComplete= 4

K=0  && number of generated requests and consumed responses (consumed, timeout or error)

j=0

gen=.F.

con=.F.

Thisform.stop=.F.

lcstop=Thisform.stop

Do While .T.   && bounce on a trampoline and send a request (genrequest)

      lcstop=Thisform. stop

* we put a genrequest coroutine (function) on a trampoline

      genrequest() &&generate and send requests, check for response and consume first arrived responses

      If  gen

*we have generated all n request, exit trampoline (stop bounce)

            EXIT && remove the genrequest from the trampoline

      Endif

* suspend, a trampoline switch execution to a VFP event loop

      DoEvents   && suspend and consume the events in the event queue

* resume, switch execution to this coroutine

      If lcstop && catch the stop message

            Unload(lcstop)

            Return

      Endif

ENDDO

*second part, switch execution to the consumer coroutines

Do While .T. 

      lcstop=thisform.stop

      * bounce

      receivedata()  

      If K<=0

            EXIT && remove a coroutine the trampoline

      Endif

Enddo

Unload(lcstop)  && clear

?"end"

 

 

A handler routine, called a trampoline (dispatcher, sheduler), initialize state and loops, repeatedly calling the coroutine and assigning the result back to the coroutine itself until it generates all requests. The code is written in a linear style; it is just a straight line of function calls. The program is organized around two scheduling loops.

 

 It loops, repeatedly calling the gerequest and assigning the result  back to the environment until  all requests are sent.  On every bounce it sends a request, tries to receive responses for previously sent requests and runs doevents to consume eventual events inside a VFP event loop (for example, clik in a textbox on a VFP form). The bounce stops when the program sends all requests. Programs control does jump from the heart of one loop to the other and back again without disturbing the state of either loop.

The first part of the program is an example of composeable flows. Inside of genrequest we have composed two flows: the "request flow" and the "receive flow" (getdata function).

The second part of the program is dedicated to the responses. Some responses have been received in the first part of the program. We are receiving the remaining responses. We will receive them in a do while loop. It will loop (bounce) till we receive all responses (or errors) or receive a stop message.

Inside the function receivedata(), we run a doevents command to suspend execution and pass program control to a VFP event loop.

The second part of the Click function can be implemented using only one do loop (inside the function receivedata()). We have used two, so we have a possibility to inject an additional suspension.

In this program we have composed the request and response flows in a sequential way: we have started with a request flow and continue with a response. We are writing asynchronous code in the same way as synchronous. We have transformed our (serial) code by injecting suspend and resume points (yield construct in JavaScript, C#, F#, Pyton).

Note. We can look at this suspend/resume operations as a special kind of functions binding (before and after the suspend/resume).

We can write asynchronous code (code for asynchronous execution) in the same way as sequential. We transform a sequential code from synchronous to asynchronous by modifying some commands and  injecting suspend and/or resume "transformations" (doevents or a trampoline). The transformed code executes asynchronous.

Note. Is this a monad? The injection can be seen as a transformation.

 

Unload

 

The unload function abort the created objects.

Function Unload(lcstop)

Activate Screen

?"Stopped",lcstop

For m=1 To N

      loRequest[m,1].abort()

Endfor

Endfunc

 

In the function click, after the initialization phase, the state is hidden. The only "disturbing" element is Thisform.stop.

Note. Thisform.stop is a "message" generated by the object form and  received by the generators manager. In another article we shall show how to "hide" this state variable (message). Our program flow is only partially event managed; it is managed by a manager Click, which coordinate the web download with the VFP event loop and the thisform.stop member.

The functions Click, Genrequest and Receivedata are reusable components: they are without arguments, they have only logic and state. We pass them the initial state. This is ours arguments: array of urls' and array of callbacks'. All the "arguments" of ours' functions are in the function initialize; it is like a settings file. For example, to fetch thousands of web resources, we modify only the function initialize.

The error can be registered also by putting all errors in a string. Managing errors is simple.

 

Test

 

We put all the programs in a procedure Factory. The click function we put in a command button (Command1) click event. We run the form, we click on the Command1 command button and we start the requests. After that we can insert data in the form text boxes. When the responses arrive, we see the messages on the screen.

 

Conclusion

 

We have used normal VFP commands and relatively unusual coroutins cooperation (function composition). Coroutines are functions that can be paused and later resumed. All the  functions we have used are closures. They permit us to manage state in an easy way. The generators coordinated with trampolines allow us to suspend and resume a process. We coordinate generators using  trampolines: we make some bounces, capture state and then pause, after that we restore state and continue bouncing and so on. It is not difficult to manage such coroutines also in cases when we have millions of processes (requests).

Another way of managing the coroutines is with a state machine.

We have presented an example of a trampoline method in programming a client. This kind or programming can be used also on the WEB server in asynchronous management of multiple thousands of client requests (green threads, lightweight processes).

Let us resume. In our code ( about hundred lines) we have asynchronous managed multiple web request, with timeouts and cancelation and without locking and other thread safe problems. We have written code in a serial way (normal VFP code). In such a way we can also read the code.

It is not difficult to write and maintain asynchronous code in VFP.