> There's still some discussion to be had on what putMVar should
> do when presented with a full MVar. The options are
>
> 1. throw an exception (as now)
> 2. block until the MVar is empty
> 3. succeed, replacing the current value in the MVar
> 4. succeed, adding the new value to a buffer in the MVar
>
> (1) is easy to implement. (2) is more convenient occasionally,
> but can always be implemented with an extra MVar. (3) is also
> more convenient in certain cases (imagine an MVar that held
> mouse movement events), but again can be implemented with extra
> MVars. (4) adds some complication to the MVar implementation.
This is one aspect of the Concurrent Haskell design I have never
been happy with - I would much prefer (2) over (1), the original
CH paper also mentioned ordered (4a) and unordered buffering
(4b). Here are some arguments for the discussion:
a. Ease of implementation should never be the first argument;-)
b. In contrast to other things, that can be built on top of the
current MVars, (1) is not only inconvenient, but unsafe,
unless (and in a sense even if, see (d)) you always protect
each use of putMVar with an exception handler (do you?).
c. Exceptions should be reserved for exceptional circumstances.
Trying to execute putMVar on an MVar that has been filled
already doesn't look exceptional to me in the presence of
shared access from other concurrent, unsynchronised processes.
It takes explicit programming to ensure that this situation
cannot occur (or is handled correctly), but if this
programming is needed in each and every case, it should be
reused by incorporating it into the implementation (or into a
library - why isn't everybody using CVars, btw?).
Why would you want to treat as an exception a condition that
occurs frequently and could be handled safely and implicitly
by other means, such as (2), or CVars?
d. With (2), scheduling determines which of two conflicting
writers gets to fill an MVar, and the loser simply blocks
until the MVar is empty again. With (1), however, scheduling
determines **whether or not there will be an exception**: if a
reader is scheduled on a filled MVar before the second writer,
everything is fine, whereas if the second writer is scheduled
before any readers, you'll have an exception. Such things are
fun to debug..
e. With the original CH semantics, exceptions would propagate to
the top-level, absorbing the whole process network. Combine
this with (d), and I do not understand why anyone would have
wanted (1) in the first place (apart from supporting the case
for exception handling?-).
Exception handling can help here, but without grouping of
processes, exception handling can only be global (which would
mean losing your current process network) or immediate in each
process that tries putMVar. In fact, I would guess that in
almost all cases the exception handling code is right next to
the putMVar, so why use a non-local language construct
(exceptions) for dealing with a perfectly localised situation?
Of course, this assumes that you never forget to handle your
exceptions.. If you `forgetī your exception handling, you
will have a system that may or may not die at any time, giving
bugs that are hard to reproduce (or might even go unnoticed
for a while..).
f. there are lots of similarities between CH and Petri nets, and
to get leverage from the popularity of Petri nets and from the
experience of the Petri net community, it seems advisable not
to introduce unnecessary differences. Petri nets tend to use
(2), or an unlimited capacity variant of (2) (mostly (4b)
without, sometimes (4a) with ordering of buffer elements).
Unordered, unlimited capacity buffers are a nice high-level
abstraction, and limited capacity buffers can easily be
modelled with pairs of unlimited capacity buffers, but as long
as the outcome of conflicting putMVars is non-deterministic,
single-place buffers should be fine for CH, and they are
easier to implement efficiently;-)
The point is: with (2), you could take persons used to
coloured Petri nets (CPNs) and tell them that CH is very
similar to what they have been using in their projects for
ages. There is added flexibility in CH, and added structural
stability in CPNs - but CH can be embedded in CPNs easily, and
more interestingly, an implementation of (a variant of) CPNs
in CH could support a nice graphical frontend for CH..
So I can see lots of reasons against (1) and only one in favour
of (1) - easy implementation.
The main reason why I did not speak up before is that the
original CH paper seemed to suggest that MVars would only be used
at the most basic implementation level, whereas users and
libraries would almost certainly use safe variants (e.g., CVars)
built, in CH, on top of MVars. This would have achieved ease of
implementation while also protecting users from the dangers of
(1). Unfortunately, the present discussion of MVar properties
shows that if a feature is easily accessible, some hackers will
use it!-)
So I would now suggest either to change the implementation from
(1) to (2), or to discourage strongly all external use of MVars
and to shift the present discussion from MVar features to
features of abstractions built on top of them. In other words,
what abstractions and operations need to be provided so that
noone will need to use MVars directly?
But I admit that I do not have practical experience with CH, so
perhaps I am missing the point, or there are practical reasons
why anyone would desperately want to have a variant of putMVar
that can throw exceptions at the will of the scheduler?
Claus
PS. As for tryTakeMVar or locks on MVars, what is wrong
with using MMVar a = (MVar (MayBe a)) and a suitable
access protocol?
MVar empty --> MMVar is locked
MVar Nothing --> MMVar is empty, not locked
MVar (Just v) --> MMVar holds value v, not locked
The only danger would be someone else filling the MVar while
it is empty, but making MMVar abstract should avoid that, no?