Not like the old-school Lisp atoms
Ah it's been long since the last entry. It's not that Gauche development is staggered; the thing is, I run several development threads concurrently, using local topic branches, and it is hard to pick a topic that can make a complete entry with conclusion.
Nah, too much excuses. From now on, I dump here whatever thoughts and experiments I did, slow but constantly, even without a nice conclusion.
Ok, so let me pick up where I left... the last entry was March 16. The next thing I did was to commit the support of atoms.
No, not that traditional Lisp atoms which mean something
that's not a cons.
atomp was once important to explain how Lisp worked,
but these days, with so many different datatypes, I don't think
having a term for non-cons objects is important. That usage
should become a history.
These days, more important atomic property is that something be atomic with regard to concurrent execution. To confess, I steal the term from Clojure. Though Gauche doesn't abstract concurrent threads nor isolate mutable operations so much as Clojure does, I do find some recurring patterns I use while writing concurrent code in Gauche, and this is one of them.
An atom here is a wrapper to bundle one or more objects
that needs to be updated atomically. It is available
gauche.threads module. The following creates
an atom with two vectors.
(use gauche.threads) (define a (atom (vector 1) (vector 2)))
a => #<<atom> 0x17467e0>
To access those vectors safely, you pass a procedure to
(atomic a (lambda (v0 v1) ...do whatever with v0 and v1...))
No other thread can access/modify the atom
Gauche's builtin hashtable is not thread-safe. I was always
wondering whether I should provide a separate thread-safe hashtable
or provide a generic support to allow exclusive access to any
objects, like Java's
synchronized mechanism. The former
would explode the number of primitive datatypes
(how about thread-safe treemap? thread-safe sparse hash? &c.)
The latter is cleaner, but difficult to implement efficiently
(especially, think about allow
synchronized on any pair!)
Now atoms can be used to provide mutable objects exclusive access.
(atom (make-hashtable 'eq?)), and then
wrap any access to the table by
atomic. I expect that
most cases of trivial use of
<mutex> can be replaced
Not only wrapping mutable objects, but atom itself can be
a mutable objects with synchronization. You can replace
its wrapped value by
(define b (atom 0))
(atomic-update! b (cut + 1 <>))
This makes an atom b works as a counter with mutex. If the
atom wraps N values, the update procedure should be N-in N-out
procedure. To extract the value, a convenient procedure
atom-ref can be used.
You may notice that if you retrieve a mutable object wrapped
by atom by
atom-ref, you can modify it outside of protection.
Atoms are not intended to prohibit you from doing dangerous things;
they just make it easier to do things safely rather than error-prone ways.
Finally but importantly, those atom primitive consistently takes optional timeout and timeout-val arguments.
(atomic a (^(x y) ... #t) 10.0 'timeout)
This returns #t if we can access the atom successfully,
but if we cannot obtain a lock for 10.0 seconds, it gives up
timeout. I find it indispensable to
write a robust code. The semantics of timeout and timeout-val
is the same as other thread primitives.