Using threads

Threads are a common building block for multitasking inside a single process. A thread is an independent stream of program execution that has access to the memory of other threads.

How to do it…

To start using threads, we will have to execute the following steps:

  1. Write the command import core.thread;.
  2. Write a function to perform your independent task.
  3. If the thread needs to pause, use the static method Thread.sleep from the thread to be paused.
  4. Create an additional thread with auto thread = new Thread, passing it as a pointer to your function.
  5. Call thread.start to begin execution.

Take a look at the following code:

import core.thread;
import core.atomic;

import std.stdio;

int count = 0;
shared(int) sharedCount = 0;

// this is the function that will run in our threads
void threadedFunction() {
  foreach(i; 0 .. 5) {
    writeln("thread running ", count++, " global: ", sharedCount);
    Thread.sleep(1.seconds);
    atomicOp!"+="(sharedCount, 1);
  }
}

void main() {
  auto thread = new Thread(&threadedFunction);
  thread.start();

  Thread.sleep(1500.msecs);

  auto thread2 = new Thread(&threadedFunction);
  thread2.start();
}

Running the program will result in the following interleaved output:

thread running 0 global: 0
thread running 1 global: 1
thread running 0 global: 1
thread running 2 global: 2
thread running 1 global: 3
thread running 3 global: 4
thread running 2 global: 5
thread running 4 global: 6
thread running 3 global: 7
thread running 4 global: 9

How it works…

The D runtime library encapsulates threads in a Thread class, one of which is created automatically when a program starts up, representing the program's main thread of execution. So, any operation that can be done by a child thread can also be done by the main thread, such as Thread.sleep, which pauses execution for the specified duration.

A thread's constructor takes a pointer to a function, which they execute to completion. This is similar to how the program as a whole runs main and terminates when it returns. A new thread is created in the paused state. To begin its execution, we must call its start method.

All started threads will run simultaneously and automatically spread out across multiple processor cores, if available. Multiple threads running simultaneously means nonatomic operations (that is, operations that consist of more than one machine instruction) may be interrupted at any time. Thus, working with shared data must be done carefully.

To alleviate the difficulty of working with shared data, D takes a different approach than most other languages. Whereas in C, for example, all global and static variables are assumed to be shared unless specifically marked as __thread, in D, all global and static variables are assumed to be thread-local unless specifically marked as shared. Shared then becomes part of the variable's type, forcing you to think about the ramifications of sharing data between threads before you use it.

Tip

You can also declare nontyped shared data with the __gshared storage class, which creates globally shared data without using the separate shared type. However, you should avoid this whenever possible because it gives up the additional checks that shared provides.

Our example program had both count and sharedCount to illustrate the difference. The count variable is thread-local, so each thread has an independent count and the variable can be used normally. The sharedCount variable, on the other hand, is shared across all threads and needs to be properly synchronized to avoid race conditions (where one thread reads a value, another thread writes a value, and the first thread writes back the old value, saving over the second thread's write).

To address this problem, instead of using the regular increment operator, we used core.atomic.atomicOp. An atomic operation is one that cannot be broken down into interruptible pieces—it is done all at once, without giving another thread a chance to intervene before it is finished. atomicOp takes an operation as a compile-time string argument, a piece of shared data to act upon, and the value by which to modify it.

In the next recipe, we'll look at a higher level of abstraction on top of the low-level threads seen here, which simplifies some of the details of data sharing.

There's more…

Threads in D, by default, are known to the garbage collector and will prevent the termination of the program until they are all completed.

To create a thread that doesn't prevent process termination, you want a daemon thread. Creating a daemon thread is easy: simply create a regular Thread and then set the isDaemon property to true, as shown in the following snippet:

  auto thread = new Thread(&threadedFunction);
  thread.isDaemon = true; // make it a daemon thread
  thread.start();

Now, if main returns before this thread is complete, the process will terminate immediately without waiting for the thread to finish.

Creating a thread that is not known to the garbage collector is more difficult and very tricky to get right. To do this, you will have to create a thread using operating system primitives like you would in C instead of the D library. Doing this is not recommended and should only be used when you are certain about what you are doing.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.199.122