This is an old revision of the document!


Lab 05: Multithreading

Concurrency

Most modern microprocessors consist of more than one core, each of which can operate as an individual processing unit. They can execute different parts of different programs at the same time.

A flow of execution through certain parts of a program is called a thread of execution or a thread. Programs can consist of multiple threads that are being actively executed at the same time. The operating system starts and executes each thread on a core and then suspends it to execute other threads, thus each thread is competing with the other threads in the system for computational time on the processor. The execution of each thread may involve many cycles of starting and suspending.

All of the threads of all of the programs that are active at a given time are executed on the very cores of the microprocessor. The operating system decides when and under what condition to start and suspend each thread. We call this process of start/suspend/swap a context switch.

The features of the std.parallelism module make it possible for programs to take advantage of all of the cores in order to run faster.

std.parallelism.Task

Operations that are executed in parallel with other operations of a program are called tasks. Tasks are represented by the type std.parallelism.Task.

Task represents the fundamental unit of work. A Task may be executed in parallel with any other Task. Using this struct directly allows future/promise parallelism. In this paradigm, a function (or delegate or other callable) is executed in a thread other than the one it was called from. The calling thread does not block while the function is being executed.

For simplicity, the std.parallelism.task and std.parallelism.scopedTask functions are generally used to create an instance of the Task struct.

Using the Task struct has three steps:

1. First, we need to create a task instance.

int anOperation(string id) {
  writefln("Executing %s", id);
  Thread.sleep(1.seconds);
  return 42;
}
 
void main() {
  /* Construct a task object that will execute
   * anOperation(). The function parameters that are
   * specified here are passed to the task function as its
   * function parameters. */
   auto theTask = task!anOperation("theTask");
   /* the main thread continues to do stuff */
}

2. Now we've just created a new Task instance, but the task isn't running yet. Next we'll launch the task execution.

  /* ... */
  auto theTask = task!anOperation("theTask");
 
  theTask.executeInNewThread(); // start task execution
  /* ... */

3. At this point we are sure that the operation has been started, but it's unsure whether theTask has completed its execution. yieldForce() waits for the task to complete its operations; it returns only when the task has been completed. Its return value is the return value of the task function, i.e. anOperation().

  /* ... */
  immutable taskResult = theTask.yieldForce();
  writefln("All finished; the result is %s\n", taskResult);
  /* ... */

The Task struct has two other methods, workForce and spinForce, that are used to ensure that the Task has finished executing and to obtain the return value, if any. Read their docs and discover the differences in behaviour and when their usage is preferred.

std.parallelism.TaskPool

As we've previously stated: all of the threads of all of the programs that are active at a given time are executed on the very cores of the microprocessor, competing for computational time with each other.

This observation has the following implication: on a system that has N cores, we can have at most N threads running in parallel at a given time. This means that in our application we should create at most N worker threads that will execute tasks (from a tasks queue) for us, thus our N worker threads will be part of a thread pool; this is a common pattern used in concurrent applications.

The std.parallelism.TaskPool gives us access to a task pool implementation to which we can submit std.parallelism.Tasks to be executed by the worker threads.

The std.parallelism module gives us access to a ready to use std.parallelism.TaskPool instance, named std.parallelism.taskPool. std.parallelism.taskPool has totalCPUs - 1 worker threads available, where totalCPUs is the total number of CPU cores available on the current machine, as reported by the operating system. The minus 1 is included because the main thread will also be available to do work.

dss/laboratoare/05.1561459085.txt.gz ยท Last modified: 2019/06/25 13:38 by eduard.staniloiu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0