Automatic Load Balancing
The Client-Server layer is probably the most commonly accessed layer within the BAP system. It hides the lower layer complexity and provides easy access to all system resources. Furthermore it may be treated as a 'boot-up system' that is active only at boot time and is later replaced by user written communication scheme.
The base of the Client-Server layer is the 'server'. A PROLOG Process becomes a server by informing the network server about its existence and by executing a server looptut_com_client_loop.
A client can access the functionality of the client-server layer using the Client-Server call constructs (dcall(), xcall() and exec()).
Default boot Up on the Client-Server Layer , Low Level Router and 'C' Layer , PROLOG Process Layer , Object layer
The idea of a Client-Server scheme is that every process participating in a Client-Server system offers services to calling processes. Within BAP, a server is just a regular PROLOG process that infinitely stays in a 'server loop': (simplified example)
repeat, retractall(_), rec_msg(Sender,Msg), call(Msg), send_msg(Sender,Msg), fail.
All processes in our client server scheme permanently stay in such a loop. If they receive a message ( rec_msg() suspends until a message arrives), the message task is executed (by the call() predicate), the result is sent back and the terminating fail guarantees that the internal memory is cleaned up and the control flow starts at the repeat again. The database is cleaned up by the preceding retractall() .
Server Management , PROLOG Process Layer
Purpose of the Client-Server layer administration is to manage the processing resources of a Transputer network on a low but transparent level. This is done by starting a server loop on every PROLOG process in the network. Furthermore one certain process is separated from the others to perform a special task:
During the start-up of a network, BAP automatically starts one PROLOG process on each processor (as described in the previous chapter). This process examines its processor and process id:
IF the process finds itself to reside on processor 1 and its Pid (Process ID) is 1 THEN the process names itself the 'network server'. This process is unique to the network. It furthermore starts two additional processes that become a regular server and a shell respectively. ELSE the process names itself 'server' and sends a message ('server_present(Tid,Pid)') to the network server (with the known address Tid=1, Pid=1) to tell it that this server is ready to receive messages. It enters its server loop and starts waiting for tasks.
The picture above show the situation after a successful boot-up of a
2x2 matrix topology: Each processor contains one server, the
fundamental processing resource. Node 1 additionally contains the
network server and a process that runs a 'shell'. The network server
contains the list of available servers
Is a PROLOG process that executes a server loop . The existence of a server is known to the network server . In general servers are managed by the network server, but application programs may partly take over this responsibility.
Servers do not have a 'state' (=data). After each call to a server, the (local) database is cleaned up so that a server always appears to be newly created. This helps reducing the network complexity because otherwise the state of all processes would have to be managed by the user. If you really need a process that maintains its state bewteen two calls, use an object instead.
ServerList , Message , Server Management
Is the process that holds the global ServerList . The network server is the only instance that holds global information about the network (at least at boot time). The network server is globally available to every process.
Every process in the network sends during its start-up (normally at boot-time) a 'server_present()' to the network server. These messages are processed in the way that for each message a database entry is created, that indicates that this server is ready to overtake a task. All these database entries together are called ServerList .
alloc_server , free_server
Is a list of all available servers in the network. The ServerList is kept by the network server .
An application can allocate processing resources by asking the network server for a server id (using the alloc_server() predicate). This server is removed from the ServerList. Before the application terminates, it has to free the allocated servers (using free_server() ). This approach avoids any dependencies on the network size and topology.
If no servers are available in the ServerList, an 'alloc_server()' waits until a server becomes available. This may cause deadlocks, if not enough servers are available. An alternative is to create additional servers in the Transputer network (using create_processes() ).
Server , Server Management
Above we saw two communicating processes. Something like that happens quite often, so we introduced a special construct to handle such situations. We called it 'dcall()' whereby 'd' stands for 'deterministic' (e.g. for a call with only one solution). Again, an example (calculating the factorial of 5 on a different process) may show the usage of the construct:
Client Process Server Process
alloc_server(ServerId), dcall(ServerId,fac(5,X)), repeat, rec_msg(Sender,Msg), call(Msg), send_msg(Sender,Msg), fail. write("Factorial(5,",X,")\n").
The client process calls alloc_server() to get an id of any free server. The 'dcall()' sends the message 'fac(5,X)' to the server.
The server receives the message using rec_msg() , executes it by call() and returns the result to the client. On the client side 'inside' the 'dcall()' routine, 'fac(5,_)' and 'fac(5,120)' are unified so that 'X' is bound to '120' and the result can be displayed.
If your compare the send_msg() - rec_msg() example with the 'dcall()' example your will recognise that nothing spectacular changed but the use of a 'dcall()' is a little bit more easy and elegant. Simply imagine, you perform the 'call(fac(5,_))' on a remote processor.
If you should take a closer look at the timing you will see, that the 'dcall()' could also be called 'synchronous'. The dcall() suspends the client process until the reply message is received. To receive the reply message, the server must perform the call(fac(5,_)) and after that, the reply message is send.
The dcall() Features:
The effect of the 'dcall()' construct is to run a task on a distinct process. No speedup is gained. Nevertheless we can show here, that it isn't too difficult to use parallel processes.
exec() , xcall()
As shown above, the dcall() isn't really a parallel construct but something like a 'remote procedure call'. This is caused by the fact that a dcall() returns a result. If this reply is not required, the client doesn't have to wait for the server to finish. This is the way the exec () construct acts. This allows to send out several tasks at the same time. The next example show how to implement something like an OCCAM 'PAR' construct:
par(TaskList):- par_demux(TaskList), % send out tasks par_mux(TaskList). % collect and synchronize par_demux(TaskList):- get_my_id(MyId), % get ID of this process member(Task,TaskList), % select one tasks alloc_server(ServerId), % allocate one server exec(ServerId, ( Task, % concat "Task" with the.. send_msg(MyId,Task) % ..command to return the.. ) % ..result (send_msg()) ), fail. % select the next task par_demux(_). par_mux():-!. par_mux([Task|Rest]):- rec_msg(_,Task), par_mux(Rest). CALL: ?-: par([fac(5,_),fac(8,_),fac(7,_)]),
The example above demonstrates the implementation of something like a 'farm' concept. Par() is called with a list of tasks that each contain an unbound variable for the reply value. First par_demux() is called with the list of tasks. Because the ' member () - fail ()' construct is used, the 'TaskList' isn't modified in par_demux(). Each task is send to a server process, together with a send_msg() that sends the result of the task back to the sender (identified by 'MyId').
Par_mux() extracts the first 'Task' from the 'TaskList' and waits for the first message to appear, that matches 'Task'. There is no problem with pulling the 'wrong' message out of the mail box because 'rec_msg()' only read messages that fit to its arguments.
The exec() Features:
dcall() , xcall()
The ' xcall ()' is the most powerful of the current 'call constructs'. It requires a little different behaviour of the server.
The xcall() Features:
xcall(member(Mem,[1,2,3,4])), write(Mem), nl, fail.
dcall() , exec()
In several examples above the alloc_server() routine was used. This chapter explains some details about the use of servers.
Servers represent the restricted processing resources of a Transputer network. By default, each Transputer starts a certain number of server, for example 4 or 8. These servers are registered in the ServerList of the network server . Only these servers are available to applications of the Client-Server Layer . They are managed by the alloc_server() and free_server() predicates.
Kernel of the server management is the network server. This process keeps track of the id's of all servers that are available to the user. The network server is the only process in the BAP system with a fixed address (the address is 'msg(1,1)').
With this knowledge, the 'alloc_server()' predicate can always send a 'server_request()' message to the network server. If at least one server is currently available, it is returned. Otherwise 'alloc_server()' waits for the first server to (re-) appear. (This may never happen and cause a dead-lock!)
Free_server() is the routine that adds a server id to the network server. Free_server() is used internally within the dcall (), exec () and xcall () constructs. The user does not need to free the servers manually when using these constructs. (Adding one server twice to the ServerList will probably cause the application to '' suspend ').
Nevertheless a user may wish to establish his own communication scheme (basing on send_msg(), rec_msg()) on top of the Client-Server Layer. To do this, his application may allocate some servers, start an individual message loop (or something similar) on them and free the servers when the application terminates. For example the management of the object layer is done this way.
The alloc_server() predicate can currently be used in two different fashions: with a partly bound argument or with an unbound argument.
ServerId = msg(1,_), alloc_server(ServerId),
The difference is that in the first case a server
on Transputer 1 is returned while a 'random' server is returned in the second case. If a user wants to place the execution of a task onto a specific processor, he has to bind the Tid part of the 'ServerId' to a valid Transputer id.
In the second case with the unbound ServerId, alloc_server() uses its default strategy to return a server (currently alloc_server() remembers the destination of the previous allocation and increments the Transputer Id with each call. This strategy fits pretty good most of the example applications provided with this BAP release). However, if a user wants to implement his own strategy he is free to analyse the state of the network and to allocate server of his choice (using alloc_server() with a partly bound argument).
We left the solution of this definitely nontrivial problem to the user because it requires intimate knowledge about the communication structure of the application that we do not have.