Resolving let-method

The previous article showed that there is a multi threading issue on let-method. In general, current Sagittarius adds generic method globally even whenever it's defined. So if the load loads a script with define-method then the generic method is added to the global binding. Thus the effect is globally done even though it's loaded in child thread.

This is not a good behaviour I believe so I've changed it. Current head has following behaviour;
  • If a method is defined in main thread - adds method globally
  • If a method is defined during importing a library - ditto
  • If a method is defined in child thread and not library importing period - adds thread local storage.
  • Generic functions are inherited from parent thread but child thread can't contaminate parent.
The thread local storage is VM's register I've added, generics (not sure if the name is proper but reasonable isn't it?). The changes are done in three places, add-method, remove-method and compute-methods. There are slight change of slot accessor of generic function as well but this is trivial.

The change of compute-methods is not a big one. It now just considers generic methods of current thread. Like I mentioned above, generic methods are located two places, one is generic function's methods slot and the other one is thread local storage. Thus compute-methods needs to get all methods both the slot and storage.

add-method and remove-method are a bit more tricky. First it needs to detect whether or not it's running on main thread or during library importing period. If the definition is executed on that term then it adds the methods to generic function's slot. If not, then it adds thread local storage with some more information. (currently maximum required argument number.)

Now following piece of code runs as I expected.
(import (rnrs) (clos user) (srfi :1) (srfi :18) (srfi :26))
(define-generic local)
(define (thunk)
  (thread-sleep! 1)
  (let-method ((local (a b c) (print a b c)))
    (thread-sleep! 1)
    (local 1 2 3))
  (local "a" "b" "c"))
(let ((ts (map thread-start! (map (cut make-thread thunk <>) (iota 10)))))
  (for-each thread-join! ts))
;; may prints some of the value
;; then raises an error.
The solution itself might be a bit ugly (treating generic function specially) but behaving properly is more important.


Shiro Kawai said...

What about this case: You're running a server, whose main thread is an event dispatch loop. For the maintenance work, you can also connect to the server to run REPL, in a thread. You may want to modify the global generic function behavior from the REPL to do hot-patching.

I think let-methods and toplevel methods are separate issues; let-methods is inherently dynamic-scoping construct, and dynamic environment is per-thread. OTOH, toplevel methods are usually regarded as a part of shared global environment, though how much to be shared may depend on the language design.

One way to interpret your design is that every thread except main one gets an implicit dynamic scope in which all subsequent method definitions in that thread live. It is plausible, though I feel somewhat awkward to treat it that way.

There are cases that you do want to isolate effects of the global environment, and for that purpose, I think something like World ( http://www.vpri.org/pdf/tr2011001_final_worlds.pdf ) may be a good way to go. That is, detaching isolation from "threads".

kei said...

I was actually thinking other way around. If remote REPL service provides global utility as generic function (e.g. unbound-variable in Sagittarius) and user A specialised with his specific purpose, then user B also gets affected. This situation would be prefered to be avoided, IMO. (although if this is so, then I think class redefinition should not be done either, to keep consistency.) And indeed in the emergency situation it would be convenient to patch without restarting server.

I also felt awkward :) This should not be done in thread context but environment. I'll have a look, thank you for the information.

Post a Comment