- 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.4 Starting a Thread
To start a thread, use the spawn function like this:
import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; spawn(&fun, low, high); foreach (i; low .. high) { writeln("Main thread: ", i); } } void fun(int low, int high) { foreach (i; low .. high) { writeln("Secondary thread: ", i); } }
The spawn function takes the address of a function &fun and a number of arguments <a1>, <a2>, ..., <an>. The number of arguments n and their types must match fun's signature, that is, the call fun (<a1>, <a2>, ..., <an>) must be correct. This check is done at compile time. spawn creates a new execution thread, which will issue the call fun (<a1>, <a2>, ..., <an> ) and then terminate. Of course, spawn does not wait for the thread to terminate—it returns as soon as the thread is created and the arguments are passed to it (in this case, two integers).
The program above outputs a total of 200 lines to the standard output. The interleaving of lines depends on a variety of factors; it's possible that you would see 100 lines from the main thread followed by 100 lines from the secondary thread, the exact opposite, or some seemingly random interleaving. There will never be, however, a mix of two messages on the same line. This is because writeln is defined to make each call atomic with regard to its output stream. Also, the order of lines emitted by each thread will be respected.
Even if the execution of main may end before the execution of fun in the secondary thread, the program patiently waits for all threads to finish before exiting. This is because the runtime support library follows a little protocol for program termination, which we'll discuss later; for now, let's just note that other threads don't suddenly die just because main returns.
As promised by the isolation guarantee, the newly created thread shares nothing with the caller thread. Well, almost nothing: the global file handle stdout is de facto shared across the two threads. But there is no cheating: if you look at the std.stdio module's implementation, you will see that stdout is defined as a global shared variable. Everything is properly accounted for in the type system.
13.4.1 immutable Sharing
What kind of functions can you call via spawn? The no-sharing stance imposes certain restrictions—you may use only by-value parameters for the thread starter function (fun in the example above). Any pass by reference, either explicit (by use of a ref parameter) or implicit (e.g., by use of an array) should be verboten. With that in mind, let's take a look at the following rewrite of the example:
import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; auto message = "Yeah, hi #"; spawn(&fun, message, low, high); foreach (i; low .. high) { writeln("Main thread: ", message, i); } } void fun(string text, int low, int high) { foreach (i; low .. high) { writeln("Secondary thread: ", text, i); } }
The rewritten example is similar to the original, but it prints an additional string. That string is created in the main thread and passed without copying into the secondary thread. Effectively, the contents of message are shared between the two threads. This violates the aforementioned principle that all data sharing must be explicitly marked through the use of the shared keyword. Yet the example compiles and runs. What is happening?
Chapter 8 explains that immutable provides a strong guarantee: an immutable value is guaranteed never to change throughout its lifetime. The same chapter explains (§ 8.2 on page 291) that the type string is actually an alias for immutable(char)[]. Finally, we know that all contention is caused by sharing of writable data—as long as nobody changes it, you can share data freely as everybody will see the exact same thing. The type system and the entire threading infrastructure acknowledge that fact by allowing all immutable data to be freely sharable across threads. In particular, string values can be shared because their characters can't be changed. In fact, a large part of the motivation behind introducing immutable into the language was the help it brings with sharing structured data across threads.