[m-rev.] for review: Add future datatype for concurrent and parallel programming
Paul Bone
paul at bone.id.au
Wed Oct 8 16:13:31 AEDT 2014
On Tue, Oct 07, 2014 at 02:36:45PM +1100, Peter Wang wrote:
> > +
> > +:- module thread.future.
> > +:- interface.
> > +
> > + % future/1 represents a value that is evaluated in a separate thread
> > + % (using spawn/3).
> > + %
> > +:- type future(T).
> > +
>
> I think the description should be more generic and not mention threads.
> Perhaps
>
> future/1 represents a value that may yet to be computed.
>
It's not a lazy value. It is intended for use with threads, unlike lazy.m.
I propose:
% future/1 represents a value that may not have been computed yet.
% Future values are usually computed by separate threads (using
% spawn/3).
%
:- type future(T).
Note that if you're not using multiple (spawn/3) threads, then there's no
point using futures because wait will block until a value is provided, it
will not force evaluation.
> > +%-----------------------------------------------------------------------------%
> > +
> > + % Create a new empty future.
> > + %
> > +:- pred init(future(T)::uo, io::di, io::uo) is det.
> > +
> > + % Provide a value for the future and signal any waiting threads. Any
> > + % further calls to wait will return immediatly.
> > + %
> > +:- pred signal(future(T)::in, T::in, io::di, io::uo) is det.
>
> immediately
>
> What happens if it is signalled multiple times? Probably throw an
> exception.
Undefined behaviour, don't do it!
I've added a comment to this effect and some code to detect this and throw
an exception if detected. However, a race can occur preventing the
detection from working and therefore I cannot say that an exception will be
thrown. So "undefined behaviour" will be the documented behaviour, but
hopefully if anybody tries to do this they'll find out via the exception.
> > +%-----------------------------------------------------------------------------%
> > +
> > + % Create a future which has the value that the closure, when evaluated,
> > + % will produce.
> > + %
> > +:- func future((func) = T) = future(T).
>
> The implicit spawn should at least be mentioned.
>
> What happens if the function throws an exception? I guess the user is
> responsible for catching exceptions and returning them.
Since I've separated futures into two different types (below), it's now easy
to trap exceptions for this type of future and re-throw them when wait is
called. So, if you create a future with a closure that throws an exception,
that exception will be re-thrown out of the call to wait.
> It might be worth differentiating the two types of futures by a type
> parameter.
Okay, I had considered that and wondered what other people thought. I'll go
ahead and do that.
> > +
> > + % Wait is pure because it always returns the same value for the same
> > + % future (if it terminates).
> > + %
> > +:- pred wait(future(T)::in, T::out) is det.
> > +:- pragma promise_pure(wait/2).
> > +
> > +wait(Future, Value) :-
> > + Future = future(MReady, Wait, MValue),
> > + impure get_mutvar(MReady, Ready),
> > + (
> > + Ready = ready
> > + % No wait necessary
> > + ;
> > + Ready = not_ready,
> > + % We need to wait, this will probably block.
> > + impure wait(Wait),
> > + % Signal the semaphore to release the next waiting thread.
> > + impure signal(Wait)
> > + ),
> > + impure get_mutvar(MValue, Value).
>
> Is it not possible that you see Read = ready but the old (uninitialised)
> value of MValue? I expect futures will usually be read exactly once so
> the optimisation seems unnecessary.
You're right. It's easily fixable bo moving the write to Ready after the
signal(Wait) as the signal is a memory fence. This will ensure that the
write to MValue will always be visible before or at the same time as the
write to Ready.
> So, we would have lazy(T), mvar(T), future(T) with three different
> interfaces, and mutvars acting as unsynchronised mvars with another
> interface.
Yes, each has different semantics even though they seem similar.
--
Paul Bone
More information about the reviews
mailing list