- 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.5 Exchanging Messages between Threads
Threads that print messages with arbitrary interleavings are hardly interesting. Let's modify the example to ensure that threads work in tandem to print messages as follows:
Main thread: 0 Secondary thread: 0 Main thread: 1 Secondary thread: 1 ... Main thread: 999 Secondary thread: 999
To achieve that, we need to define a little protocol between the two threads: the main thread should send the message "Print this number" to the secondary thread, and the secondary thread must answer back, "Done printing." There is hardly any concurrency going on, but the example serves well the purpose of explaining pure communication. In real applications, threads should spend most of their time doing useful work and spend relatively little time communicating with each other.
First off, in order for two threads to communicate, they need to know how to address each other. A program may have many threads chattering away, so an identification means is necessary. To address a thread, you must get a grip on its thread id, nicknamed henceforth as "tid," which is returned by spawn. (The name of a tid's type is actually Tid.) In turn, the secondary thread also needs a tid to send the response back. That's easy to do by having the sender specify its own Tid the same way you'd write the sender's address on a snail mail envelope. Here's what the code looks like:
import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; auto tid = spawn(&writer); foreach (i; low .. high) { writeln("Main thread: ", i); tid.send(thisTid, i); enforce(receiveOnly!Tid() == tid); } } void writer() { for (;;) { auto msg = receiveOnly!(Tid, int)(); writeln("Secondary thread: ", msg[1]); msg[0].send(thisTid); } }
This time around writer takes no more arguments because it receives the information it needs in the form of messages. The main thread saves the Tid returned by spawn and then uses it in the call to the send method. The call sends two pieces of data to the other thread: the current thread's Tid, accessed via the global property thisTid, and the integer to be printed. After throwing that data over the fence to the other thread, the main thread waits for acknowledgment in the form of a call to receiveOnly. The send and receiveOnly functions work in tandem: one call to send in one thread is met by a call to receiveOnly in the other. The "only" in receiveOnly is present because receiveOnly accepts only specific types—for example, in the call receiveOnly!bool(), the caller accepts only a message consisting of a bool value; if another thread sends anything else, receiveOnly throws a MessageMismatch exception.
Let's leave main rummaging around the foreach loop and focus on writer's implementation, which implements the other side of the mini-protocol. writer spends time in a loop starting with the receipt of a message that must consist of a Tid and an int. That's what the call receiveOnly!(Tid, int)() ensures; again, if the main thread sent a message with some different number or types of arguments, receiveOnly would fail by throwing an exception. As written, the receiveOnly call in writer matches perfectly the call tid.send(thisTid, i) made from main.
The type of msg is Tuple!(Tid, int). Generally, messages with multiple arguments are packed in Tuple objects with one member per argument. If, however, the message consists only of one value, there's no redundant packing in a Tuple. For example, receiveOnly!int() returns an int, not a Tuple!int.
Continuing with writer, the next line performs the actual printing. Recall that for the tuple msg, msg[0] accesses the first member (i.e., the Tid) and msg[1] accesses the second member (the int). Finally, writer acknowledges that it finished writing to the console by simply sending its own Tid back to the sender—a sort of a blank letter that only confirms the originating address. "Yes, I got your message," the empty letter implies, "and acted upon it. Your turn." The main thread waits for that confirmation before continuing its work, and the loop goes on.
Sending back the Tid of the secondary thread is superfluous in this case; any dummy value, such as an int or a bool, would have sufficed. But in the general case there are many threads sending messages to one another, so self-identification becomes important.