From: Bruno H. <br...@cl...> - 2004-06-29 02:55:15
|
> I'm working on the spec Good! Let's talk about the fundamentals first. Denis writes: > If a function is merely potentially destructive of one or more of its > arguments (e.g. NCONC), it itself doesn't need to be made thread-safe, > if synchronization is expected to be done by the programmer. Agreed. > So, how should one signal enabling MT? (MT:USE-THREADS)? Somehow > everything relevant in CL is going to have to change when MT is > enabled. Here I'd like to make two wishes: A) There should be no global (MT:USE-THREADS) switch. In other words, a program that runs without using threads should also run when other Lisp files are loaded that use threads. (This simple rule has been violated by ACL regarding their uppercase/lowercase symbol switch: Two worlds that don't operate together. It's a mess. Instead, make thread-safe and not thread-aware code coexist.) The usual restriction is that the old, non-MT code is called from a single thread only. B) A clisp built with MT enabled should have comparable performance when running non thread-aware code than a build without MT. By comparable I mean a performance decrease of 30% is acceptable, but a performance decrease of a factor 5 is not. Can you realize these wishes together? Sam writes: > So, the simple macroexpansion > > (push a b) ==> (setq b (cons a b)) > > is WRONG. > it should be something like > > (with-write-lock (b) > (setq b (cons a b))) > > OTOH, SETQ being a special form, it may just always aquire write locks > to all symbols it will set before it starts to evaluate any of the forms. Interesting: What kind of thread graining do you have in mind? Do you expect that every thread executes, say, ca. 5 or 10 instructions until it terminates? I.e. that threads get created very frequently, like in the Oz programming language? If so, your requirement may make some sense. Otherwise, if you expect that thread creation is rare, i.e. that we essentially want to keep the way Lisp programs are written (not Oz-like), then I think doing a lock manipulation at every PUSH or SETQ will make it impossible to satisfy rule B. Furthermore I don't think such automatically generated with-write-lock will help much applications. Because: 1) Where is the corresponding with-read-lock access generated? At each variable access? 2) Applications typically need to do more than one operation with the lock held. Example: (defvar *width* 10) (dervar *height* 20) (defun double-the-size () (with-lock (...) (setq *width* (* 2 *width*)) (setq *height* (* 2 *height*)))) It doesn't help you to protect each individual variable access, because what the programmer wants is that the *width*/*height* manipulation appears as a single atomic entity. So he _must_ place locks for himself anyway, and thus is brings nothing if the implementation does any additional locking at the setq level. It justs wastes CPU cycles. But if you want an Oz-like Lisp, the story if completely different, because then noone expects the wish B to be fulfillable... Bruno ________________________________________________________________ Verschicken Sie romantische, coole und witzige Bilder per SMS! Jetzt neu bei WEB.DE FreeMail: http://freemail.web.de/?mc=021193 |
From: Denis B. <db...@st...> - 2004-06-29 12:17:41
|
On 28 Jun 2004, at 22.50, Bruno Haible wrote: >> I'm working on the spec > > Good! Let's talk about the fundamentals first. >> So, how should one signal enabling MT? (MT:USE-THREADS)? Somehow >> everything relevant in CL is going to have to change when MT is >> enabled. > > Here I'd like to make two wishes: > A) There should be no global (MT:USE-THREADS) switch. In other words, > a program that runs without using threads should also run when > other > Lisp files are loaded that use threads. (This simple rule has been > violated > by ACL regarding their uppercase/lowercase symbol switch: Two > worlds > that don't operate together. It's a mess. Instead, make > thread-safe and > not thread-aware code coexist.) The usual restriction is that the > old, > non-MT code is called from a single thread only. As it says in the spec, I had thought the ansi functions would be not-necessarily-thread-safe when threading was off (i.e. (MT:USE-THREADS) had not been called), and thread-safe when enabled; you're saying the the ansi functions should all be made thread-safe and always used as such, even in a non-thread-aware program? I would think that might violate Wish B, though I have no evidence to back me up. I didn't mean that non-thread-aware functions somehow would cease to work after a call to (MT:USE-THREADS); just that that call would change the implementations of the predefined ansi functions, resulting in thread-safe ansi functions. But if you still think it's a bad idea, I'll take it out. > B) A clisp built with MT enabled should have comparable performance > when > running non thread-aware code than a build without MT. By > comparable I > mean a performance decrease of 30% is acceptable, but a performance > decrease of a factor 5 is not. > > Can you realize these wishes together? WRT Wish B, I think the spec and the code will have to evolve together, because aside from coding up and testing the spec as it matures, I have no idea how I would measure the efficiency of non-thread-aware code under clisp built with MT. Additionally, a given mutator policy might influence another, and if the first turns out to have an unacceptable decrease in efficiency, it'll be more work to redo them both than to just have tested the first policy at the beginning. > > Sam writes: >> So, the simple macroexpansion >> >> (push a b) ==> (setq b (cons a b)) >> >> is WRONG. >> it should be something like >> >> (with-write-lock (b) >> (setq b (cons a b))) >> >> OTOH, SETQ being a special form, it may just always aquire write locks >> to all symbols it will set before it starts to evaluate any of the >> forms. > > Interesting: What kind of thread graining do you have in mind? > > Do you expect that every thread executes, say, ca. 5 or 10 > instructions until > it terminates? I.e. that threads get created very frequently, like in > the Oz > programming language? If so, your requirement may make some sense. > > Otherwise, if you expect that thread creation is rare, i.e. that we > essentially > want to keep the way Lisp programs are written (not Oz-like), then I > think > doing a lock manipulation at every PUSH or SETQ will make it impossible > to satisfy rule B. > > Furthermore I don't think such automatically generated with-write-lock > will help much applications. Because: > 1) Where is the corresponding with-read-lock access generated? At > each variable access? > 2) Applications typically need to do more than one operation with the > lock held. Example: > (defvar *width* 10) > (dervar *height* 20) > (defun double-the-size () > (with-lock (...) > (setq *width* (* 2 *width*)) > (setq *height* (* 2 *height*)))) > It doesn't help you to protect each individual variable access, > because > what the programmer wants is that the *width*/*height* manipulation > appears as a single atomic entity. So he _must_ place locks for > himself > anyway, and thus is brings nothing if the implementation does any > additional locking at the setq level. It justs wastes CPU cycles. To me it seems there should be some generic way of signaling, in various situations, whether some variable is shared. Or at least a way of doing that for most objects (e.g. hash-tables, structures). Because, for example, something like this: (defun accumulate-stuff (n fn) (let ((result '())) (dotimes (i n) (push (funcall fn) result)) (nreverse result))) Each push should not have a lock, because there's no way any other thread could mess with it. Having a lock on each push would violate rule B, I would think. However, in this case: (defun memo-fact (n) (multiple-value-bind (val isb) (gethash n *memo-fact-table*) (if isb val (setf (gethash n *memo-fact-table*) (* n (memo-fact (1- n))))))) Aside from the fact that this is definitely _not_ the most elegant way of implementing memoization, my point is that because memo-fact uses one table for each call, that table should have RW type lock, because any thread could be reading or writing at any time. One solution is to declare when the data structure is created that it's created in a shared manner, e.g., (defparameter *memo-fact-table* (make-hash-table :mt-type :shared)) Then according to some granularity spec for a hash table, *memo-fact-table* would have a RW lock. At this point the question is, is each key/bucket locked individually, or the table as a whole? Maybe the programmer should be able to specify, without explicitly writing the code to implement it. -- Denis Bueno PGP: http://pgp.mit.edu:11371/pks/lookup?search=0xA1B51B4B&op=index "Acquaintance, n.: A person whom we know well enough to borrow from, but not well enough to lend to." - Ambrose Bierce |