C# Multithreading (14): Basics of Tasks II

2020年4月28日 78点热度 0人点赞 0条评论
内容目录

In the previous article, we learned the basics of tasks, various ways to schedule and execute tasks, and how to asynchronously retrieve return results. This article will focus solely on code practice and example operations.

Judging Task Status

| Property | Description |
|--------|--------|
| IsCanceled | Gets a value indicating whether this Task instance has completed due to being canceled. |
| IsCompleted | Gets a value indicating whether the task has completed. |
| IsCompletedSuccessfully | Indicates whether the task has completed successfully. |
| IsFaulted | Gets a value indicating whether this Task has completed due to an unhandled exception. |
| Status | Gets the TaskStatus of this task. |

To detect if a task has failed (meaning it has terminated due to an unhandled exception), you should use the IsCanceled and IsFaulted properties. If the task throws an exception, IsFaulted will be true. However, canceling a task actually throws an OperationCancelException, which does not imply that the task failed.

Even when a task throws an unhandled exception, it is still considered complete, so the IsCompleted property will be true.

Here is an example:

The code is somewhat lengthy, making it difficult to view; please copy it to your program and run it.

class Program
{
    static void Main()
    {
        // Normal task
        Task task1 = new Task(() =>
        {
        });
        task1.Start();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        GetResult(task1.IsCanceled, task1.IsFaulted);
        Console.WriteLine("Task completed: " + task1.IsCompleted);
        Console.WriteLine("-------------------");

        // Exception task
        Task task2 = new Task(() =>
        {
            throw new Exception();
        });
        task2.Start();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        GetResult(task2.IsCanceled, task2.IsFaulted);
        Console.WriteLine("Task completed: " + task2.IsCompleted);
        Console.WriteLine("-------------------");
        Thread.Sleep(TimeSpan.FromSeconds(1));

        CancellationTokenSource cts = new CancellationTokenSource();
        // Cancel the task
        Task task3 = new Task(() =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(3));
        }, cts.Token);
        task3.Start();
        cts.Cancel();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        GetResult(task3.IsCanceled, task3.IsFaulted);
        Console.WriteLine("Task completed: " + task3.IsCompleted);
        Console.ReadKey();
    }

    public static void GetResult(bool isCancel, bool isFault)
    {
        if (!isCancel && !isFault)
            Console.WriteLine("No exception occurred.");
        else if (isCancel)
            Console.WriteLine("Task was canceled.");
        else
            Console.WriteLine("Task raised an unhandled exception.");
    }
}

On Parent-Child Tasks

In the previous article “C# Multithreading (13): Task Basics I”, we learned about parent-child tasks, where the parent task must wait for the child task to complete for it to be considered finished.

The last chapter only provided examples but did not clarify scenarios and experimental results, so here is a new example for clarification.

Non-parent-child tasks:

The outer task does not wait for the embedded task to complete and simply completes or returns a result.

static void Main()
{
    // Two tasks have no hierarchical relationship; they are independent
    Task<int> task = new Task<int>(() =>
    {
        // Not a child task
        Task task1 = new Task(() =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            for (int i = 0; i < 10; i++)
            {
            });
        });

        task1.Start();
        return 0;
    });
}

Parent-child tasks:

The parent task waits for the completion of the child task to be considered complete, then returns the result.

static void Main()
{
    // Parent-child task
    Task<int> task = new Task<int>(() =>
    {
        // Child task
        Task task1 = new Task(() =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            for (int i = 0; i < 10; i++)
            {
            });
        });

        task1.Start();
        task1.Wait(); // Wait for the child task to complete
        return 0;
    });
}

Combination Tasks/Continuation Tasks

The Task.ContinueWith() method creates a continuation task that executes asynchronously upon the completion of a Task instance.

There are many overloaded methods for Task.ContinueWith(), which you can refer to here: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith?view=netcore-3.1#--

Here is the constructor we are using defined as follows:

public Task ContinueWith(Action<task> continuationAction);

A simple example:

Task task = new Task(() =>
{
    Console.WriteLine("First Task.");
    Thread.Sleep(TimeSpan.FromSeconds(2));
});

// Next, the second task
task.ContinueWith(t =>
{
    Console.WriteLine("Second Task.");
    Thread.Sleep(TimeSpan.FromSeconds(2));
});
task.Start();

file

A single task can have multiple continuation tasks that run in parallel, for example:

static void Main()
{
    Task task = new Task(() =>
    {
        Console.WriteLine("First Task.");
        Thread.Sleep(TimeSpan.FromSeconds(1));
    });

    // Task 1
    task.ContinueWith(t =>
    {
        for (int i = 0; i < 10; i++)
        {
        });
    });

    task.Start();
}

file

By implementing continuation/combination tasks multiple times, you can achieve a robust task workflow.

Complex Continuation Tasks

After the last section, we learned about ContinueWith() for continuing tasks; now let's explore more overload methods to implement more complex continuations.

There are numerous overloads for ContinueWith(), which parameters include one or more of the following.

  • continuationAction

    Type: Action or Func

A task to execute.

  • state

    Type: Object

The parameter passed to the continuation task.

  • cancellationToken

    Type: CancellationToken

A cancellation token.

  • continuationOptions

    Type: TaskContinuationOptions

Controls the creation and characteristics of the continuation task.

  • scheduler

    Type: TaskScheduler

A TaskScheduler associated with the continuation task and used during its execution process.

The first four parameters (types) have already been covered in previous articles, so we will not elaborate further; the TaskScheduler type will be explained later.

Note the distinction between TaskCreationOptions and TaskContinuationOptions; we studied TaskCreationOptions in the previous article. Here we will study TaskContinuationOptions.

TaskContinuationOptions can be used in the following overloads:

ContinueWith(Action, CancellationToken, TaskContinuationOptions, TaskScheduler);
ContinueWith(Action, TaskContinuationOptions);

Using it like this in continuations is ineffective:

Task task = new Task(() =>
{
    Console.WriteLine("First Task.");
    Thread.Sleep(TimeSpan.FromSeconds(1));
});
task.ContinueWith(t =>
{
    for (int i = 0; i < 10; i++)
    {
    });

Because TaskContinuationOptions requires a nested hierarchical parent-child relationship for it to work.

Correct usage method:

static void Main()
{
    // Parent-child task
    Task<int> task = new Task<int>(() =>
    {
        // Child task
        Task task1 = new Task(() =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("Inner task 1");
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }, TaskCreationOptions.AttachedToParent);

        task1.ContinueWith(t =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("Inner continuation task, also a child task.");
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }, TaskContinuationOptions.AttachedToParent);

        task1.Start();

        Console.WriteLine("Outer layer task");
        return 666;
    });

    task.Start();
    Console.WriteLine($"Task result is: {task.Result}");
    Console.WriteLine("\n-------------------\n");

    Console.ReadKey();
}

file

Parallel (Asynchronous) Task Processing

Here we will learn about the usage of the Task.WhenAll() method.

Task.WhenAll(): Waits for all provided Task objects to complete execution.

The usage is as follows:

static void Main()
{
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 5; i++)
    {
        tasks.Add(new Task(() =>
        {
            Console.WriteLine("Task started executing.");
        }));
    }

    // public static Task WhenAll(IEnumerable<Task> tasks);

    // Equivalent to multiple tasks generating a single task
    Task taskOne = Task.WhenAll(tasks);
    // If you don't need to wait, you can skip this
    taskOne.Wait();

    Console.ReadKey();
}

Task taskOne = Task.WhenAll(tasks); can be written as Task.WhenAll(tasks);, and the returned Task object can be used to check the execution status of the tasks.

Note that the following is ineffective:

You can modify the above code to test.

    tasks.Add(new Task(() =>
    {
        Console.WriteLine("Task started executing.");
    }));

I also don't know why new Task() doesn't work...

If tasks have return values, you can use the following method:

static void Main()
{
    List<Task<int>> tasks = new List<Task<int>>();

    for (int i = 0; i < 5; i++)
    {
        tasks.Add(new Task<int>(() =>
        {
            Console.WriteLine("Task started executing.");
            return new Random().Next(0, 10);
        }));
    }

    Task<int[]> taskOne = Task.WhenAll(tasks);

    foreach (var item in taskOne.Result)
        Console.WriteLine(item);

    Console.ReadKey();
}

Parallel (Synchronous) Task Processing

Task.WaitAll(): Waits for all provided Task objects to complete execution.

Let’s take a look at one of the overloaded method definitions for Task.WaitAll():

public static bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
  • tasks Type: Task[]

All tasks to be executed.

  • millisecondsTimeout Type: Int32

The number of milliseconds to wait; -1 means wait indefinitely.

  • cancellationToken Type: CancellationToken

The CancellationToken to observe while waiting for tasks to complete.

An example of Task.WaitAll() is as follows:

static void Main()
{
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 5; i++)
    {
        tasks.Add(new Task(() =>
        {
            Console.WriteLine("Task started executing.");
        }));
    }

    Task.WaitAll(tasks.ToArray());

    Console.ReadKey();
}

Task.WaitAll() will make the current thread wait for all tasks to complete. Additionally, Task.WaitAll() does not have generics and does not return results.

Task.WhenAny for Parallel Tasks

Task.WhenAny() and Task.WhenAll() are similar in use. Task.WhenAll() completes only when all tasks are finished, while Task.WhenAny() completes as soon as any one of the tasks finishes.

This can be referenced in the parent-child tasks discussed above.

Here is a sample usage:

        static void Main()
        {
            List<task> tasks = new List<task>();

            for (int i = 0; i < 5; i++)
            {
                tasks.Add(Task.Run(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(0, 5)));
                    Console.WriteLine("正在执行任务");
                }));
            }
            Task taskOne = Task.WhenAny(tasks);
            taskOne.Wait(); // Any one task completes, and we can release the wait.

            Console.WriteLine("有任务已经完成了");

            Console.ReadKey();
        }

Of course, Task.WhenAny() also has a generic method that can return results.

Parallel Task Status

The Task.Status property can retrieve the status of a task. Its property type is a TaskStatus enumeration defined as follows:

| Enumeration | Value | Description |
|-------------|-------|-------------|
| Canceled | 6 | The task has been canceled via CancellationToken. |
| Created | 0 | The task has been initialized, but not yet scheduled. |
| Faulted | 7 | The task completed due to an unhandled exception. |
| RanToCompletion | 5 | The task completed successfully. |
| Running | 3 | The task is currently running but not yet completed. |
| WaitingForActivation | 1 | The task is waiting for the .NET Framework infrastructure to internally activate and schedule it. |
| WaitingForChildrenToComplete | 4 | The task has completed execution and is implicitly waiting for additional child tasks to complete. |
| WaitingToRun | 2 | The task has been scheduled but has not yet started executing. |

When using parallel tasks, the Task.Status value has certain patterns:

  • If any of the tasks experiences an unhandled exception, it will return TaskStatus.Faulted.
  • If all tasks have unhandled exceptions, it will return TaskStatus.RanToCompletion.
  • If any task is canceled (even if it has unhandled exceptions), it will return TaskStatus.Canceled.

Value Change Issues in Loops

Please run the tests for the following two examples:

        static void Main()
        {
            for (int i = 0; i < 5; i++)
            {
                Task.Run(() =>
                {
                    Console.WriteLine($"i = {i}");
                });
            }

            Console.ReadKey();
        }
        static void Main()
        {
            List<task> tasks = new List<task>();

            for (int i = 0; i < 5; i++)
            {
                tasks.Add(Task.Run(() =>
                {
                    Console.WriteLine($"i = {i}");
                }));
            }
            Task taskOne = Task.WhenAll(tasks);
            taskOne.Wait();

            Console.ReadKey();
        }

You will find that the results of the two examples are not 1,2,3,4,5, but rather 5,5,5,5,5.

This issue is known as a Race Condition. You can refer to Wikipedia:

https://en.wikipedia.org/wiki/Race_condition

There is also an explanation of this issue in the Microsoft documentation, please refer to:

https://docs.microsoft.com/zh-cn/archive/blogs/ericlippert/closing-over-the-loop-variable-considered-harmful

Since i resides in the same memory location throughout its lifecycle, each thread or task points to the same location for its value.

This works:

        static void Main()
        {
            for (int i = 0; i < 5; i++)
            {
                int tmp = i; // Capture the value of i
                Task.Run(() =>
                {
                    Console.WriteLine($"i = {tmp}");
                });
            }

            Console.ReadKey();
        }

This is ineffective:

            for (int i = 0; i < 5; i++)
            {
                Task.Run(() =>
                {
                    Console.WriteLine($"i = {i}");
                });
            }

Timed Tasks with TaskScheduler Class

The TaskScheduler class represents an object that handles scheduling tasks to threads on a low level.

Most examples online are related to WPF and WinForm, and the Microsoft documentation appears quite complicated: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskscheduler?view=netcore-3.1#properties

It seems the TaskScheduler mainly controls the SynchronizationContext, meaning it influences the UI.

Since the author does not write WPF or WinForms, this topic will be skipped. Ha ha ha.

痴者工良

高级程序员劝退师

文章评论