The concurrency library here is based upon the style of concurrency used in systems like Erlang and Termite. It is currently at a very early stage and only supports concurrent processes within a single Factor image. The interface is very likely to change so it is quite experimental at this stage. The ability to have distributed processes is planned.
A concurrency oriented program is one in which multiple processes run simultaneously in a single Factor image. The processes can communicate with each other by asynchronous message sends. Although processes can share data via Factor's mutable data structures it is not recommended as the use of shared state concurrency is often a cause of problems.
A process is basically a thread with a message queue. Other processes can place items on this queue by sending the process a message. A process can check its queue for messages, blocking if none are pending, and process them as they are queued.
Factor processes are very lightweight. Each process can take as little as 900 bytes of memory. This library has been tested running hundreds of thousands of simple processes.
The messages that are sent from process to process are any Factor value. Factor tuples are ideal for this sort of thing as you can send a tuple to a process and the predicate dispatch mechanism can be used to perform actions depending on what the type of the tuple is.
Processes are usually created using the spawn' word:
IN: concurrency spawn ( quot -- process )
This word takes a quotation on the stack and starts a process that will execute that quotation asynchronously. When the quotation completes the process will die. 'spawn' leaves on the stack the process object that was started. This object can be used to send messages to the process using the 'send' word:
IN: concurrency send ( message process -- )
'send' will return immediately after placing the message in the target processes message queue. A process can get an message from its queue using the 'receive' word:
IN: concurrency : receive ( -- message )
This will get the most recent message and leave it on the stack. If there are no messages in the queue the process will 'block' until a message is available. When a process is blocked it takes no CPU time at all.
[ receive print ] spawn "Hello Process!" swap send
This example spawns a process that first blocks, waiting to receive a message. When a message is received, the 'receive' call returns leaving it on the stack. It then prints the message and exits. 'spawn' left the process on the stack its available to send the 'Hello Process!' message to it. Immediately after the 'send' you should see 'Hello Process!' printer on the console.
A process can get access to its own process object using the 'self' variable so it can pass it to other processes. This allows the other processes to send messages back. A simple example of using this gets the current processes 'self' and spawns a process which sends a message to it. We then receive the message from the original process
self get
.s
=> << process ... >>
[ "Hello!" swap send ] cons spawn drop
receive .
=> "Hello"
A common idiom is to create 'server' processes that act on messages that are sent to it. These follow a basic pattern of blocking until a message is received, processing that message then looping back to blocking for a message.
The 'spawn-server' word does exactly that:
IN: concurrency spawn-server ( quot -- process )
A process is spawned in the same manner as 'spawn', but instead of the process existing then the quotation completes, the quotation is re-called. A process spawned using this method can break out of the infinite loop and exit the process using the 'exit-server' call:
IN: concurrency : exit-server ( -- )
The following example shows a very simple server that expects a cons cell as its message. The 'car' of the list should be the senders process object. If the 'cdr' is 'ping' then the server sends 'pong' back to the caller. If the 'cdr' is anything else then the server exits:
: pong-server1 ( -- process)
[
receive uncons "ping" = [
"pong" swap send
] [
"Pong server shutdown commenced" swap send
exit-server
] ifte
] spawn-server ;
pong-server1
self get "ping" cons over send receive .
=> "pong"
self get "ping" cons over send receive .
=> "pong"
self get "shutdown" cons over send receive .
=> "Pong server shutdown commenced"
Exiting process: G:12361
The idiom of sending the callers process object along with the message is so common that some standard routines are built into the concurrency library to handle this. A tuple called 'message' is used as the standard message sent to processes that wish to acknowledge receipt of the messaeg with a reply back to the caller:
IN: concurrency TUPLE: message data from tag ;
The 'data' contains the actual message data to be sent to the server. 'from' is the process object of the caller. 'tag' is an automatically generated unique value that the receving server will send back along with the reply so the caller can match it up with the original request. A 'send-reply' word is available that has the following signature:
IN: concurrency send-reply ( message pred quot -- )
The 'message' is a message tuple. 'pred' is a quotation with the signature ( data -- boolean ). It will be called with the message-data portion of the message. If it returns false, all three arguments are popped off the stack and nothing is done.
If the predicate returns true, then the quotation is called with the message data on the stack again. This quotation has the signature ( data -- result ). The result of the quotation will be sent back to the callinging process in a message tuple, with the same tag as the original message and the message data will be the result.
To make it easier to send a message tuple without having to generate a tag, get the 'self' process, etc, the 'send-message' word is available:
IN: concurrency send-message ( data process -- reply )
Given the message data it will construct a message tuple with a randomly generated tag and send it to the given process. It will then wait for a reply containing that specific tag and take the message data from it, leaving it on the stack.
Using these words our pong server example becomes:
: pong-server2 ( -- process)
[
receive
dup [ "ping" = ] [ drop "pong" ] send-reply
dup [ "shutdown" = ] [ drop "Pong server shutdown commenced" ] send-reply
message-data "shutdown" = [ exit-server ] when
] spawn-server ;
pong-server2
"ping" over send-message .
=> "pong"
"ping" over send-message .
=> "pong"
"shutdown" over send-message .
=> "Pong server shutdown commenced"
Exiting process: G:12364
'send-reply' is not really a good name, and it isn't that useful an interface. This is currently being worked on.
Write about how processes can be linked using 'spawn-link'. An error thrown in the quotation will cause the process to die, and the process that called 'spawn-link' to receive the exception when it next attempts to receive from its message queue.
Write about futures. Calling 'future' spawns a process to run a quotation that returns a result. Using '?future' with that process on the stack will block the calling process until the result is returned.
Copyright (c) 2004, Chris Double. All Rights Reserved.