The book "Competition in C #. Asynchronous, parallel and multithreaded programming. 2nd int. ed. "

imageHello, habrozhiteli! If you are afraid of competitive and multithreaded programming, this book is written for you. Stephen Cleary has 85 recipes for working with .NET and C # 8.0 for parallel processing and asynchronous programming. Competition has already become the accepted method for developing highly scalable applications, but concurrent programming remains a daunting task. Detailed examples and comments on the code will help to understand how modern tools increase the level of abstraction and simplify competitive programming. You will learn how to use async and await for asynchronous operations, expand the capabilities of the code through the use of asynchronous threads, explore the potential of parallel programming with the TPL Dataflow library,create data stream pipelines with the TPL Dataflow library, use LINQ-based System.Reactive functionality, use thread-safe and immutable collections, conduct unit testing of competitive code, take control of the thread pool, implement correct cooperative cancellation, analyze scripts to combine competitive methods , use all the features of asynchronously compatible object-oriented programming, recognize and create adapters for code that uses old styles of asynchronous programming.analyze scripts to combine competitive methods, use all the features of asynchronously compatible object-oriented programming, recognize and create adapters for code that uses old styles of asynchronous programming.analyze scripts to combine competitive methods, use all the features of asynchronously compatible object-oriented programming, recognize and create adapters for code that uses old styles of asynchronous programming.

Basics of Parallel Programming


4.1. Parallel data processing


Task


There is a collection of data. You must perform the same operation with each data item. This operation is computationally limited and may take some time.

Decision


The Parallel type contains the ForEach method, designed specifically for this task. The following example gets a collection of matrices and rotates these matrices:

void RotateMatrices(IEnumerable<Matrix> matrices, float degrees)
{
   Parallel.ForEach(matrices, matrix => matrix.Rotate(degrees));
}

There may be situations in which it is necessary to abort the cycle prematurely (for example, if an invalid value is detected). The following example reverses each matrix, but if an invalid matrix is ​​found, the loop will be interrupted:

void InvertMatrices(IEnumerable<Matrix> matrices)
{
   Parallel.ForEach(matrices, (matrix, state) =>
   {
      if (!matrix.IsInvertible)
        state.Stop();
      else
        matrix.Invert();
   });
}

This code uses ParallelLoopState.Stop to stop the loop and prevent any further calls to the loop body. Keep in mind that the cycle is parallel, therefore, other calls to the body of the cycle may already be made, including calls for elements following the current one. In the given code example, if the third matrix is ​​not reversible, then the cycle is interrupted and new matrices will not be processed, but it may turn out that other matrices are already being processed (for example, the fourth and fifth).

A more common situation occurs when you need to cancel a parallel loop. This is not the same as stopping a cycle; the cycle stops from within and is canceled outside. For example, the cancel button can cancel the CancellationTokenSource, canceling the parallel loop, as in the following example:

void RotateMatrices(IEnumerable<Matrix> matrices, float degrees,
      CancellationToken token)
{
   Parallel.ForEach(matrices,
         new ParallelOptions { CancellationToken = token },
         matrix => matrix.Rotate(degrees));
}

It should be borne in mind that each parallel task can be performed in a different thread, therefore, any joint state must be protected. The following example inverts each matrix and counts the number of matrices that could not be inverted:

// :     .
//      
//    .
int InvertMatrices(IEnumerable<Matrix> matrices)
{
  object mutex = new object();
  int nonInvertibleCount = 0;
  Parallel.ForEach(matrices, matrix =>
  {
     if (matrix.IsInvertible)
    {
       matrix.Invert();
    }
    else
    {
       lock (mutex)
      {
         ++nonInvertibleCount;
      }
    }
  });
  return nonInvertibleCount;
}

Explanation


The Parallel.ForEach method provides parallel processing for a sequence of values. A similar Parallel LINQ (PLINQ) solution provides almost the same features in LINQ-like syntax. One of the differences between Parallel and PLINQ is that PLINQ assumes that it can use all the cores on the computer, while Parallel can dynamically respond to changing processor conditions.

Parallel.ForEach implements a parallel foreach loop. If you need to execute a parallel for loop, the Parallel class also supports the Parallel.For method. The Parallel.For method is especially useful when working with multiple arrays of data that receive a single index.

Additional Information


Recipe 4.2 discusses the parallel aggregation of a series of values, including the summation and calculation of averages.

Recipe 4.5 covers the basics of PLINQ.

Chapter 10 deals with cancellation.

4.2. Parallel aggregation


Task


It is required to aggregate the results at the end of the parallel operation (examples of aggregation - summing values ​​or calculating the average).

Decision


To support aggregation, the Parallel class uses the concept of local values ​​— variables that exist locally within a parallel loop. This means that the loop body can simply access the value directly, without the need for synchronization. When the loop is ready to aggregate all of its local results, it does this using the localFinally delegate. It should be noted that the localFinally delegate does not need to synchronize access to the variable to store the result. Example of parallel summation:

// :     .
//      
//    .
int ParallelSum(IEnumerable<int> values)
{
  object mutex = new object();
  int result = 0;
  Parallel.ForEach(source: values,
        localInit: () => 0,
        body: (item, state, localValue) => localValue + item,
        localFinally: localValue =>
       {
          lock (mutex)
             result += localValue;
       });
  return result;
}

Parallel LINQ provides more comprehensive aggregation support than the Parallel class:

int ParallelSum(IEnumerable<int> values)
{
   return values.AsParallel().Sum();
}

Okay, that was a cheap trick because PLINQ has built-in support for many common operators (like Sum). PLINQ also provides generic aggregation support with the Aggregate operator:

int ParallelSum(IEnumerable<int> values)
{
  return values.AsParallel().Aggregate(
        seed: 0,
        func: (sum, item) => sum + item
  );
}

Explanation


If you are already using the Parallel class, you should use its aggregation support. In other cases, PLINQ support is usually more expressive, and the code is shorter.

Additional Information


Recipe 4.5 outlines the basics of PLINQ.

4.3. Parallel call


Task


There is a set of methods that should be called in parallel. These methods are (mostly) independent of each other.

Decision


The Parallel class contains a simple Invoke method designed for such scenarios. In the following example, the array is split in two, and the two halves are processed independently:

void ProcessArray(double[] array)
{
   Parallel.Invoke(
         () => ProcessPartialArray(array, 0, array.Length / 2),
         () => ProcessPartialArray(array, array.Length / 2, array.Length)
   );
}

void ProcessPartialArray(double[] array, int begin, int end)
{
   // ,   ...
}

You can also pass an array of delegates to the Parallel.Invoke method if the number of calls is unknown before execution:

void DoAction20Times(Action action)
{
   Action[] actions = Enumerable.Repeat(action, 20).ToArray();
   Parallel.Invoke(actions);
}

Parallel.Invoke supports cancellation, like other methods of the Parallel class:

void DoAction20Times(Action action, CancellationToken token)
{
   Action[] actions = Enumerable.Repeat(action, 20).ToArray();
   Parallel.Invoke(new ParallelOptions { CancellationToken = token },
        actions);
}

Explanation


The Parallel.Invoke method is a great solution for a simple parallel call. I note that it is not so well suited for situations in which you want to activate an action for each input data element (for this it is better to use Parallel.ForEach), or if each action produces some output (Parallel LINQ should be used instead).

Additional Information


Recipe 4.1 discusses the Parallel.ForEach method, which performs an action for each data item.

Recipe 4.5 deals with Parallel LINQ.

about the author


Stephen Cleary , an experienced developer, has gone from ARM to Azure. He contributed to the open source Boost C ++ library and released several proprietary libraries and utilities.

»More information about the book can be found on the publisher’s website
» Contents
» Excerpt

For Khabrozhiteley 25% discount on the coupon - Cleary

Upon payment of the paper version of the book, an electronic book is sent by e-mail.

All Articles