c# concurrent and asynchronous III. Task

1, Thread limitations

1. Although it is not difficult to transfer data to the thread when it is started, it is difficult to get the "return value" after the thread joins. Usually you have to create some shared fields (to get the "return value").

2. It is also very troublesome to catch and handle exceptions thrown by operations in threads.

3. After the thread completes, it cannot be started again. On the contrary, it can only be joined (and block the current operation thread).

These limitations affect the stable implementation of fine-grained concurrency. It will increase the dependence on manual synchronization processing (such as using locks, signaling, etc.), and it is easy to cause problems.

Using threads directly also has an impact on performance. Moreover, if you need to run a large number of concurrent I/O-Intensive operations, the thread based method will consume hundreds of gigabytes of memory only in terms of the overhead of the thread itself.

2, Task task

1. Task Brief

Compared with threads, Task is a higher-level abstract concept. It represents a concurrent operation, which does not necessarily depend on threads.

Task s can be composed (you can concatenate them through continuation operations). They can use thread pool to reduce startup delay, or use TaskCompletionSource to use callback to avoid multiple threads waiting for I/O-Intensive operations at the same time.

Task type is also the basic type of C# asynchronous function.

2. Start task

2.1 method I

The easiest way to start a thread based Task is to use Task Run (the Task class is under the System.Threading.Tasks namespace) static method. When calling, only one Action delegate needs to be passed in.

Task.Run (() => Console.WriteLine ("This is a Task"));

By default, tasks use threads in the thread pool, which are background threads. This means that when the main thread ends, all tasks will stop.

        Task.Run will return a task object, which can be used to monitor the execution process of the task. You can use the Status attribute of a task to track its execution Status.

2.2 method II

TaskCompletionSource can create a task, which is rarely used.

This task is not the kind of task that needs to be started and then stopped; It is a "satellite" task that is created manually at the end of the operation or when an error occurs. This is ideal for I/O-Intensive work. It not only takes advantage of all the advantages of the task (the ability to pass return values, exceptions, or continuations), but also does not need to block threads during operation execution.

static async void Run()
{
    var tcs = new TaskCompletionSource<bool>();
 
    var fireAndForgetTask = Task.Delay(5000)
                                .ContinueWith(task => tcs.SetResult(true));
 
    await tcs.Task;
}

3. Wait method

Calling the Wait method of the Task can block the current method until the Task is completed, which is similar to calling the Join method of the thread object.

Task task = Task.Run (() =>
{
  Console.WriteLine("Task start");
  Thread.Sleep (2000);
  Console.WriteLine ("Output content");
});
Console.WriteLine(task.IsCompleted);  // False
task.Wait();

4. Long task

By default, the CLR will run the task on the thread pool thread, which is very suitable for performing short and computationally intensive tasks. If you want to perform long-term blocking operations (such as the above example), you can avoid using thread pool threads in the following ways:

Task task = Task.Factory.StartNew (() =>
{
  Console.WriteLine ("The thread started");
  Thread.Sleep (2000);
  Console.WriteLine ("Thread output");
}, TaskCreationOptions.LongRunning);

task.Wait();  // Wait until the thread completes

Running a long-running task on the thread pool does not cause problems; However, if you want to run multiple long-running tasks in parallel (especially those that will cause blocking), it will have an impact on performance.

In this case, compared with using taskcreationoptions For longrunning, a better solution is:

If you are running I/O-Intensive tasks, use TaskCompletionSource and asynchronous functions to achieve concurrency through callback functions rather than threads.

If tasks are computationally intensive, the use of Producer / consumer queues can control the number of concurrency caused by these tasks and avoid thread and process starvation.

5. Return value

Task has a generic subclass task < tresult >, which allows the task to return a value. If you are calling task When running, a func < tresult > delegate (or a compatible Lambda expression) is passed in instead of Action to obtain a task < tresult > object.

Task<int> task = Task.Run (() => { Console.WriteLine ("Task output"); return 3; });

//The return value of the task can be obtained by querying the Result property. If the current task is not completed, calling this property will block the current thread until the task ends.
int result = task.Result;      // Block if not completed
Console.WriteLine (result);    // 3

Another example with a return value

Task<int> primeNumberTask = Task.Run (() =>
  Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));

Console.WriteLine ("Task running...");
Console.WriteLine ("The answer is " + primeNumberTask.Result);

This code will output "Task running..." Then output the answer 216815 after a few seconds.

6. Exception handling

If an error occurs during the task, the exception will be thrown again when calling Wait() or accessing the Result attribute of task < tresult >.

Task task = Task.Run (() => { throw null; });
try 
{
  task.Wait();
}
catch (AggregateException aex)
{
  if (aex.InnerException is NullReferenceException)
    Console.WriteLine ("Null!");
  else
    throw;
}

7. Continuation

Continuation is usually implemented by a callback method that executes after the operation is completed.

1. Calling the GetAwaiter method of the task will return an awaiter object. The OnCompleted method of this object tells the antecedent task (primeNumberTask) to call a delegate when it finishes executing (or has an error).

Task<int> primeNumberTask = Task.Run (() =>
  Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));

var awaiter = primeNumberTask.GetAwaiter();
awaiter.OnCompleted (() => 
{
  int result = awaiter.GetResult();
  Console.WriteLine (result);       // Writes result
});

2. Another way to attach continuations is to call the ContinueWith method of the Task object. The ContinueWith method itself will return a Task object, so it is very suitable for adding more continuations.

Task<int> primeNumberTask = Task.Run (() =>
  Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));

primeNumberTask.ContinueWith (antecedent => 
{
  int result = antecedent.Result;
  Console.WriteLine (result);          // Writes 123
});

Keywords: C# Task

Added by prc on Fri, 21 Jan 2022 20:10:31 +0200