- 13.1 Concurrentgate
- 13.2 A Brief History of Data Sharing
- 13.3 Look, Ma, No (Default) Sharing
- 13.4 Starting a Thread
- 13.5 Exchanging Messages between Threads
- 13.6 Pattern Matching with receive
- 13.7 File Copyingwith a Twist
- 13.8 Thread Termination
- 13.9 Out-of-Band Communication
- 13.10 Mailbox Crowding
- 13.11 The shared Type Qualifier
- 13.12 Operations with shared Data and Their Effects
- 13.13 Lock-Based Synchronization with synchronized classes
- 13.14 Field Typing in synchronized classes
- 13.15 Deadlocks and the synchronized Statement
- 13.16 Lock-Free Coding with shared classes
- 13.17 Summary
13.11 The shared Type Qualifier
We already got acquainted with shared in § 13.3 on page 397. To the type system, shared indicates that several threads have access to a piece of data. The compiler acknowledges that reality by restricting operations on shared data and by generating special code for the accepted operations.
The global definition
shared uint threadsCount;
introduces a value of type shared(uint), which corresponds to a global unsigned int in a C program. Such a variable is visible to all threads in the system. The annotation helps the compiler a great deal: the language "knows" that threadsCount is freely accessible from multiple threads and forbids naïve access to it. For example:
void bumpThreadsCount() { ++threadsCount; // Error! // Cannot increment a shared int! }
What's happening? Down at machine level, ++threadsCount is not an atomic operation; it's a read-modify-write operation: threadsCount is loaded into a register, the register value is incremented, and then threadsCount is written back to memory. For the whole operation to be correct, these three steps need to be performed as an indivisible unit. The correct way to increment a shared integer is to use whatever specialized atomic increment primitives the processor offers, which are portably packaged in the std.concurrency module:
import std.concurrency; shared uint threadsCount; void bumpThreadsCount() { // std.concurrency defines // atomicOp(string op)(ref shared uint, int) atomicOp!"+="(threadsCount, 1); // Fine }
Because all shared data is accounted for and protected under the aegis of the language, passing shared data via send and receive is allowed.
13.11.1 The Plot Thickens: shared Is Transitive
Chapter 8 explains why const and immutable must be transitive (aka deep or recursive): following any indirections starting from an immutable object must keep data immutable. Otherwise, the immutable guarantee has the power of a comment in the code. You can't say something is immutable "up to a point" after which it changes its mind. You can, however, say that data is mutable up to a point, where it becomes immutable through and through. Stepping into immutability is veering down a one-way street. We've seen that immutable facilitates a number of correct and pain-free idioms, including functional style and sharing of data across threads. If immutability applied "up to a point," then so would program correctness.
The same exact reasoning goes for shared. In fact, with shared the necessity of transitivity becomes painfully obvious. Consider:
shared int* pInt;
which according to the qualifier syntax (§ 8.2 on page 291) is equivalent to
shared(int*) pInt;
The correct meaning of pInt is "The pointer is shared and the data pointed to by the pointer is also shared." A shallow, non-transitive approach to sharing would make pInt "a shared pointer to non-shared memory," which would be great if it weren't untenable. It's like saying, "I'll share this wallet with everyone; just please remember that the money in it ain't shared."6 Claiming the pointer is shared across threads but the pointed-to data is not takes us back to the wonderful programming-by-honor-system paradigm that has failed so successfully throughout history. It's not the voluntary malicious uses, it's the honest mistakes that form the bulk of problems. Software is large, complex, and ever-changing, traits that never go well with maintaining guarantees through convention.
There is, however, a notion of "unshared pointer to shared data" that does hold water. Some thread holds a private pointer, and the pointer "looks" at shared data. That is easily expressible syntactically as
shared(int)* pInt;
As an aside, if there exists a "Best Form-Follows-Function" award, then the notation qualifier(type) should snatch it. It's perfect. You can't even syntactically create the wrong pointer type, because it would look like this:
int shared(*) pInt;
which does not make sense even syntactically because (*) is not a type (granted, it is a nice emoticon for a cyclops).
Transitivity of shared applies not only to pointers, but also to fields of struct and class objects: fields of a shared object are automatically qualified as shared as well. We'll discuss in detail the ways in which shared interacts with classes and structs later in this chapter.