Arc Forumnew | comments | leaders | submitlogin
Atomic-invoke and GILs
4 points by dido 4697 days ago | 1 comment
I've been reading through the sources of ac.scm in an attempt to duplicate some of its functionality for Arcueid and noticed that atomic-invoke actually provides a much tighter lock than one might expect from the name. It would appear that in Arc3 atomic-invoke uses a single global mutex for all invocations, locking this mutex whenever any thunk is executed by atomic-invoke, and releasing it when the thunk exits. I suppose that means that at any given time only one thread protected by atomic-invoke may be running at any given time. One might have thought that every use of atomic-invoke would produce its own mutex, so only atomic-invokes on the same thunk would be thus restricted, but it would seem not.

I thus managed to very easily implement it in Arcueid's arc.arc using a global synchronization channel as follows:

    (def atomic-invoke (thunk)
      (if (isnt (type thunk) 'fn) (err "atomic-invoke requires fn arg")
          (__acell__) (thunk)
          (do (__acell__ t)
	      (<-= (__achan__) t)
	      (protect (fn () (thunk))
		       (fn () (<- (__achan__))
		              (__acell__ nil))))))
The internal function __acell__ retrieves/sets a per-thread cell with a true or nil value depending on whether the thread holds the atomic channel. __achan__ retrieves the global synchronization channel. The <- function reads from the channel, blocking if there is nothing to read and resuming when something can be read, The <-= function writes to the channel, blocking if something was already written there before, and resuming once the channel has been read from.*

It seems that this behaves almost like a global interpreter lock, and removing it will increase concurrency at the possible expense of having more bookkeeping overhead. I see that atomic-invoke is used by practically everything in arc.arc that manipulates places and setforms (in the form of atwith and atwiths), and I get the feeling that replacing this with finer-grained locks (which would not be hard for Arcueid to do and probably not at all difficult to do in ac.scm for that matter) would cause bookkeeping overhead to skyrocket in the same way that attempts to remove the GIL in Python had.

Well, why not remove such usage of atwith and atwiths inside places and setforms, and note that one must not assume that assignments made that way are atomic, and let the programmer use atomic-invoke or other more flexible synchronization primitives oneself where this is desirable? Or better yet, provide alternate versions of =, swap, push, rotate, etc. that are guaranteed atomic? I suppose this is much more in keeping with Arc's philosophy that the programmer is a smart person, and should be aware that threads are in that way fraught with peril.

----

* The idea of using such channels for synchronization has been shamelessly stolen from C.A.R. Hoare's CSP and the notation of <- and <-= has in turn been just as shamelessly stolen from the Limbo programming language used in the Bell Labs Inferno OS. It essentially implements an idea I sketched here long ago: http://www.arclanguage.org/item?id=1823.



3 points by rocketnia 4696 days ago | link

"One might have thought that every use of atomic-invoke would produce its own mutex, so only atomic-invokes on the same thunk would be thus restricted, but it would seem not."

I never thought of that. I suppose that would have been possible to do by constructing a mutex during macroexpansion.

  (self-atomic:foo bar baz)
  ==>
  (self-atomic-invoke '#<mutex> (fn () (foo bar baz)))
  ...where #<mutex> is a new mutex value
I don't see how this would have accomplished much, though. While self-atomic may protect the thunk from itself, it doesn't help two lexically separate parts of the code.

It does mean someone can easily construct multiple GILs.

  (def my-gil-1 (func)
    (self-atomic:func))
  (def my-gil-2 (func)
    (self-atomic:func))
  (= my-gil-3 [self-atomic:_])  ; same result
If they use eval, they can construct mutexes on the fly (even without a language-provided constructor utility):

  (def make-mutex ()
    (eval '[self-atomic:_]))
So I suppose that alternate world isn't too bad. :-p

---

"Well, why not remove such usage of atwith and atwiths inside places and setforms, and note that one must not assume that assignments made that way are atomic, and let the programmer use atomic-invoke or other more flexible synchronization primitives oneself where this is desirable?"

For what it's worth (noting again that I haven't used threads), that's been my preference for a long time, because...

"Or better yet, provide alternate versions of =, swap, push, rotate, etc. that are guaranteed atomic?"

...there's no need for these as alternatives. We can already say (atomic:push ...) if and when we need to.

On the other hand, if these alternatives were to let their subexpressions finish evaluating before they began their lock, something like this...

  (push (foo) (car (bar)))
  ==>
  (with (fooval (foo) barval (bar))
    (atomic:scar barval (cons fooval (car barval))))
...then we'd have a totally different option than (atomic:push ...), and I expect it to be closer to what we intend to express with (atomic:push ...) anyway. A finer-grained (synchronized barval (scar ...)) would probably be even closer.

-----