|
From: Lars R. N. <lar...@gm...> - 2009-10-27 01:26:06
|
(..I see you actually mention "special variable". If you only consider bindings (LET) to special variables (not "top-level" SETQ/SETF against them) you will find that these bindings are thread safe; each thread can have _its own_ binding to the special variable. This is actually a very useful feature; http://www.sbcl.org/manual/Special-Variables.html . If you want more performance and "real" globals that do not have this special or dynamic behavior (which just happen to be thread-safe wrt. bindings as a bonus) in the first place, I'd go for SB-EXT:DEFGLOBAL combined with locks, SB-EXT:COMPARE-AND-SWAP and/or SB-EXT:ATOMIC-INCF instead of DEFVAR/DEFPARAMETER..) I think you're looking for SB-EXT:COMPARE-AND-SWAP or perhaps the less general purpose SB-EXT:ATOMIC-INCF. These handle the crucial point where a read+write (composition) must be executed atomically. They both have good doc-strings. While I'm no expert at this I've found the following little "wrapper" to work well here: (defstruct (atomt (:constructor mk-atom (&optional value)) (:copier nil)) (value)) (macrolet ((swap (place new) "This binds %OLD. NEW might be evaluated more than once." (with-gensyms (mnew) `(loop (let* ((%old ,place) (,mnew ,new)) (when (eq %old (sb-ext:compare-and-swap ,place %old ,mnew)) (return ,mnew))))))) (defmacro swap-atom (atom new) "NEW must be a value that yields T for (EQ NEW NEW). NEW might be evaluated more than once." (once-only (atom) `(swap (atomt-value ,atom) ,new))) (declaim (inline swap-atom-fn)) (defun swap-atom-fn (atom fn) "FN is a function that accepts one argument; the old or current value of ATOM. It must return a value that yields T for (EQ VALUE VALUE). Note that FN might be called more than once and the argument passed to it might change for each call." (declare (atomt atom) (function fn)) (swap-atom atom (funcall fn %old))) ) ;; macrolet Ref. the example posted by David and assuming I've not made a mistake, this will now always return 2: AMX> (let* ((x (mk-atom 0)) (thread-1 (sb-thread:make-thread (lambda () (swap-atom-fn x #'1+)))) (thread-2 (sb-thread:make-thread (lambda () (swap-atom-fn x #'1+))))) (sb-thread:join-thread thread-1) (sb-thread:join-thread thread-2) (atomt-value x)) 2 AMX> Even if the threads race, the read+set vs. that single X variable stays atomic in nature. Reading is always safe as long as one follow the rules mentioned in the doc-strings for SB-EXT:COMPARE-AND-SWAP and SB-EXT:ATOMIC-INCF. So, e.g., dealing with a variable or a slot storing a big-num is not safe (without using traditional locking) since "the EQ rule" doesn't hold for it in the first place: AMX> (eq (1+ most-positive-fixnum) (1+ most-positive-fixnum)) NIL (..if one follow the Hyperspec in detail EQ doesn't even hold for fixnums ..*sigh*.., but I think EQ for fixnums pretty much "actually holds" for all the commonly used Lisp implementations out there?.. personally, I'm not interested in "currently stupid" implementations anyway, so..) |