Coordination of  VFP loops

Josip Zohil, Koper, Slovenija, July 2013

 

We use the VFP loops also to traverse the VFP tables by  listing them, replacing values and accumulating. Sometimes we use a SQL statement for such a task, but long running SQL commands can »block the computer«. Also the execution of programs based on VFP loops can block. In this article I shall show how to prevent these blocking operations and create your application reactive.

 

Accumulating loops

 

Let us explain a »classic« VFP program of accumulation values and adding the results in another table. The »calssic« VFP program is:

Function accum()

createtab()

Select test

GO top

Do while not eof()

Lcid=id

Lcacc=0

Do while id=lcid and not eof()

lcacc=lcacc + quantity

Skip

Enddo

SELECT testacc

APPEND BLANK

replace id WITH lcid,quantity WITH lcacc

SELECT test

ENDDO

Endfunc

FUNCTION createtab()

CREATE TABLE test (id I,quantity N10)

APPEND BLANK

replace id with 1, quantity with 5

APPEND BLANK

replace id with 1, quantity with 3

APPEND BLANK

replace id with 2, quantity with 6

Copy stru to testacc

Select 0

Use testacc

Endfunc

A function accum is all serial code. A computer executes it line by line. There is no parallel, asynchronous or concurrent execution. Let us observe the variables (we call them state variables or state). Our variables are:

1) lcid and

2) lcacc.

3) We have also an array of values id and quantity stored in the tables test and testacc.

A function createtab creates the initial values (state) for the two tables. A function accum manipulate this initial state and give us:

1) a not modified table test,

2) a modified table testacc with the accumulated values,

3) we have used (mutated) the variables lcid and lcacc only for intermediate computation.

Pay attention to the composition of the two loops: the second is inside the first. The program executes the first loop, »pause« it, runs the second, »pause« (terminate) it, continue with the first and so on. It is like an orchestra of two musicians: The first musician plays, pauses, the second musician plays, pauses, the first musician plays and so on. A bandmaster has to work only on the points of the pauses of the two players. We programers don't like these »points of pauses«, because we have to write additional code to manage the transition from »the first loop to the second and back to the first. As a bandmaster we shall pay attention to these »pause« points, we shall manage them.

 

SQL state transformation

 

We can look at a function accum as a transformation from a table test (initial state) to a table testacc (endstate) and write this transformation as a SQL statement:

Select id,sum(quantity) as quantity from test into table testacc group by id

Here is another SQL version:

Lcfields=« id,sum(quantity) as quantity«

Lctable=«test«

Lcgroup=«id«

Lcres=«testacc«

Select &lcfields from &lctable into table &lcres group by &lcgroup

 

Note. This SQL and can run slower than the first query.

This SQL statement is a »composition« of commands (or groups of commands) in a function accum and give us the same results as a function accum (we run our code as a single user). The SQL statement is without the state variables lcid and lcacc.

A second version of a SQL statement is pure logic. It is a declaration to the compiler to execute the logic. The state is not visible. When we give this SQL statement the four variables, it transforms the input values to output. It is a state transformer function which will give you the final result once you give it the initial state.

Note. Behind the scene the SQL locks the tables and navigates on an immutable data structure, our state is protected. But attention, a lock is a blocking operation and SQL is also.

Note. We can compose multiple SQL statements. For example, the result of a SQL query is a table, on it we can run another SQL query and so on. Something similar we can do with VFP loops and also more.

 

Coordination of multiple threads

 

Let us rewrite the function accum as a coordinator of multiple threads of execution (processes). We shall introduce new functions (names for block of code) to ease our explanation.

FUNCTION genproduce()  && a generator that produces values

Do while id=lcid and not eof()  (transform a state of lcacc multiple times)

  Genadd()

ENDDO

Endfunc

Function genadd()  &generator (transform state of lcacc)

 Lcacc=lcacc+quantity

 Skip

endfunc

FUNCTION genconsume()  && a generator that consumes values

 SELECT testacc

 APPEND BLANK  && add a new record (generator)

 replace id WITH lcid,quantity WITH lcacc && takes states lcid and lcac and put them in testacc

ENDFUNC

FUNCTION gencompose()  && coordinator of three processes

Select test

Go top

Do while not eof()

  Lcid=id   && initialize value for lcid

  Lcacc=0   && initialize value for lcacc

   genproduce()  &&produce accum values

*pause

   DOEVENTS  && consume events in a VFP event loop

*continue   

   genconsume()   && consume accum values

   SELECT test

   IF i<2       && pause (suspend) execution

    EXIT

   ENDIF

enddo

endfunc

A function gencompose() is a coordinator of three processes:

1) Genproduce(),

2) Doevents,

3) Genconsume().

We can add also the fourth process, a stop.

The interesting (non visible) transformation inside this function is at its pause point. Gencompose, without doevents, is a blocking operation; it blocks our screen. A doevents suspend a program gencompose and pass execution to the function doevents to consume the events in a VFP event loop. A doevents command totally changes our program flow.

For a moment let us read doevents as a pause command. A function genproduce computes (»returns«) a value lcacc.

Note. This »hidden«  passing  of the values (state) without programmer intervention (no arguments) is a characteristic of the method.

After a pause a function genconsume takes this value and elaborate it. We can look at this as a sequence of value transformations. In this »sequence« a »pause« operation is very important. Naively said, all our unblocked code derives from this »pause« operation.

Note. A pause operation is not like a sleep command, which block a thread.

 

Function accum()

  Createtab()

  i=1

  gencompose() && stop (suspend) execution (yield keyword)

  i=i+1

  ?lcacc

*doevents

  gencompose()  && continue

ENDFUNC

A new version of a function accum runs a function gencompose two times. The first time it »consume« the first id, then it pauses, after that it continue, where it has stopped and »consume« the second id. We can pause and resume this function multiple times.

The new version of a function accum is given as a non blocking accumulating program, and gives us an opportunity to inject code (doevents, stop, do another task).

We can look at the example in this way:

1) We have a generator genadd and we coordinate (force it) using a function genproduce, another generator. We compose genadd using a generator genproduce. After every call of the generator genadd we can inject VFP code to change a program flow.

2) We have two genarators (genproduce and genconsume). Gencompose coordinate them and  we obtain a new generator.

3)Accum compose a function gencompose. After every call to it, we can inject VFP code (for example »?lcacc«), execute it and continue with our basic program flow. We can inject also another function or procedure.

4) Accum is another generator, a table test accumulator. You can compose it together with another accumulator for a table test1 and so on. This code doesn't block. You can execute this code for an hour without blocking the computer, your screen is responsive. Most importantly, the application becomes massively scalable, and its behavior will degrade gracefully

under load. Your application is reactive.

In the last version of a function accum we run asynchronous code. We pass the values lcid and lcacc in a fire and forget way. The function genconsume accept them after a pause (or never in case of errors in executing doevents).

 

Multiple loops in parallel

 

Suppose we have another do loop to acumulate values for a table test1, with gencompose1 and a variable j instead of i. We can compose the two loops in various ways to run them concurrently, for example:

 

Function accummulti()

  Createtab ()

  Createtab1()

  i=1

  j=1

  gencompose() && stop (suspend) execution (yield keyword)

  gencompose1()  

  i=i+1

  j=j+1

  ?lcacc

*doevents

  gencompose1()  && continue

  gencompose() 

ENDFUNC

 

The two loops are running in aconcurrent way, without blocking each other or the screen. In a accummulti() we have intentionally reverse the order of gencompose1 and gencompose. The results are independent of the order of execution.

In a loop you can accept a client connection on a WEB server using a websocket protocol (permanent connection). By composing multiple non blocking loops you can accept thousand and thousand of clients: your web application is scalable.

The beauty of coroutins (generators) is how they manage state. We can run in a concurrent way hundred loops; each of them will take care of its state.

 

Naive server

Let us create an example of a server that runs one million (1.000.000) of concurrent loops. Every loop can simulate a request to the server. We have one million of concurrent request to our server. We have two kinds of requests:

Short,

Long.

Function Init()

For i=1 To 1000000   && one million

      ar[i,1]=0  && closure variable ar

      If Int(i/10)=i/10 && every tenth is a long running

            ar[i,2]=.T. &&Long

      Else

            ar[i,2]=.F. &&Short

      Endif

Endfor

Endfunc

We shall manage the requests with an array (ar) of three components (columns):

In the first column we shall save the computation,

In the second we will label, if a request is long running (.T.) or short (.F.),

In the third column is a label, if a computation is concluded.

A function init puts the initial values in the array.

 

Function runserv(llen)

nol=0

Do While .T.

      nol=nol+1

** execute in chunks of 100 threads to eventually inject code

      For j=1 To 100 && all the array elements in the range

            i=d1+j

            If ar[i,2]   && long running

                  If Not ar[i,3]  && active (not terminated)

                        For p=1 To 20

                             genlong()  && ten times

                        Endfor

                  Endif

            Else   && short running

                  If Not ar[i,3]  && not terminated

                        For p=1 To 20

                             genshort()

                        Endfor

                  Endif

            Endif

      Endfor

      d1=d1+100

      If d1+100>1000000

            d1=0  && start a new cycle

      Endif

      If nol>llen  && stop

            Exit

      Endif

Enddo

Endfunc

 

A function runserv executes the million tasks in an asynchronous way. It accepts the number of times it will visit each array element (llen). It takes a chunk of hundred array elements and executes the tasks, stops and continue with the next 100 and so on. In each of its loops it executes the long running task (genlong()) or the short (genshort()). Attention, every time the program visits a component, it executes only a chunk of a task of this component.

 

Function genshort()  &&generate numbers till 20

If ar[i,1]<11

      For k=1 To 10

            ar[i,1]=ar[i,1]+1

      Endfor

      If ar[i,1]=20     && the first 20

            ar[i,3]=.T.

      Endif

      If Int(i/10)<>i/10 And i<50 && the first 50

            ?ar[i,1],i  && print the first 50

      Endif

Else

Endif

Endfunc

Genshort simulates a short running task. On each call it ten times increase a value in the array first column of 1 (in total  of 10). We call it 2 times (ar[i,1]<11).

Function genlong() && generate numbers till 400

If ar[i,1]<400

      For k=1 To 10

            ar[i,1]=ar[i,1]+1

      Endfor

Else

      If ar[i,1]=400

            ar[i,3]=.T.

      Endif

Endif

Endfunc

Genlong simulates a long running task. On each call it ten times increase a value in the array first column of 1 (in total  of 10). We call it 40 times (ar[i,1]<400).

 

Clear

Dimension ar[1000000,3]

Init()

d1=0

t1=Seconds()

runserv(20000)  &&20000 400=20*20, 100*10*20=20000

?Seconds()-t1

?ar[333,1],ar[340,1]

Return

We run the million loops with a parameter 20000 (20 times to visit each array component – row). On my laptop it executes in less than a minute. The short tasks are computed very fast. From the results on the screen you can conclude:

1)      The loops are computed in a concurrent way,

2)      Execution is asynchronous (each task is manipulated multiple times),

3)      The long running tasks don't block,

4)      The short running task has »priority«,

5)      All these are done without multithreading, simple VFP code.

 

Conclusion

 

In a VFP loop we have injected a new composition »operator« doevents (pause and resume).  If we run this function in a VFP  form, the VFP loop do not block the screen, the computer is reactive.

Generators are composable. It is easy to pause (suspend) them and reactivate. The same you can do with generators compositions.

For us, the programmers are more important that the generators are »decoposable«: before composing them we can inject our logic and change a program flow.

Note. This composition is possible, because generators are derived form monads. In another article I shall describe some monads in VFP.

Pay special attention also to the pause »operators«. They are composable elements. May be they are  in relationship with monads.

With minimal effort we have managed the state of million server processes: we have started them, paused and stoped. We have coordinated an orchestra of million players! Can you run this code on your computer? Will it run out of memory?