Walter is right about transitive readonly - here's the alternative

Janice Caron caron800 at googlemail.com
Thu Sep 13 02:34:54 PDT 2007


Walter is right about transitive readonly - but we need something
instead. I believe this is that something!

As other threads have shown, there are a squillion use-cases which
seem to need either transitive const or mutable members. (The two
concepts are to some extent interchangeable).

However, as Walter has pointed out, such declarations make it possible
(...and in fact, easy...) to write thread-unsafe code. If I have
understood the arguments correctly, this is the primary reason why
Walter wants to outlaw them.

So, here's a better alternative. Let's do thread-safety _properly_.

We're going to need three new keywords, and a slightly new way of doing things.

So, let's take this one step at a time...

First off, we introduce a new storage class, whose meaning is
"threadsafe". I'm going to use the keyword "shared" for that. It will
have some very precise rules and features, so I'll explain one thing
at a time.

Let's start with a class, C, defined as follows

 class A
 {
     int x;
     int y;
 }

Nothing new there. Now lets use our new storage class:

 class B
 {
     A a;
     shared A shared_a;
 };

Observe the new keyword shared. The first thing implied by that
keyword is that all of the member variables of shared_a are now
completely hidden, as though the entire contents of A had been
declared private and B were in another module. That means...

 B b;
 int n = b.a.x; /* OK */
 int n = b.a.y; /* OK */
 int n = b.shared_a.x; /* Error */
 int n = b.shared_a.y; /* Error */

You cannot access the member variables of something declared shared -
not even to /read/. To do so is a compile time error. (And I stress,
compile time. It's nice to have your thread-safety ensured at
compile-time).

So how do you read B's shared member variables? Well, that's where the
second new keyword comes in - "readable"

 scope a = readable(b.shared_a);
 int n = a.x; /* OK - reads b.shared_a.x */
 int m = a.y; /* OK - reads b.shared_a.y */

Observe that a is a scope variable. That's because readable obtains a
shared-access mutex, which must be released when a goes out of scope
by any means. The expression "readable(x)" is more or less equivalent
to "new ReadLock!(typeof(x))(x)". In C++, I would overload the ->
operator of the resulting class, but you can't do that in D, which is
why doing this at the language level is a really cool solution.

But you still can't write to b.shared_a.

 a.x = 4; /* Error - a is readonly */
 a.y = 4; /* Error - a is readonly */

To write to b.shared_a, you need the third keyword. You guessed it - "writable".

 scope a = writable(b.shared_a);
 int n = a.x; /* OK - reads b.shared_a.x */
 a.y = n; /* OK - assigns b.shared_a.y */

Again, a is a scope variable. That's because writable obtains an
exclusive-access mutex, which must be released when a goes out of
scope by any means. As with readable, the expression "writable(x)" is
more or less equivalent to "new WriteLock!(typeof(x))(x)". In C++, I
would overload the -> operator of the resulting class, but you can't
do that in D, which, again, is why doing this at the language level is
a cool solution.

To recap...

It is a compile-time error to refer to the member variables of
b.a_shared directly. Instead, you must obtain a second reference to
b.a_shared, using either readable(b) or writable(b), and you can
access the member variables /only/ through those. The locks are scoped
so that the mutexes always get released.

Now

- - - - how does this help with const? - - - -

Because shared variables are threadsafe, it is now harmless regard
them as mutable. This allows to to re-introduce the concept of
"logical constness" to D ... but only in a threadsafe way. Like this:

 class Shape
 {
     shared class Raster raster_s;
     abstract void draw();
 }

 class Rectangle
 {
     int x0, y0, x1, y1;

     void draw()
     {
         scope raster = writable(raster_s);
         raster.drawRectangle(x0,y0,x1,y1);
     }
 }

 const Rectangle r;
 r.draw(); /* OK */

Voila!

There is no longer any need for the keyword "mutable". (Just use
"shared" instead). There is no longer any need for const transitivity.
(Just use "shared" as needed).

- - - - you still need to be careful - - - -

There is no magic bullet to thread-safety. This mechanism does ensure
that multiple threads won't try to access the same variable at the
same time, but what it /won't/ do is prevent deadlocks. It's also
still possible to screw up. For instance - you cannot get a write lock
if you already hold a lock...

 scope a = readable(x);
 scope b = writable(x); /* Error - b is already locked by this thread */

That error is not always detectable by the compiler.

And you have to be aware that other threads may modify shared things
when they're not locked. For example, my lookup-from-file class
becomes

 class Lookup
 {
     shared int[int] map_s;

     int readonly lookup(int key)
     {
         {
             auto map = readable(map_s);
             if (key in map) return map[key];
         }
         {
             auto map = writable(map_s);
             if (key in map) return map[key]; /* pay attention */

             /* read stuff from file and store it in map */

             return map[key];
         }
     }
 }

See how I had to check the cache twice? Once while readable, then
again while writable? That's because, between the unlocking of the
read lock and the locking of the write lock, another thread could have
got in and updated the map.

- - - - conclusion - - - -

This is a viable, threadsafe alternative to both intransitive const
and logical const.

It allows us to build all of the real-world use-cases that have been
suggested a need for intransitive const or logical const.

You get three new keywords:
* shared
* readable
* writable

You have to learn a new discipline to use these features, but it's not that hard



More information about the Digitalmars-d mailing list