-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
At yesterday's MSLUG meeting Dominique showed us how easy distribution can be done using Erlang. There was a question asking how Erlang and Termite compare. To give a quick idea, here is an example of how distribution can be done with Gambit-C/Termite.
Assuming you have built the Gambit runtime to include Termite, you need to start a Termite instance on a machine (say "dynamo") and initialize it:
dynamo% gsi -e '(node-init (make-node "dynamo" 7777))' -
This instance of termite is a "node" which is identified by the IP address "dynamo" and the IP port 7777. Note that a REPL is started, but we will not be using it.
Now let's start a Termite instance on machine "neo" to get a REPL:
neo% gsi -e '(node-init (make-node "neo" 8888))' -
This second node is identified by the IP address "neo" and the IP port 8888. We could have started two nodes on the same machine, as long as the IP ports are different. All the remaining REPL interaction is on neo. First, let's create a reference to the first node on "dynamo":
(define dynamo (make-node "dynamo" 7777))
Synchronous remote procedure call is done with the "on" function. It executes a thunk remotely and returns the result:
(on dynamo host-name)
"dynamo.iro.umontreal.ca"
Let's develop an asynchronous remote procedure call in stages. First a simple example of local process creation and communication:
(define s (self)) (define p (spawn (lambda () (! s (expt 2 (?)))))) (! p 10) (?)
1024
The process p uses the "?" function to wait for and retrieve the next message in p's mailbox. The function "!" sends a message back to the primordial process (whose PID was saved in global variable s). This code can easily be extended to get a simple "power of 2" server:
(define p (spawn (lambda () (let loop () (! s (expt 2 (?)))
(loop)))))
(! p 16) (! p 32) (?)
65536
(?)
4294967296
Note that the results are retrieved asynchronously (i.e. the mailbox queues the messages received). Moreover the results are always sent back to the primordial process. To generalize, the request can be a pair containing, in the car, the PID of the process which the result should be sent to, and in the cdr, a function that is executed by the server to generate the result:
(define p (spawn (lambda ()
(let loop () (let ((x (?))) (! (car x) ((cdr x))) (loop))))))
(! p (cons (self) host-name)) (?)
"neo.iro.umontreal.ca"
In this example the server is running locally. By changing the "spawn" to a "remote-spawn" the server can be started on "dynamo":
(define p (remote-spawn dynamo
(lambda () (let loop () (let ((x (?))) (! (car x) ((cdr x))) (loop))))))
(! p (cons (self) host-name)) (?)
"dynamo.iro.umontreal.ca"
Any serializable Scheme object can be passed in a message. This includes closures. Here is an example:
(! p (cons (self)
(let ((a (host-name))) (lambda () (let ((b (host-name))) (lambda () (list a b (host-name))))))))
(define f (?)) (f)
("neo.iro.umontreal.ca" "dynamo.iro.umontreal.ca" "neo.iro.umontreal.ca")
Note that the cdr of the pair sent to p is a closure which remembers the binding for a, which is "neo.iro.umontreal.ca" because that's where the first (host-name) was evaluated. When the server calls this closure, the second (host-name) is evaluated, giving "dynamo.iro.umontreal.ca". The binding of b is remembered in the closure created by the second lambda. This closure is sent back to the primordial process, which binds f to it. When this closure is called the third (host-name) is evaluated, resulting in "neo.iro.umontreal.ca".
This example shows that the meaning of the global environment is node dependent. The global environment is not automatically kept consistent on all nodes, it is the programmer's responsibility to ensure this where needed. So for example:
(on dynamo (lambda ()
(with-exception-catcher (lambda (e) e) (lambda () (fact 10))))) #<unbound-global-exception #2>
(on dynamo (lambda () (load "test.scm")))
"/Users/feeley/test.scm"
(on dynamo (lambda ()
(with-exception-catcher (lambda (e) e) (lambda () (fact 10))))) 3628800
(fact 10)
*** ERROR IN (console)@24.2 -- Unbound variable: fact 1>
Erlang's autoloading behavior is not built in to Termite. However it could be implemented on top of Gambit's exception handling system. The idea would be to extract from a fully qualified name (such as test#fact) the filename of the file to load, like this:
(define (char-pos str c) (let loop ((i 0)) (if (< i (string-length str)) (if (char=? c (string-ref str i)) i (loop (+ i 1))) #f)))
(define base-eh (current-exception-handler))
(current-exception-handler (lambda (e) (if (unbound-global-exception? e) (let* ((sym (unbound-global-exception-variable e)) (str (symbol->string sym)) (i (char-pos str ##))) (if (not i) (base-eh e) (let ((mod (substring str 0 i))) (load (string-append mod)) (let ((val (##global-var-ref (##make-global-var sym)))) (if (##unbound? val) (base-eh e) val))))) (base-eh e))))
(namespace ("test#" fact))
(pp (fact 10))
The call (fact 10), due to the namespace declaration, is rewritten into (test#fact 10). When the interpreter evaluates the reference to test#fact the exception that is raised is caught by the exception handler which extracts the module name "test" and loads that file to access the definition of "fact". The file "test.scm" should contain:
(namespace ("test#" fact))
(define (fact n) (if (= n 0) 1 (* n (fact (- n 1)))))
Marc
Afficher les réponses par date