(defn evens []
  (iterate (partial + 2) 0))

(defn odds []
  (iterate (partial + 2) 1))

(def foo (agent []))

(defn push-foo [s]
  (doseq [x s] (Thread/sleep (rand-int 100)) (send foo conj x)))

(defn pusher-threads [ss]
  (map
    (fn [s] (Thread. #(do (push-foo s) (println "done!"))))
    ss))

(doseq [t (pusher-threads [(take 10 (evens)) (take 10 (odds))])]
  (.start t))

foo



Typical output:

user=> (doseq [t (pusher-threads [(take 10 (evens)) (take 10
(odds))])] (.start t))
nil
done!
done!

user=> foo
#<Agent [0 1 2 4 3 6 5 8 7 10 12 9 11 14 16 13 15 18 17 19]>

user=> (filter even? @foo)
(0 2 4 6 8 10 12 14 16 18)

user=> (filter odd? @foo)
(1 3 5 7 9 11 13 15 17 19)



The odds are interleaved more or less randomly with the evens, but the
odds are themselves in sequence, as are the evens, and nothing is
missing. This demonstrates the documented behavior of agents: no race
conditions and the sends from any single thread are processed in the
order they were sent.

This might be useful for someone just learning clojure. (Other things
demonstrated: creation of simple infinite integer sequences with
iterate; avoiding holding onto the heads of same by not def'ing them
directly but instead defining generator functions; doseq for running a
side effect for each element of a collection, x2; Java interop with
the Thread class: static method invocation, constructor call with
argument, and instance method invocation; rand-int; println for
monitoring execution of code; do for bundling multiple expressions in
an if clause or #(...); accumulating stuff in a vector with conj;
closures, both (fn ...) and #(...), and partial to generate functions
in-line; even? and odd?; def and defn; and map, take, and filter.)



A detailed breakdown of the code that may also be useful for beginners:

The evens function returns (iterate (partial + 2) 0), which returns a
sequence starting with 0 and continuing by adding 2 to each previous
item -- mathematically, a recurrence. Since it's just adding a fixed
amount each step, it's a special subtype called an arithmetic
progression. In this case it's just the nonnegative even numbers in
ascending order.

The odds function does almost the same thing, but the starting point
of its sequence is 1 and so every number is one higher than in evens.
This obviously yields up the nonnegative odd numbers.

Directly defining vars holding these sequences would hold onto their
heads; generally a bad idea with large or infinite sequences. Instead
it's better to define a no-arg function to produce fresh instances as
needed, so you can grab a copy, work with it, and lose its head. Hence
making evens and odds functions rather than vars directly holding the
sequences.

The foo agent initially holds an empty vector.

The push-foo function takes a sequence of objects and pushes them onto
the agent's vector using (send foo conj x). The vector thus
accumulates the values. It waits a random interval before each push,
however, up to 100ms, using the Thread/sleep Java method. This ensures
that the threads don't run so fast that one finishes in the time it
takes for the other to be launched (the start method seems to be quite
slow, and, the way this code is written, the .start calls use
reflection), or interleave too predictably (say, [0 1 2 3 ...]).

The pusher-threads function takes a sequence of sequences and for each
sequence s creates a Thread whose body is (do (push-foo s) (println
"done!")). # #(do (push-foo s) (println "done!")) encapsulates that as
a closure, (Thread. # #(do (push-foo s) (println "done!"))) creates a
Thread object with that closure as the code it will run when started,
and the (fn [s] ...) form wraps this up in a larger closure, which map
runs for each sequence in the input to produce a sequence of threads,
each of which will, when started, run push-foo on one of the input
sequences and then announce that it is done.

The doseq line that does the deed is trickier as there's lots going on
in it. (take 10 (evens)) calls (evens) to get an instance of the
even-numbers sequence and chops off the tail to make it finite. (take
10 (odds)) does likewise with the odd numbers. The two are enclosed in
square brackets [...] -- an expression that will evaluate to a
two-element vector, one with the first ten nonnegative even numbers
followed by the first ten nonnegative odd numbers. Calling
pusher-threads on this vector produces a sequence of two pusher
threads, one to push the evens and one to push the odds. The doseq
itself runs over these threads starting each one with (.start t),
which invokes the Java instance method start() on the Thread t,
launching it.

The doseq returns nil but the threads chug along autonomously and
concurrently, waiting random intervals and sending conjes to the agent
foo holding the initially empty vector. Each prints a "done!" line
when it is finished with its sequence of numbers to send to the agent
to conj onto the vector.

After roughly one second, both should be done on a dual-core-or-more
machine; it could take up to two seconds on a single-core machine.
(Most of the time in both threads is spent in Thread/sleep, which
doesn't cause contention, and some more is spent generating sequence
elements, which also doesn't; very little is spent in the agent thread
pool conjing onto the vector, by contrast.)

At that point, printing the value of foo should produce a sequence of
the numbers from zero to 19 without any missing, somewhat out of order
but roughly ascending.

If two agent operations on the same agent ran concurrently, one might
read the vector, generate a new vector by conjing say 7 onto the end,
and then set the agent's state to the new vector while another read
the state, conjed 10 onto the end, and set the agent's state to THAT
vector; if they both read the state before either the 7 or the 10 got
added, and got [0 1 2 4 3 6 5 8], then one would set the agent's state
to that with either the 7 or the 10 added, and then the other would
set it to that with the other added instead, and you'd have either [0
1 2 4 3 6 5 8 7] or [0 1 2 4 3 6 5 8 10] afterward. In one case the 10
gets lost and in the other the 7 gets lost. This is called a "race
condition" and it's bad. You probably don't want your computer
randomly losing information it was told to store somewhere!

But agents avoid race conditions by executing their sends one at a
time, in some order. Both 2 and 4 got added between 1 getting added
and 3 getting added, in this instance, but in general, the vector is
read, something is conjed on, and the state is set to the new vector;
then the vector is read, something is conjed on, and the state is set;
and so on, without any overlaps that would cause any of the numbers to
get lost.

The expression (filter even? @foo) extracts the value held by foo,
then extracts just the even numbers, in the order they occur in foo's
vector, presenting them as a sequence; (filter odd? @foo) does the
same thing except it extracts just the odd numbers. The two sequences,
though interleaved in foo's vector, are each separately in numerical
order, because agent sends from a single thread are processed in the
order they were sent. Since (take 10 (odds)) produced 1, 3, 5, ..., 19
in order, and (push-foo ...) sent corresponding conj messages to foo
in order and from a single thread, the odd numbers got conjed on in
order, though sometimes one or more even numbers got conjed on in
between two successive odd numbers.

The whole thing can be reset by using (send foo (constantly [])) to
wipe the vector clean. (constantly x) returns a function that always
returns x, so sending (constantly x) to an agent will cause the agent
to replace its state with x after a while.

Another three runs produced the following successive outputs on my machine:

[0 1 3 2 5 7 4 9 6 8 11 10 13 12 15 14 16 18 17 19]

[1 0 3 2 4 5 7 9 6 8 11 10 13 12 14 15 17 19 16 18]

[0 1 2 3 5 4 7 6 8 10 9 12 14 11 16 13 18 15 17 19]

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to