C# Multi-threading (15): Basics of Tasks III

2020年4月29日 52点热度 0人点赞 3条评论
内容目录

The basics of tasks consist of three articles. This is the third article, and afterward, we will begin learning about asynchronous programming, concurrency, and asynchronous I/O.

This article will continue to discuss some APIs and common operations associated with Task.

TaskAwaiter

Let's talk about TaskAwaiter. TaskAwaiter represents an object that waits for an asynchronous task to complete and provides the result.

Task has a GetAwaiter() method, which returns either TaskAwaiter or TaskAwaiter<TResult>. The TaskAwaiter type is defined in the System.Runtime.CompilerServices namespace.

The properties and methods of the TaskAwaiter type are as follows:

Properties:

| Property | Description |
|----------|-------------|
| IsCompleted | Gets a value that indicates whether the asynchronous task has completed. |

Methods:

| Method | Description |
|--------|-------------|
| GetResult() | Ends the waiting for the asynchronous task to complete. |
| OnCompleted(Action) | Specifies an operation to be executed when the TaskAwaiter object stops waiting for the asynchronous task to complete. |
| UnsafeOnCompleted(Action) | Schedules continuation operations associated with this awaiter. |

An example of usage is shown below:

        static void Main()
        {
            Task<int> task = new Task<int>(() =>
            {
                Console.WriteLine("I am the predecessor task");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            TaskAwaiter<int> awaiter = task.GetAwaiter();

            awaiter.OnCompleted(() =>
            {
                Console.WriteLine("I will continue executing when the predecessor task is completed");
            });
            task.Start();

            Console.ReadKey();
        }

Additionally, we mentioned earlier that if an unhandled exception occurs in a task, or if the task is terminated, it is still considered a completed task.

Another Method for Continuation

In the previous section, we introduced the .ContinueWith() method to implement continuation. Here, we will discuss another method for continuation: .ConfigureAwait().

The .ConfigureAwait() method takes a Boolean value where true attempts to marshal the continuation back to the original context; otherwise, it is false.

Let me explain: the continuation task of .ContinueWith() will execute on the same thread as the predecessor task once it completes. This method is synchronous, meaning both tasks run sequentially on the same thread.

The .ConfigureAwait(false) method enables asynchronous execution; after the predecessor task completes, the subsequent task can run on any thread without regard for the original context. This feature is particularly useful in UI applications.

You can refer to: https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f

Here is how it is used:

        static void Main()
        {
            Task<int> task = new Task<int>(() =>
            {
                Console.WriteLine("I am the predecessor task");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();

            awaiter.OnCompleted(() =>
            {
                Console.WriteLine("I will continue executing when the predecessor task is completed");
            });
            task.Start();

            Console.ReadKey();
        }

ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter has the same properties and methods as TaskAwaiter.

Another difference between .ContinueWith() and .ConfigureAwait(false) is that the former can continue multiple tasks and continuation tasks (multi-layer), whereas the latter can only continue one layer of tasks (though one layer can contain multiple tasks).

Another Way to Create Tasks

As previously mentioned, there are three ways to create tasks: new Task(), Task.Run(), and Task.Factory.StartNew(). Now, we will learn about the fourth method: the TaskCompletionSource<TResult> type.

Let's take a look at the properties and methods of the TaskCompletionSource<TResult> type:

Properties:

| Property | Description |
|----------|-------------|
| Task | Gets the Task created by this TaskCompletionSource. |

Methods:

| Method | Description |
|--------|-------------|
| SetCanceled() | Transitions the underlying Task to the Canceled state. |
| SetException(Exception) | Transitions the underlying Task to the Faulted state with a specified exception. |
| SetException(IEnumerable) | Transitions the underlying Task to the Faulted state with a collection of specified exceptions. |
| SetResult(TResult) | Transitions the underlying Task to the RanToCompletion state. |
| TrySetCanceled() | Attempts to transition the underlying Task to the Canceled state. |
| TrySetCanceled(CancellationToken) | Attempts to transition the underlying Task to the Canceled state while enabling the stored cancellation token. |
| TrySetException(Exception) | Attempts to transition the underlying Task to the Faulted state with a specified exception. |
| TrySetException(IEnumerable) | Attempts to transition the underlying Task to the Faulted state with a collection of specified exceptions. |
| TrySetResult(TResult) | Attempts to transition the underlying Task to the RanToCompletion state. |

The TaskCompletionSource<TResult> class can control the lifecycle of a task.

First, access a Task or Task<TResult> through the .Task property.

            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task; // Task myTask = task.Task;

Then, control the lifecycle of myTask through the task.xxx() methods, but note that myTask itself does not have any task logic.

An example of use is below:

        static void Main()
        {
            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task;       // task controls myTask

            // Start a new task for experimentation
            Task mainTask = new Task(() =>
            {
                Console.WriteLine("I can control the myTask task");
                Console.WriteLine("Press any key, and I will immediately complete the myTask task");
                Console.ReadKey();
                task.SetResult(666);
            });
            mainTask.Start();

            Console.WriteLine("Starting to wait for the myTask result");
            Console.WriteLine(myTask.Result);
            Console.WriteLine("End");
            Console.ReadKey();
        }

Other methods such as SetException(Exception) can be explored on your own, and we won't elaborate on them here.

Reference materials: https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/

This article is quite good and includes illustrations: https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/

Implementing a Type that Supports Synchronous and Asynchronous Tasks

This section continues the explanation of TaskCompletionSource<TResult>.

Here we will design a class similar to Task that supports both synchronous and asynchronous tasks.

  • Users can use GetResult() for synchronous result retrieval;
  • Users can use RunAsync() to execute the task and Result property to retrieve the result asynchronously.

The implementation is as follows:

/// <summary>
/// Implement a type that supports synchronous and asynchronous tasks
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
    private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
    private Task<TResult> task;
    // Store the task the user wants to execute
    private Func<TResult> _func;

    // Whether the task has been completed, either synchronously or asynchronously
    private bool isCompleted = false;
    // Task execution result
    private TResult _result;

    /// <summary>
    /// Retrieve the execution result
    /// </summary>
    public TResult Result
    {
        get
        {
            if (isCompleted)
                return _result;
            else return task.Result;
        }
    }
    public MyTaskClass(Func<TResult> func)
    {
        _func = func;
        task = source.Task;
    }

    /// <summary>
    /// Synchronously retrieves result
    /// </summary>
    /// <returns></returns>
    public TResult GetResult()
    {
        _result = _func.Invoke();
        isCompleted = true;
        return _result;
    }

    /// <summary>
    /// Asynchronously executes the task
    /// </summary>
    public void RunAsync()
    {
        Task.Factory.StartNew(() =>
        {
            source.SetResult(_func.Invoke());
            isCompleted = true;
        });
    }
}

In the Main method, we create an example of a task:

    class Program
    {
        static void Main()
        {
            // Instantiate the task class
            MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // Directly get the result synchronously
            Console.WriteLine(myTask1.GetResult());

            // Instantiate the task class
            MyTaskClass<string> myTask2 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // Asynchronously get the result
            myTask2.RunAsync();

            Console.WriteLine(myTask2.Result);

            Console.ReadKey();
        }
    }

Task.FromCanceled()

According to Microsoft's documentation: it creates a Task that is completed due to a cancellation operation from a specified cancellation token.

Here, I have copied an example:

var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);

There are many such examples online, but what is this used for? Isn't new Task sufficient?

With this question in mind, let's explore it with an example:

        public static Task Test()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            source.Cancel();
            return Task.FromCanceled<object>(source.Token);
        }
        static void Main()
        {
            var t = Test(); // Set a breakpoint here to monitor variables
            Console.WriteLine(t.IsCanceled);
         }

Using Task.FromCanceled() allows you to construct a canceled task. I couldn't find a good example; it is beneficial if a task is canceled before it starts.

Many examples can be found here: https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken

How to Internally Cancel a Task

Previously, we discussed using CancellationToken to pass parameters to cancel a task. But this time, we will implement a way to cancel a task without using CancellationToken.

We can throw a System.OperationCanceledException exception using the ThrowIfCancellationRequested() method of the CancellationToken. This will terminate the task, putting it in a canceled state; however, the token must still be passed initially.

Here, I will design something slightly more complex: a class that can execute multiple tasks in order.

    /// <summary>
    /// An asynchronous type capable of completing multiple tasks
    /// </summary>
    public class MyTaskClass
    {
        private List<Action> _actions = new List<Action>();
        private CancellationTokenSource _source = new CancellationTokenSource();
        private CancellationTokenSource _sourceBak = new CancellationTokenSource();
        private Task _task;

        /// <summary>
        /// Adds a task
        /// </summary>
        /// <param name="action"></param>
        public void AddTask(Action action)
        {
            _actions.Add(action);
        }

        /// <summary>
        /// Starts executing tasks
        /// </summary>
        /// <returns></returns>
        public Task StartAsync()
        {
            // _ = new Task() is ineffective in this example
            _task = Task.Factory.StartNew(() =>
             {
                 for (int i = 0; i < _actions.Count; i++)
                 {
                     int tmp = i;
                     Console.WriteLine($"Task {tmp} started");
                     if (_source.Token.IsCancellationRequested)
                     {
                         Console.ForegroundColor = ConsoleColor.Red;
                         Console.WriteLine("Task has been canceled");
                         Console.ForegroundColor = ConsoleColor.White;
                         _sourceBak.Cancel();
                         _sourceBak.Token.ThrowIfCancellationRequested();
                     }
                     _actions[tmp].Invoke();
                 }
             }, _sourceBak.Token);
            return _task;
        }

        /// <summary>
        /// Cancels the task
        /// </summary>
        /// <returns></returns>
        public Task Cancel()
        {
            _source.Cancel();

            // This can be omitted
            _task = Task.FromCanceled<object>(_source.Token);
            return _task;
        }
    }

In the Main method:

        static void Main()
        {
            // Instantiate the task class
            MyTaskClass myTask = new MyTaskClass();

            for (int i = 0; i < 10; i++)
            {
                int tmp = i;
                myTask.AddTask(() =>
                {
                    Console.WriteLine("     Task 1 Start");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine("     Task 1 End");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                });
            }

            // Equivalent to Task.WhenAll()
            Task task = myTask.StartAsync();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine($"Is task canceled: {task.IsCanceled}");

            // Cancel the task
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Press any key to cancel the task");
            Console.ForegroundColor = ConsoleColor.White;
            Console.ReadKey();

            var t = myTask.Cancel();    // Cancel the task
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"Is task canceled: 【{task.IsCanceled}】");

            Console.ReadKey();
        }

You can cancel the task at any stage.

Yield keyword

The iterator keyword allows data to be returned one at a time when needed, which is also quite asynchronous.

When an iterator method reaches a yield return statement, it returns an expression and retains its position in the code. The next time the iterator function is called, execution resumes from that position.

You can use the yield break statement to terminate the iteration.

Official documentation: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield

Most of the online examples are with foreach, and some classmates do not understand what this really means. Here, the author briefly explains.

We can also write an example like this:

This time there is no foreach.

        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                i++;
                yield return list[i];
            }
        }

However, a student asked, does this return object need to implement IEnumerable<T>? What are those documents talking about regarding iterator interfaces?

We can modify the example a bit:


        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                int num = list[i];
                i++;
                yield return num;
            }
        }

Can you call this in the Main method and see if it runs normally?

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

This shows that the object returned by yield return does not need to implement the IEnumerable<int> method.

In fact, yield is syntactic sugar; you only need to call it within a loop.

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < 100)
            {
                i++;
                yield return i;
            }
        }
    }

It will automatically generate IEnumerable<T> without the need for you to implement IEnumerable<T> first.

Additional Knowledge points

  • There are various methods for thread synchronization: Critical Section, Mutex, Semaphores, Events, Tasks;
  • Task.Run() and Task.Factory.StartNew() wrap the Task;
  • Task.Run() is a simplified version of Task.Factory.StartNew();
  • In some cases, new Task() is ineffective; however, Task.Run() and Task.Factory.StartNew() can be used;

This article is the conclusion of the basics of tasks in C#. Up to now, the C# multithreading series has completed 15 articles, and I will continue to explore more usage methods and scenarios of multithreading and tasks in the future.

If you like my author, remember to follow me~

痴者工良

高级程序员劝退师

文章评论