A type system that prevents data races must not eliminate useful concurrency patterns or force the programmer to maintain multiple copies of almost identical data structures.
In my previous post about Guava, a dialect of Java, I talked about a type system that enforces the separation of thread-local and shared objects at the class level. Unfortunately, such rigid system forces the programmer (especially a library writer) to provide dual implementations of many generic classes like vectors, queues, etc. Often the only difference between implementations is the use of synchronized (or, in case of Guava, the special base class called Monitor).
To solve this problem, Boyapati and Rinard proposed a system where the same generic class may be used in different sharing contexts. For instance, the same parameterized vector class, may be used to instantiate a thread-local instance that requires no locking, as well as a shared instance that has a built-in lock.
The paper precedes Generic Java, but employs similar notation. For instance, it lets you define a generic vector class like this:
class vector<thisOwner> { ... }
and then instantiate it as thread-local (no locking requirements):
vector<thisThread> localV= new vector<thisThread>;
or as a shared monitor:
vector<self> sharedV= new vector<self>;
The first template parameter is always interpreted as “the owner” (more about it later). Objects owned by thisThread are thread-local, objects owned by self are monitors.
Even though the notation used in GRFJ (the acronym the authors use for their Java dialect) is different from that of Guava, there are many similarities in the two approaches, since both have to deal with similar issues: explicit sharing, ownership, passing objects between threads, etc.
-Explicit sharing
You might remember that, in Guava, only those classes that inherit from Monitor may be shared. In GRFJ, sharing is defined at the instance level (the instantiation of a template). Every instance declaration must specify the owner of the object. If the owner is not thisThread, the object may be shared between threads. The equivalent of the Guava Monitor is a self-owned object–its owner is declared as self.
-Ownership
Ownership plays an essential role in preventing data races. Every object has an owner. In GRFJ there are three basic types of owners:
- thisThread–the object owned by thisThread is never shared.
- self–the object is the root of an ownership tree.
- Another object–the sharing is defined by the root of the ownership tree
Types of ownership translate naturally into protection patterns. If the owner is thisThread there is no need for locking. If the owner is self, all methods must be synchronized by the object’s lock. In the third case, the object’s owner is responsible for locking. More precisely, the root of the ownership tree to which the object belongs has to be locked, if it’s not declared thread-local.
There are some interesting combinations of ownership. For instance, you can declare a thread-local vector that stores self-owned (shared) items. Or you can declare a shared Stack that contains (owns) a Vector object. All access to Vector will be protected by the Stack’s lock.
For this level of expressiveness, we need classes that are parameterized by owners. Notice that, if the owner of the object x is another object, that object must exist when the declaration of x is reached.
A template might be parameterized by multiple owners. The first one on the list is always the owner of this. In the example below, Stack is parameterized by two owners–the first owns the Stack, the second owns the Values. Note that, in this case, all Values will always share the same owner.
class Stack<thisOwner, valueOwner> { Node<thisOwner, valueOwner> head = null; void push(Value<valueOwner> value) requires (this) { Node<thisOwner, valueOwner> newNode = new Node<thisOwner, valueOwner>; newNode.init(value, head); head = newNode; } Value<valueOwner> pop() requires (this) { if (head == null) return null; Value<valueOwner> value = head.value(); head = head.next(); return value; } }
The requires clause specifies whose lock must be held when calling a particular method. In this case, the lock on this must be held. But if this is owned by another object, the locking responsibility automatically moves up a level, until it reaches the ownership root.
Here’s the definition of Node:
class Node<thisOwner, valueOwner> { Value<valueOwner> value; Node<thisOwner, valueOwner> next; void init(Value<valueOwner> v, Node<thisOwner, valueOwner> n) requires (this) { this.value = v; this.next = n; } Value<valueOwner> value() requires (this) { return value; } Node<thisOwner, valueOwner> next() requires (this) { return next; } }
And the definition of Value:
class Value<thisOwner> { int x = 0; }
Using the declarations above we are now ready to declare different values and stacks:
Value<thisThread> v1 = new Value<thisThread>; Value<self> v2 = new Value<self>;
We have created two values from the same template–v1 is thread-local, v2 is a monitor (access to x is protected by its lock).
Stack<thisThread, thisThread> s1 = new Stack<thisThread, thisThread>; Stack<thisThread, self> s2 = new Stack<thisThread, self>; Stack<self, self> s3 = new Stack<self, self>;
Stack s1 is thread-local and can store only thread-local values. No locks or locking code will be created by the compiler. Stack s2 is also thread-local, but it stores shareable values. A thread-local stack will never be visible from other threads. But self-owned values it stores might be accessed from multiple threads. Finally, s3 is a shared stack containing shared values. Both, the stack s3 and the values it stores have their own independent locks.
s1.push(v1); s2.push(v2);
We may push a thread-local value v1 on the stack s1, but if we tried to push v2 on s1, the compiler would consider it an error. Pushing v2 on s2, on the other hand, is okay.
Since GRFJ is based on Concurrent Java, the locking and threading look a little odd. To illustrate the sharing of s3, we fork a thread, passing it s3 and v2 (both are sharing-ready) and executing the code s3.push(v2) under the lock of s3.
fork (s3,v2) {synchronized (s3) in {s3.push(v2);}}
Notice that, according to the declaration of s3, it would be an error to push v1 onto it. Indeed, that could result in illegal sharing of a thread-local object. The type system protects us from a hard-to-detect error.
This is hardly a free lunch, though. Annotating every class and every variable might be just too much for a programmer. Fortunately, most owners can be inferred by the compiler by analyzing assignments. Because of that, single threaded programs in GRFJ require virtually no annotations.
-Foreign owners
In most cases, ownership tree follows the containment tree. The owner contains the ownee. Although desirable from the architectural point of view, this arrangement is not strictly necessary. An object might declare another separate object as its owner. This is safe under one condition–the owner object may not be overwritten. Hence the requirement that the owner be final. Here’s the relevant example:
final Foo<self> owner = new Foo<self>; Bar<owner> ownee = new Bar<owner>;
This becomes important when building new locking protocols from pre-existing parts.
-Object migration
The passing of objects between threads requires either deep copying (like Guava’s Values), or move semantics. In GRFJ, move semantics is implemented by specifying another special type of owner–unique. Unique objects cannot be copied, they have to be moved. The “move” operator is the postfix decrement, just like in Guava. It moves the reference and nulls the source.
Value<unique> v3 = new Value<unique> Value<unique> v4 = v3--;
Our Stack class is not prepared to store unique objects. This prohibition may be included in its definition (compare it with C++ “concepts”):
class Stack<thisOwner, valueOwner> where (valueOwner != unique)
Conversely, we might want to redefine Stack to store unique objects. A few code changes would be necessary though. For instance, in push, the value must be moved:
newNode.init(value--, head);
In pop, the return value has to be moved:
return value--;
The class Node requires similar changes.
The authors note that, despite appearances, the moving of objects is multiprocessor safe. Even though the assignment to a new reference is not guaranteed to be immediately visible to other threads, a unique object is always published to another thread via a monitor (for instance, a shared stack). The new thread can only get hold of the object by first acquiring the monitor’s lock, which forces previous stores to commit.
-Alias control
Move semantics requires control over aliasing–a moved object may not leave any aliases, nor may it carry along any references to thread-local objects. GRFJ provides additional annotations to mark non-escaping method parameters. The syntax is !e appended to the parameter type. Here’s an example:
void display(Value<unique>!e val); Value<unique> v5 = new Value<unique>; display(v5);
A unique object here is passed by reference only because display guarantees that its argument won’t escape.
-Immutable objects
Immutable objects may be passed between threads by reference. In GRFJ, immutable objects are marked by another special owner type, readonly. Interestingly, the problem of the construction of immutable objects is cleverly avoided. You first create a unique object and then move it to a readonly reference.
Value<unique> v6 = new Value<unique>; v6.x = 1; Value<readonly> v7 = v6--;
This is a perfectly safe operation, since there is a guarantee that no writable aliases to the unique object may stay behind. The move to readonly freezes the object forever.
Immutable objects can only be passed to methods that promise not to modify their arguments. This is done by appending !w to the type of the parameter. The two suffixes may be combined to form !ew (I am not kidding you!), a parameter that doesn’t escape and is not modified by the method.
-Limitations
– Some concurrent programs use multi-stage access patterns that are not expressible in GRFJ. For instance, a shared array is divided into disjoint sections and each thread operates exclusively on its section without any locking. After all threads synchronize on a barrier, they pick up different sections and continue. The ownership of sections changes with time. (In D this pattern might be implementable using array slices.)
– Dynamic downcasting, which used to be the workhorse of Java before generics, can’t verify the ownership part of the cast, because this information is not available at runtime.
– Static variables may be accessed only when the client holds the class lock. Each class with static variables must therefore have a static lock.
– The authors mention the idea of parameterizing methods that accept poly-owned arguments. This is not so easy as it sounds, since virtual functions cannot be parameterized by a potentially infinite set of types. My guess is that this is possible because detailed ownership information is only needed during type checking. Still, the compiler might have to produce additional implementations of a method depending on whether the parameter is thread-local or not (imagine the parameterized method creating a local variable of the same ownership type–sometimes this variable must contain a lock, sometimes not). Still, this is a finite set of possibilities, so vtable slots may be preallocated for all of them.
– The template syntax for ownership won’t work in languages where templates already accept value parameters, and the compiler isn’t always able to distinguish between types and values.
May 26, 2009 at 9:57 am
[…] also my previous posts on Guava and GRFJ that discuss race free dialects of Java. Possibly related posts: (automatically […]
May 27, 2009 at 8:52 am
[…] also my previous blog on Generic Race-Free Java. Possibly related posts: (automatically generated)const Correctness: Difference between Foo* const […]
June 15, 2009 at 12:58 pm
[…] The polymorphic scheme and the examples are based on the GRFJ paper I discussed in a past post. […]
September 10, 2015 at 10:10 pm
Thank you very much for the posts.
I would be interested in the first limitation you mentioned here. Do you have a hint, how to implement this? Maybe some further literature for this?
September 13, 2015 at 7:55 pm
@Alex: I think that in many cases the problem may be solved by picking a different data structure instead of one monolithic array. For instance you could try a list of smaller arrays that are uniquely owned by different threads. When the parallel processing is finished, the chunks may be moved to one owner. It’s not perfect, but it will work.