[plt-dev] Parallel Futures Release
On Dec 7, James Swaine wrote:
> I'm pleased to announce the initial release of parallel futures, a
> construct for fine-grained parallelism in PLT.
Random notes:
1. Your message mentioned requiring `scheme/futures' -- it should be
`scheme/future'.
2. Is it possible to move `processor-count' into the core and make it
available for non-future builds too? That function is useful even
without futures (for example, for code that runs subprocesses for
all cores -- in my scripts I just read a from from /proc, and
having a portable replacement would be nice). For the sake of code
that wants to use futures, I think that the current function is
better named `futures-count'.
3. Can you please include the command line for building with futures
support? This way, you can have it show both `--enable-futures'
and `--disable-mred' to avoid confusion.
4. You should also mention what Windows users should do, even if it's
"not use futures", since on windows there is no configure step.
5. The term `unsafe' was used with a particular meaning by the foreign
interface, then came the scheme/unsafe/ops library with a slightly
different but related meaning. The futures documentation uses
`unsafe' with a completely different meaning -- so it would be
better to find some other term, or at least make that very clear.
(This applies in general too -- for example, in your reply to Jay
you mention "unsafe primitives"... A question about the confusion
with `scheme/unsafe/ops' or `scheme/foreign' would also be
difficult to answer when the only term you have is "unsafe".)
6. It would be nice to be able to get a list of the currently running
futures? (With threads, this is possible through custodians --
with futures being global this won't make much sense.)
7. There should be some way to disable futures. I'm thinking of
situations like running sandboxed code, where you don't want
untrusted code to be able to grab cpus. Of course it would be nice
to be able to have a finer control over it (like how many cores
each future sees) -- but with nested futures being forbidden there
doesn't seem to be much point in that.
8. Looks like there is no way to kill a running future. There should
be one. To make it practical, it should also be killed on a
custodian shutdown -- but again, it's not clear to me how that
might look when futures are inherently global to the process.
9. Worse, it looks like when a future is touched, it's not really
joining the main execution thread. For example, when I run your
suggested example:
(for-each touch
(for/list ([i (in-range 0 (processor-count))])
(future loop)))
I get my 4 cores spinning away happily, but hitting break leaves
them *all* running. I'd expect the first future to stop as a
result. And with future values giving you only the ability to
touch, this means that your example is a way to render your
mzscheme process useless until it exits.
10. (This will be a long one.)
The "unsafe" primitives make some sense as far as using them.
(For example, it's clear that using any IO will suspend the
future.)
But there is no mention at all of what happens when there are
potential race conditions. Is writing to memory going to block a
future in any way? How is reading handled? Since this is not
documented, I ran some experiments. (My machine has two double
core Xeons cpus at 3ghz, in case it matters.)
First, I did this:
#lang scheme
(require scheme/future)
(define x
(let ([v (vector 0 0 0 0)])
(define ((spin n))
(let loop () (vector-set! v n (add1 (vector-ref v n))) (loop)))
(cons v (map (compose future spin) '(0 1 2 3)))))
(sleep 10)
x
(exit)
and the printout had similar numbers, between 53M and 58M.
Realizing that there's nothing useful I can do with the future
values, I changed it to:
(define x
(let ([v (vector 0 0 0 0)])
(define ((spin n))
(let loop () (vector-set! v n (add1 (vector-ref v n))) (loop)))
(for-each (compose future spin) '(0 1 2 3))
v))
and this produced very different numbers, with one counter being
at around 110M, two more around 50-60M, and one around 10M. The
high one was usually the first, and the low one was usually the
last. I couldn't really attribute this to delays in firing off
the futures, because one of the results was #(74M 49M 98M 10M).
Next I tried to rephrase it, to avoid delays in starting them as
soon as possible:
(define x
(let ([v (vector 0 0 0 0)])
(define (spin n)
(future
(lambda ()
(let loop () (vector-set! v n (add1 (vector-ref v n))) (loop)))))
(for-each spin '(0 1 2 3))
v))
and the results went back to all being around 60M. The *sum* of
all counters is roughly the same (around 230M), but I can't think
of any way the previous example would have such huge differences.
Next, I tried this:
(define x
(let ([v (vector 0 0)])
(define (spin n)
(future
(lambda ()
(let loop () (vector-set! v n (add1 (vector-ref v n))) (loop)))))
(for-each spin '(0 1 1 1))
v))
and the counts were 63M and 77M resp. This *looks* like reading
and writing from the same memory location is "safe", and you get
the usual races -- so the three cores that use the second counter
keep running into races of "read N, write N+1 after another thread
wrote N+1 or more", resulting in races mostly making the counts
the same, but with some "success rate" that leads to a higher
number.
If this is correct, and together with the fact that the usual
synchronization primitives in mzscheme are useless in futures
(since they seem to be "unsafe"), then this is a pretty important
thing to describe in the docs. Without being able to sync, then
people should be told very explicitly that the system avoids race
conditions that violate the mzscheme vm, but it knows nothing
about your own races, which means that you should be extra careful
to use futures for "very" independent tasks.
--
((lambda (x) (x x)) (lambda (x) (x x))) Eli Barzilay:
http://barzilay.org/ Maze is Life!