- 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.6 Pattern Matching with receive
Most useful communication protocols are more complex than the one we defined above, and receiveOnly is quite limited. For example, it is quite difficult to implement with receiveOnly an action such as "receive an int or a string."
A more powerful primitive is receive, which matches and dispatches messages based on their type. A typical call to receive looks like this:
receive( (string s) { writeln("Got a string with value ", s); }, (int x) { writeln("Got an int with value ", x); } );
The call above matches any of the following send calls:
send(tid, "hello"); send(tid, 5); send(tid, 'a'); send(tid, 42u);
The first send call matches a string and is therefore dispatched to the first function literal in receive, and the other three match an int and are passed to the second function literal. By the way, the handler functions don't need to be literals—some or all of them may be addresses of named functions:
void handleString(string s) { ... } receive( &handleString, (int x) { writeln("Got an int with value ", x); } );
Matching is not exact; instead, it follows normal overloading rules, by which char and uint are implicitly convertible to int. Conversely, the following calls will not be matched:
send(tid, "hello"w); // UTF-16 string (§ 4.5 on page 118) send(tid, 5L); // long send(tid, 42.0); // double
When receive sees a message of an unexpected type, it doesn't throw an exception (as receiveOnly does). The message-passing subsystem simply saves the non-matching messages in a queue, colloquially known as the thread's mailbox. receive waits patiently for the arrival of a message of a matching type in the mailbox. This makes receive and the protocols implemented on top of it more flexible, but also more susceptible to blocking and mailbox crowding. One communication misunderstanding is enough for a thread's mailbox to accumulate messages of the wrong type while receive is waiting for a message type that never arrives.
The send/receive combo handles multiple arguments easily by using Tuple as an intermediary. For example:
receive( (long x, double y) { ... }, (int x) { ... } );
matches the same messages as
receive( (Tuple!(long, double) tp) { ... }, (int x) { ... } );
A call like send(tid, 5, 6.3) matches the first function literal in both examples above.
To allow a thread to take contingency action in case messages are delayed, receive has a variant receiveTimeout that expires after a specified time. The expiration is signaled by receiveTimeout returning false:
auto gotMessage = receiveTimeout( 1000, // Time in milliseconds (string s) { writeln("Got a string with value ", s); }, (int x) { writeln("Got an int with value ", x); } ); if (!gotMessage) { stderr.writeln("Timed out after one second."); }
13.6.1 First Match
Consider the following example:
receive( (long x) { ... }, (string x) { ... }, (int x) { ... } );
This call will not compile: receive rejects the call because the third handler could never be reached. Any int sent down the pipe stops at the first handler.
In receive, the order of arguments dictates how matches are attempted. This is similar, for example, to how catch clauses are evaluated in a try statement but is unlike object-oriented function dispatch. Reasonable people may disagree on the relative qualities of first match and best match; suffice it to say that first match seems to serve this particular form of receive quite well.
The compile-time enforcement performed by receive is simple: for any message types <Msg1> and <Msg2> with <Msg2>'s handler coming after <Msg1>'s in the receive call, receive makes sure that <Msg2> is not convertible to <Msg1>. If it is, that means <Msg1> will match messages of type <Msg2> so compilation of the call is refused. In the example above, the check fails when <Msg1> is long and <Msg2> is int.
13.6.2 Matching Any Message
What if you wanted to make sure you're looking at any and all messages in a mailbox—for example, to make sure it doesn't get filled with junk mail?
The answer is simple—just accept the type Variant in the last position of receive, like this:
receive( (long x) { ... }, (string x) { ... }, (double x, double y) { ... }, ... (Variant any) { ... } );
The Variant type defined in module std.variant is a dynamic type able to hold exactly one value of any other type. receive recognizes Variant as a generic holder for any message type, and as such a call to receive that has a handler for Variant will always return as soon as at least one message is in the queue.
Planting a Variant handler at the bottom of the message handling food chain is a good method to make sure that stray messages aren't left in your mailbox.