(19)Task asynchronous: task creation, return value, exception capture, task cancellation, temporary variables

    

1. Creation of Task tasks


    1. Create in four ways, one for interface button and one for info.
      


        code

        private void BtnStart_Click(object sender, EventArgs e)
        {
            Task t = new Task(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]new Task.---1");
            });
            t.Start();

            Task.Run(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Run.---2");
            });

            TaskFactory tf = new TaskFactory();
            tf.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]TaskFactory.---3");
            });

            Task.Factory.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Factory.---4");
            });
        }

        private void DisplayMsg(string s)
        {
            if (TxtInfo.InvokeRequired)
            {
                BeginInvoke(new Action(() =>
                {
                    TxtInfo.AppendText($"{s}\r\n");
                }));
            }
            else
            {
                TxtInfo.AppendText($"{s}\r\n");
            }
        }


    
    
    2. What is the difference between Environment.CurrentManagedThreadId and Thread.CurrentThread.ManagedThreadID?
        Answer: In short: Use Environment.CurrentManagedThreadId.
            Environment.CurrentManagedThreadId and Thread.CurrentThread.ManagedThreadID are both used to obtain the unique identifier (Thread ID) of the current thread, but they have some differences.
            (1) Environment.CurrentManagedThreadId is a static property that can be accessed from anywhere, while Thread.CurrentThread.ManagedThreadID is an instance property that can only be accessed on the instance of the current thread.
            (2) Environment.CurrentManagedThreadId was introduced in .NET Framework 4.0 and later, while Thread.CurrentThread.ManagedThreadID was introduced in earlier versions.
            (3) Environment.CurrentManagedThreadId returns a thread ID of type int, and Thread.CurrentThread.ManagedThreadID returns a thread ID of type IntPtr.
            Overall, both can be used to get the unique identifier of the current thread, but Environment.CurrentManagedThreadId is more convenient and easier to use, especially in multi-threaded programming.
            
            warn:Do not put the parameter {Environment.CurrentManagedThreadId} in DisplayMsg, because the above four are all delegates. The extracted thread is the current thread. When delegated, the execution is on the UI thread. If it is placed in Display, it will all be extracted from the UI thread. This The ID number is usually 1.
    
    
    3. What are the differences between the above four asynchronous operations?
        Answer: There are four methods for creating and starting tasks. The differences are as follows:
            (1) new Task(); task.Start();: This is the most basic method of creating and starting tasks. First, use new Task() to create a new task object, and then call the Start() method to start the task. This method is a low-level and manual method, which requires manual management of the status and scheduling of tasks.
            (2)task.Run(): This is a simplified way introduced in .NET Framework 4.5 and later for creating and starting tasks. Use the Task.Run() method to directly create and start a task without manually calling the Start() method. This method is more concise and easier to use, suitable for most situations.
            (3)Task.Factory.StartNew(): This is a method of creating and starting tasks introduced in .NET Framework 4.0 and later. The Task.Factory.StartNew() method can accept a delegate as a parameter and create and start a task to execute the delegate. This method also provides some additional options, such as specifying the scheduler of the task, the type of task, etc.
            (4)f = new TaskFactory(); f.StartNew():This is how you use the TaskFactory class to create and start tasks. First, create a TaskFactory object, and then call the StartNew() method to create and start the task. The TaskFactory class provides more options and flexibility, such as setting the scheduler of the task, canceling the task, etc.
            Summary: Task.Run() is the most concise and commonly used method to create and start tasks, and is suitable for most situations. Task.Factory.StartNew() and TaskFactory.StartNew() provide more options and flexibility for situations where more control is required. new Task(); task.Start(); is a relatively low-level and manual method, and is generally not recommended unless there are special needs.
            
            
            Both Task.Run and TaskFactory.StartNew can be used to create and start a new task. The difference is:
            (1) The static method calling method is different:
            Task.Run creates and starts a task through a static method.
            TaskFactory.StartNew creates and starts a task by calling the StartNew method of the TaskFactory object through the instance method.
            (2) The default scheduler and the way to create a new scheduler are different:
            Task.Run uses the default scheduler in the current thread pool to plan the execution of the task.
            TaskFactory.StartNew can choose to use the default scheduler in the current thread pool, or use a custom task scheduler to plan task execution.
            (3)The return value types are different:
            Task.Run returns a generic Task<TResult> object that represents an operation that can be performed asynchronously and returns the result.
            TaskFactory.StartNew returns a Task object that represents an operation that can be performed asynchronously but does not return a result.
            
            
    
    4. Introduction to Task.Factory
        Task.Factory is a class in the .NET Framework that is used to create and manage task (Task) factories. It is a static property of the Task class and provides some methods and properties to create and manage tasks.
        Task.Factory provides the following commonly used methods:
        (1) Task.Factory.StartNew(Action action): Create and start a task, which performs the specified action (Action).
        (2)Task.Factory.StartNew(Action<object> action, object state): Create and start a task, which performs the specified action and passes an object as a parameter.
        (3)Task.Factory.StartNew(Func<TResult> function): Create and start a task, which executes the specified function (Func<TResult>) and returns a result.
        (4)Task.Factory.StartNew(Func<object, TResult> function, object state): Create and start a task, which executes the specified function, passes an object as a parameter, and returns a result.

        private static void Main(string[] args)
        {
            Task.Factory.StartNew(Test, 3);
            Task.Factory.StartNew(Test1, 3);
            Console.ReadKey();
        }

        private static void Test(object obj)
        {
            int n = (int)obj;
            Console.WriteLine(2 * n);
        }

        private static int Test1(object obj)
        {
            int n = (int)obj;
            Console.WriteLine("111");
            return 2 * n;
        }      

 
        
        In addition to the above methods, Task.Factory also provides some other methods and properties, such as:
            - Task.Factory.ContinueWhenAll(): Creates a task that continues to execute after a specified set of tasks are completed.
            - Task.Factory.ContinueWhenAny(): Creates a task that continues execution after any one of the specified set of tasks completes.
            - Task.Factory.FromAsync(): Convert an asynchronous operation (such as BeginXXX and EndXXX methods) into a task.
            - Task.Factory.CancellationToken: Gets a cancellation token (CancellationToken), which is used to cancel the execution of the task.
        Task.Factory can make it easier to create and manage tasks. It provides some convenient methods and properties to make task creation and control more flexible and efficient.
    


2. Understanding asynchronous return values


    1. When an asynchronous task has no return value, the type is Task instead of void.
        
        Using Task as the return type in an asynchronous method, that is, Task or Task<T>, is to indicate the completion status and result of the asynchronous operation.

        Asynchronous methods typically perform time-consuming operations such as network requests, file reading and writing, or computationally intensive tasks. In order to avoid blocking the main thread or UI thread, we can put these time-consuming operations in an asynchronous method and use the await keyword to wait for the operation to complete.

        If the asynchronous method has no return value, we can set the return type to Task. In this way, we can use the await keyword to wait for the completion of the asynchronous operation and ensure that the asynchronous method is completed before performing the next operation. For example: Suppose we have an asynchronous method DoSomethingAsync that performs some time-consuming operation but returns no results:

        public async Task DoSomethingAsync()
        {
            await Task.Delay(1000); // 模拟一个耗时的操作

            Console.WriteLine("操作完成");
        }


        When calling the DoSomethingAsync method, we can use the await keyword to wait for the completion of the asynchronous operation. In this way, we can ensure that the asynchronous method completes execution before performing the next step without blocking the main thread or UI thread.

        public async Task RunAsync()
        {
            Console.WriteLine("开始执行异步操作");

            await DoSomethingAsync();

            Console.WriteLine("异步操作完成");
        }


        The above RunAsync method calls the DoSomethingAsync method and uses the await keyword to wait for the completion of the asynchronous operation. When the asynchronous operation is completed, the next operation will continue.

        By setting the return type to Task, we can clearly express the nature of the asynchronous method and wait for the completion of the asynchronous operation using the await keyword. This way, we can better manage the execution order and results of asynchronous operations.
        
        
        In human words: await has two functions. One is a wait, that is, waiting for the completion of task execution; the other is to obtain the internal value of the completed task.
        This function must be executed under the condition of Task or Task<T> type, therefore, if there is no return value, the Task type must also be marked.
        
    
    2. Why is it that when the return is viewed in two parts, the type is identified as Task<int>, but the value is int?

        private static async Task<int> Main(string[] args)
        {
            Task<int> task = Task.Run(() =>
            {
                return 32;
            });
            
            Console.ReadKey();
            return await task;
        }  

 
        In the above example, the task is of type Task<int>. It should be int after await task, so it is still int after return, but Task<int> is used before the method.
        
        This is to be viewed in two parts, the type and value are separated.
        Separating the return type and return value makes it clearer to express the asynchronous nature of the method and the actual value returned.

        By using Task<int> in the method's return type, we explicitly indicate that this method is an asynchronous method, which returns a Task<int> object, indicating that the result of the asynchronous operation is an int type value. This allows the caller to clearly know that this method is an asynchronous method and that its return value needs to be processed asynchronously.

        Inside the method, by using the await keyword to wait for the completion of a Task<int> object and obtain its result, we can conveniently use this int type value, just like a normal synchronization operation. In this way, we can use the result of the asynchronous operation directly as the return value of the method without packaging it into a Task<int> object.
        
        This separate way of treating return types and return values ​​can express the asynchronous nature of the method more clearly and make it easier to use the results of asynchronous operations. At the same time, it can also help other developers better understand and use this method.
        If await is removed, the returned Task<int> should be consistent with the type Task<int> in front of the method, but it really reports an error:


        So when it comes to return values ​​and return types, you need to understand why it is designed this way. Commonly used places are in front of methods, return, and a Result attribute.
    
        Similarly, for Task<string> and string:
        In some cases, Task<string> can be used as a string. This is because Task<string> represents an asynchronous operation and its result is a value of type string.

        When we use the await keyword to wait for a Task<string> object, it will pause the execution of the current method and wait for the task to complete. Once the task is completed, the await expression returns the result of the task, which is a value of type string.

        In this case, we can use the result of the await expression directly, just like a normal string value. For example, we can assign it to a variable of type string, or pass it as a method parameter to a method that accepts a parameter of type string.

        Note that although you can use Task<string> as a string, they are different types. Task<string> represents an asynchronous operation, and string represents the result of a synchronous operation. Therefore, in some cases where a clear distinction between synchronous and asynchronous operations is required, we need to pay attention to the difference between the two and use them correctly.
        
        In addition, if the method return type is Task, the method does not need to write return. If it does, it will cause an error because it will be understood as void. At the same time, its results are also unusable, that is, there is no task.Result attribute.
        If the method return type is Task<string>, then return string must be written to match. At this time, you can use the result, using task.Result, which is a string type value.
    


3. Asynchronous exception catching


    Main reference: https://blog.csdn.net/weixin_46879188/article/details/118018895, summarized very well! ! !
    
        Use try..catch... to catch exceptions. Try catching has the following characteristics:
        
    1. It is definitely possible to catch exceptions in synchronous methods.
        The try-catch statement is mainly used to catch exceptions and is not limited to synchronizing threads. A try block is a region that contains code that may throw an exception. If the code in the try code block throws an exception, the system will jump to the catch code block and execute the corresponding exception handling logic.
        Whether it is a synchronous thread or an asynchronous thread, as long as the code in the code block throws an exception, try-catch can catch and handle it. Therefore, try-catch can be used to catch exceptions thrown by synchronous threads and can also handle exceptions from asynchronous threads.
        
        Examples of understanding asynchronous exceptions:

        private static async Task Main(string[] args)//a
        {
            try
            {
                await TestAsync();//b
            }
            catch (Exception)
            {
                Console.WriteLine("调用方捕捉到异常。");//c
            }
            Console.ReadKey();
            //return 0;//f
        }

        private static async Task TestAsync()//d
        {
            //try
            //{
            //    await Task.Delay(1000);
            //    throw new Exception("被调用方发生异常.");
            //}
            //catch (Exception)
            //{
            //    Console.WriteLine("异步中发生异常。");
            //}
            await Task.Delay(1000);//e
            throw new Exception("被调用方发生异常.");
        }   

 
        (1) Generally, the return value in the console at a is void or int. Writing void will cause an error. Because the previous async is generally not matched with void, it will make the exception unable to be caught (the special case of matching void is the control event in winform, and there is a special exception handling problem). Therefore, it is changed to Task here. Of course, it can also be Task<int>, in which case there must be cooperation at f.
            In an asynchronous method, when the async method does not have a clear return type, such as async void, you cannot use await to wait for the completion of the method, nor can you directly catch exceptions that may occur in the method. This may cause the caller to be unable to correctly handle or track the execution results of the asynchronous method.
            In addition, using the Task return type can be easily combined and concatenated with other asynchronous methods. By returning a Task, you can easily establish dependencies on asynchronous operations and write more expressive and readable asynchronous code.
            Although the return type can be omitted in some cases, such as async void Main, in a console application, the Main method is the entry method of the application and is mainly used to start and coordinate the execution of the application. Using async Task<int> as the return type of the Main method allows for better handling of asynchronous operations and exceptions to provide an appropriate exit code when the application exits. In , using the Task return type can provide better support for asynchronous operation waiting and exception handling, making asynchronous code more readable, maintainable and reliable.
        
        (2) The matching description of d and e is an asynchronous operation. Even if there is no e, because of the explicit description of async at d, it is still an asynchronous operation.
            The TestAsync method is an asynchronous method and is marked with the async keyword. Even if you do not explicitly use Task.Run or similar calls for asynchronous execution, the await Task.Delay(1000) statement will still execute in an asynchronous context. Therefore, the asynchronous method will put the task (i.e. Task.Delay) into the task queue and then return to the caller immediately without waiting for the completion of the task.
            Even if there are no await statements in an async method, the method is still considered asynchronous. In an asynchronous method, use the async keyword to declare the method as an asynchronous method, indicating that the method may perform asynchronous operations. Even if there is no await statement in the method body, the method can still be considered an asynchronous method.
            If there is no await statement, the asynchronous method will complete execution immediately and return a completed Task object. This Task object indicates that the asynchronous operation has been completed, but no asynchronous operation actually occurred. In this case, the asynchronous method is executed like a normal synchronous method, except that the async keyword is used in the method signature. It does not create new threads or introduce asynchronous operations. Note that the async method in this case may have some behavior that becomes ambiguous since no asynchronous operation occurs. Normally, we should use the await statement in an asynchronous method to wait for the completion of the asynchronous operation to ensure the normal execution and correct behavior of the asynchronous method.
            
            Therefore, even if there is no e, due to asynchronous reasons, the exception that occurs will not be interrupted within this method. It will be "infected" to the main program caller by returning the Task, so using try on the caller will catch the exception.
            
        (3) At d, asynchrony occurs inside the asynchronous method and does not interrupt the asynchronous method.
            Exceptions in an asynchronous method will not immediately interrupt the execution of the entire program. Instead, the exception will be propagated to the caller through the Task object until it is caught or propagated to the top-level method. Use try-catch blocks to effectively catch and handle exceptions in asynchronous methods to ensure the normal execution of the program.
            
    
    2. 2. The try...catch inside the Task method can catch
        exceptions that occur inside the thread, so the preferred processing method is to use try...catch... in the Task to handle the exception.
        try...catch can only catch exceptions that occur within the try block. If an exception occurs in code outside the try block, it cannot be caught. Therefore, when writing code, code that may cause exceptions should be placed in a try block so that exceptions can be caught and handled in time.
        
        Note that the order of catch blocks is important. If you place the catch block of the parent class exception before the catch block of the subclass exception, the catch block of the parent class exception will never be executed because the catch block of the subclass exception has already caught the exception. Therefore, when writing code, the order of catch blocks should be determined based on specific requirements and the inheritance relationship of exceptions.

        try
        {
            // 异步方法调用
            await MyAsyncMethod();
        }
        catch (ChildException ex)
        {
            // 捕捉子类异常
        }
        catch (ParentException ex)
        {
            // 捕捉父类异常
        }


    
    3. The internal exception of the Task method is not handled (the exception disappears in outer space.), how to catch it externally?

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.Wait();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()//d
        { throw new Exception("异步中抛出异常"); }


        If the statement a is not added above, the exception caused by the task asynchronously will not be caught. This exception will disappear in "outer space". If this "space" is the UI, then the UI may crash.
        At point a above, when using the Wait method of Task to wait for an asynchronous task to complete, if the asynchronous task throws an exception, the exception will be encapsulated in an AggregateException and will not be thrown immediately. If this AggregateException is not handled explicitly, it will be passed to the top of the call stack, eventually causing the program to crash.
        When task.Wait() at point a is not added, the exception thrown by the asynchronous task TestAsync() is not handled in time, so the exception is not caught. The task.Wait() method added at a will block the current thread and wait for the asynchronous task to complete. At the same time, the exception thrown by the asynchronous task will be re-thrown, so that the exception can be caught by the try...catch block.
        
        Note:
        When an asynchronous operation throws an exception, the exception will be encapsulated in the Task object. The exception is not immediately passed to the calling thread before waiting for the task to complete. Instead, it remains in the Task object until the caller explicitly waits (through a wait expression or other means) for the task to complete.
        
        Therefore, the exception is not caught at Task task= (although there is asynchronous information in the task), but when the wait is displayed, the exception is thrown, and try catches it.
    
    4. Thread exception capture methods
        There are two types of thread exception capture methods: blocking capture and asynchronous capture.
        Blocking-type catching exceptions are:
            when one or more threads are started, we wait until all threads have completed execution, determine whether an exception occurs in these threads, and then execute subsequent code.
        Asynchronously catching exceptions is:
            After a thread is started, we do not wait for its execution to complete, but directly execute other subsequent codes. When an exception occurs in a thread, the exception information will be returned automatically, or the thread exception information will be actively obtained when needed.        (1) Wait(), Result, GetAwaiter().GetResult() (blocking capture)
            Exceptions thrown in Task can be captured, but not directly, but when the Wait() method is called or the Result property is accessed. When an exception is obtained, the exception is first thrown as AggregateException type. If there is no AggregateException exception, the exception is thrown as Exception. The GetAwaiter().GetResult() method throws an exception as Exception.
            For an example, see wait at point a in 3 above.
            
            Question: What is GetAwaiter()?
            Answer: GetAwaiter() is a method of the Task type and other awaitable objects. It returns a TaskAwaiter object that can be used to wait for the completion of asynchronous operations.
                The TaskAwaiter type is designed to support the await operator. It provides some special methods and properties for more fine-grained control and access when waiting for asynchronous operations.
                By calling the GetAwaiter() method, you can obtain the corresponding TaskAwaiter object, and then you can use the methods and properties it provides.
                Commonly used methods and properties of TaskAwaiter:
                    IsCompleted: Gets a Boolean value indicating whether the asynchronous operation has been completed.
                    GetResult(): Get the result of asynchronous operation. If the asynchronous operation has not yet completed, this method blocks the current thread until the operation completes.
                    OnCompleted(Action continuation): Set a continuation operation and call the specified delegate when the asynchronous operation is completed. This allows additional logic to be executed after the task is completed.

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.GetAwaiter().GetResult();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()
        { throw new Exception("异步中抛出异常"); }


                Point a: The result of the asynchronous operation can be obtained by calling the GetAwaiter().GetResult() method of the Task object. This method blocks the current thread until the asynchronous operation completes and returns the result.
                If the return type of the asynchronous operation is void, the GetResult() method will return a void value. This means it has no results to fetch.
                If the return type of the asynchronous operation is Task or Task<T>, the GetResult() method will return the result of the asynchronous operation. If the asynchronous operation has completed, the GetResult() method returns the result immediately. If the asynchronous operation has not completed, the GetResult() method blocks the current thread until the asynchronous operation completes and returns the result.
                Note:
                When using the GetResult() method to obtain the result of an asynchronous operation, if the asynchronous operation throws an exception, the exception will be encapsulated in an AggregateException and will be re-thrown when the GetResult() method is called.
    
    
        (2) Use ContinueWith to capture exceptions (asynchronous capture) (recommended).
            But if no result is returned, or you do not want to call the Wait() method, how to get the exception? Just use ContinueWith.

            Task<int> task = Task.Run(async () =>
            {
                await Task.Delay(300);
                Console.WriteLine("异步进行中...");
                throw new Exception("发生异常."); //a
                return 4; //b
            });
            task.ContinueWith(t =>
            {
                Console.WriteLine(t.Exception.Message.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);//c


            Asynchronously throws an exception at a. In the asynchronous thread, b will not be executed again and will return. When continuewith is used at c, the code inside will be executed when an error occurs (equivalent to catching asynchronous).
            This is a common method for handling errors that occur during asynchronous operations. By using the ContinueWith method, you can perform specific follow-up actions when a task is completed or an error occurs.
            
            The author of the original article wrote two extension methods for simplicity so that they can be used directly:

            public static Task Catch(this Task task)
            {
                return task.ContinueWith<Task>(delegate (Task t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch exception thrown by Task: {0}", new object[]
                                {
                                     exception
                                });
                            }
                            return t;
                        }).Unwrap();
            }

            public static Task<T> Catch<T>(this Task<T> task)
            {
                return task.ContinueWith<Task<T>>(delegate (Task<T> t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch<T> exception thrown by Task: {0}", new object[]
                                {
                                        exception
                                });
                            }
                            return t;
                        }).Unwrap<T>();
            }     

       
            The master is different. In order to further learn unwrap, I read a bunch of references...
            
            
    5. Issues that should be paid attention to in ContinueWith
        . Task.Run(...) will generally arrange to start execution immediately.
        
        Task.Run(...).ContinueWith(...) is to continue with the execution of continuewith immediately after the previous run is executed. As for the final use of await or Result, it is to confirm that the entire task chain has been completed. Therefore, the entire task task chain starts executing as soon as it appears.
        
        It is different from Linq. Linq is just a plan or an operation chain, which defines the operations to be performed after the task is completed. The execution of the task and operation chain will not be triggered until it is actually used. This delayed execution feature can improve the flexibility and flexibility of the code. Efficiency, tasks and operations are actually performed only when the results are needed.

        Task<int> task1 = Task.Run<int>(() =>
        {
            Console.WriteLine("第一个任务完成");
            return 3;
        }).ContinueWith(t =>
        {
            Console.WriteLine("第二个任务完成");
            return t.Result + 2;
        });

        await Task.Delay(500);
        Console.WriteLine("最后任务已经完成.");


        Result;
            the first task is completed,
            the second task is completed,
            and the last task is completed.
            
        Note: ContinueWith() is usually followed by Action<Task<TResult>> or Func<Task<TResult>> with one parameter. This parameter is the previous Run A task that is returned after running to completion.
        
            
    6. What is UnWrap()?
            
        ( 1) The Unwrap method is a method of the Task class , used to expand nested tasks. When a task returns another task, we call it a nested task. By calling the Unwrap method, you can unwrap the results of a nested task into the results of an outer task.
        Specifically, when there are nested internal tasks in a Task<Task<T>>, calling the Unwrap method will expand the internal tasks and return results of type Task<T>. In this way, we can more conveniently handle the results of nested tasks without needing to manually access the inner tasks.
        The vivid metaphor is: the nested Task object can be regarded as a box with another box inside. The function of the Unwrap() method is to open the outermost box and take out the inner box so that we can directly process the inner box. In this way, we can more conveniently operate the inner box without caring whether it is nested in another box (that is, there is no need for the outer Task).
        The Unwrap() method is mainly used to handle asynchronous nested tasks to simplify code and improve readability. Not applicable in synchronous programming.
        (2) The purpose of using the Unwrap() method is to easily obtain the results of the inner Task, while retaining the outer Task to provide more context and readability. In this way, we can not only obtain the results of the inner task, but also further process the outer task when needed.

        private static void Main(string[] args)
        {
            NestedTaskUnwrap();
            Console.ReadKey();
        }

        private static async void NestedTask()
        {
            //Task返回Task<Task<string>>
            //第一个await后result类型为Task<string>
            Task<string> result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            });
            Console.WriteLine(await result);//第二个await后才会返回string
        }

        private static async void NestedTaskUnwrap()
        {
            //Task返回Task<Task<string>>
            //await后类型为Task<string>,Unwrap后result类型为string
            string result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            }).Unwrap();
            Console.WriteLine(result); //不需要await,result已经是string
        }


        
        The relationship between inner and outer Tasks can be seen below:

        Task<Task<int>> nestedTask = Task.Run<Task<int>>(() =>
                                      {
                                          return Task.Run(() =>
                                          {
                                              return 42;
                                          });
                                      });
        Console.WriteLine(nestedTask.Result);
        Console.WriteLine(nestedTask.Result.Result);//42

        Task<int> innerTask = nestedTask.Unwrap();
        Console.WriteLine(innerTask.Result);//42


        
        (3) The Unwrap() method is designed to handle the special relationship between Task<T> and the internal type T.

        In asynchronous programming, sometimes we may return another Task<T> type task in a task. In this case, we usually want to get the results of the inner task, not the wrapped task itself.

        The main purpose of the Unwrap() method is to solve this problem. It will expand the nested Task<T> and return the results of the inner task instead of returning a nested task. This way we can directly get the results of the inner tasks without having to deal with the nested task structure.

        Note that the Unwrap() method can only be applied to nested Task<Task<T>> structures. If there are no nested tasks, or the nested task is not of type Task<Task<T>>, then calling the Unwrap() method will have no effect.
            
            
    7. Catching exceptions in asynchronous methods
        
        (1) async...await can catch exceptions

        private static async Task TestAsync()
        {
            try
            {
                await Task.Run(() =>
                {
                    throw new Exception("异步中抛出异常。");
                });
            }
            catch (Exception)
            {
                await Console.Out.WriteLineAsync("捕捉到异常");
            }
        }    

    
        
            
        (2) For C# asynchronous methods, try to avoid using async void and instead use async Task. The
        try-catch block can only catch exceptions raised in the current code block and cannot catch exceptions raised in SynchronizationContext.
        When an exception is thrown in an async void method, the exception is thrown directly on the calling thread's SynchronizationContext and is not passed to the caller code. This means that an exception thrown from an async void method cannot be caught using a try-catch block, and the exception may cause the program to crash.
        So avoid using async void.
        
        Note:
        In the WinForms control, the reason why exceptions can be caught in the async void method is because the WinForms control itself implements a SynchronizationContext, called WindowsFormsSynchronizationContext. This particular SynchronizationContext allows exceptions to be caught in async void methods and passed to the caller code.
        WindowsFormsSynchronizationContext catches exceptions thrown from async void methods, encapsulates them in a special exception object, and passes it to the caller's exception handling code. This way, you can use try-catch blocks in WinForms controls to catch and handle exceptions thrown from async void methods.
        


4. Thread cancellation


    1. 8 threads execute asynchronously at the same time. When one stops asynchronously, the rest will stop at the same time.
        When 8 threads are running in parallel, a signal light is set in front. If there is an abnormality, the light will be turned on. When the other threads see the light, they will all stop.
        Interface: a button, a textbox
     


        program:

        private void BtnRun_Click(object sender, EventArgs e)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300));
                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        { throw new Exception($"[{key}]异常,线程取消 "); }
                    }

                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }
        }

        private void Display(string s)
        { Invoke(new Action(() => { TxtInfo.AppendText(s); })); }


        
    2. The above does not use ct to monitor the second parameter, but uses IsCancellationRequested.
        to monitor at the beginning and end of the asynchronous thread.
        
        Disadvantage: If the console will continue to interrupt the program, this phenomenon does not exist in winform.
        
        
        Question: After the asynchronous prompt "[Key_6] exception." occurs in the result, there are still two normal ends at the end. Why?
        Answer: When an asynchronous cancellation occurs in Key_6, the Key_3 and Key_1 tasks have started executing before the asynchronous code is executed to ct.ThrowIfCancellationRequested(), and after executing ct.ThrowIfCancellationRequested(), the cancellation request is detected and OperationCanceledException is thrown. Exception, but because the thrown exception is caught and processed in the asynchronous thread, it will not affect the execution of other tasks.
            Therefore, the tasks of Key_3 and Key_1 will continue to execute until they are completed and show a normal end. This is because ct.ThrowIfCancellationRequested() is just a method to check for cancellation requests, it does not force the task to be terminated immediately.
            Moreover, there is a delay in starting, canceling, transmitting information, etc. of the thread. Even if the cancellation is performed immediately, there is a slight delay in the transmission of this cancellation, the stopping of the thread (car braking), and the checking of traffic lights and cancellation of other threads (cars). Sometimes it needs to be integrated. Think about the end result.
        
        
        Q: Why is there a delay in starting the thread?
        Answer: There are several reasons why there may be a delay in starting a C# thread.
            (1) Scheduling delay: The startup of a thread requires thread scheduling by the operating system, and the operating system may have other tasks being executed, so there may be a certain delay.
            (2) Thread pool delay: When using the thread pool to start a thread, the thread pool may have reached the maximum number of threads, and you need to wait for other threads to complete before starting a new thread, resulting in delay.
            (3) Priority scheduling: The operating system may schedule according to the priority of the thread. If other high-priority threads are executing, low-priority threads may be delayed.
            (4) Thread synchronization: If you need to make some preparations or wait for certain conditions to be met before starting a thread, these operations may cause a delay in thread startup.
            Note that the delay is uncertain, and the specific delay time depends on many factors, including the operating system's scheduling algorithm, system load, thread priority, etc.
        
        
        
    3. Optimize:

        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken ct = cts.Token;
        for (int i = 0; i < 8; i++)
        {
            string key = $"Key_{i}";
            Task.Run(async () =>
            {
                await Task.Delay(new Random().Next(100, 300), cts.Token);
                ct.ThrowIfCancellationRequested();

                Display($"[{key}] 正常开始...\r\n");

                if (key.Equals("Key_6"))
                {
                    Display($"[{key}] 异常。\r\n");
                    cts.Cancel();
                    throw new Exception($"[{key}] 异常。");
                }

                ct.ThrowIfCancellationRequested();
                Display($"[{key}] 正常结束.\r\n");
            });
        }  

         
        (1)await Task.Delay(new Random().Next(100, 300), cts.Token) will wait for a random delay time and respond to cancellation requests to terminate the task execution at the appropriate time.
            This operation will wait for the specified time, but will also respond to the incoming cancellationToken. If the cancellationToken is canceled during the delay period (that is, cts.Cancel() is called), Task.Delay will throw a TaskCanceledException, causing the asynchronous task to enter the cancellation state.
            
        (2)ct.ThrowIfCancellationRequested() is used to check cancellation requests during the execution of asynchronous tasks. If the CancellationToken is canceled after calling cts.Cancel(), then ct.ThrowIfCancellationRequested() will throw OperationCanceledException.
        
            What is OperationCanceledException and will the program crash?
            OperationCanceledException is a special exception commonly used in asynchronous programming to indicate the cancellation of a request. It does not interrupt the entire thread, nor does it cause the entire program to crash.
            When OperationCanceledException is thrown, it becomes a normal exception object that can be caught and handled by the exception handling mechanism. An OperationCanceledException in an asynchronous task will not automatically cause the entire program to crash unless the exception is not handled appropriately.
            Normally, after an OperationCanceledException is thrown in an asynchronous task, you can catch the exception at the appropriate location and take appropriate handling measures, such as releasing resources, cleaning up, or performing appropriate rollback operations. You can use a try-catch block to catch and handle the exception to control the behavior of the program.
            If an OperationCanceledException is not handled correctly in an asynchronous task, it may propagate into the code calling the asynchronous task until it is caught or an unhandled exception handling mechanism is thrown. So make sure to catch and handle OperationCanceledException in the appropriate places to suit your code logic and needs.
            
            
        (3) Why is there not a second parameter ct added to the parameter list of the asynchronous method to monitor cancellation more finely?
            If you are already using ct.ThrowIfCancellationRequested() inside an asynchronous method to check the cancellation flag and throw an exception, then there is no need to add the second parameter ct to the parameter list. Adding the second parameter ct will only cause repeated monitoring without any additional benefits, but will affect the readability and performance of the code.
            
        (4) Is it wasteful to interrupt asynchrony by throwing async?
            Answer: Yes. Can be further optimized:

            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300), ct);
                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }

                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        return;
                    }

                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }


            Use the return statement to directly interrupt the execution of an asynchronous method and return immediately. This can be used as an alternative to throwing an exception to cancel the task. This avoids unnecessary waste of resources and improves code efficiency.
            
            
    4. There is a small bug in the above. When a thread is canceled but a thread is not started,
        the thread will still start, but will exit due to an exception.
        Optimization: Add an if (ct.IsCancellationRequested) break before string key = $"Key_{i}"; so that you can jump out of the loop directly without starting the thread again.
        Or add a second parameter:

        Task.Run(async () =>
        {
            await Task.Delay(new Random().Next(100, 300), ct);
            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常开始...\r\n");

            if (key.Equals("Key_6"))
            {
                Display($"[{key}] 异常。\r\n");
                cts.Cancel();
                { throw new Exception($"[{key}]异常,线程取消 "); }
            }

            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常结束.\r\n");
        },ct);//a   

         
        Add a second parameter at point a above to determine whether there is a cancellation status.
        
        Question: What is the role of the second parameter ct?
        Answer: The biggest effect is that when the thread has not started, checking ct can cancel the task before it starts.
            If the task has been canceled, it can finely control the cancellation. For example, you can use ct.IsCancellationRequested or ThrowIfCancellationRequested() directly in the task code to judge the cancellation request.
            In fact, adding it or not at this time seems to have little effect. Personally, I feel that apart from the previous function, it has no use. It just means that I added "monitoring" and the task may be cancelled.
 


5. Temporary variables


    1. What is the result of the following program?

        for (int i = 0; i < 20; i++)
        {
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }


    
        The answer is: 20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,
    
    
    2. Why are they all 20?
        The above is a closure, Task.Run accesses the external variable i. Closure reference:
            https://blog.csdn.net/dzweather/article/details/132741575?spm=1001.2014.3001.5501
        
        The for loop is executed in less than 1 millisecond, i becomes 20, and each Task has There will be a delay when using the i variable to apply for threads. When they apply successfully, i has already become 20, and then the output can only be 20. So let’s add a delay and see:

        for (int i = 0; i < 20; i++)
        {
            Thread.Sleep(1);
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }

        
        The result becomes: 2,3,3,4,4,6,6,8,9,10,10,11,12,13,14,16,16,18,18,19,
        you can see that due to delay , is not complete, but if you count it in detail, it is still 20 numbers, because when some threads finally execute and return, i has changed, so it is not a complete number. Increase the
        
        waiting time, such as Thread.Sleep(100), and the above number More complete, the bigger the more complete.
        
    
    3. Optimize again, use t.Wait() instead of Thread.Sleep.

        for (int i = 0; i < 20; i++)
        {
            Task t = Task.Run(() =>
            {
                Console.Write($"{i},");
            });
            t.Wait();
        }


        Better than Sleep above, because you can't determine how long you will sleep before the thread applies, executes, and completes! ! !
        This will directly display all numbers 0-19: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18 ,19.
        Because it is forced to wait for each asynchronous result before proceeding, the results come out in order.
    
    
    4. Classic approach: Use temporary variables (recommended)
        . Although the above display is normal, wait needs to wait for each thread to finish executing before proceeding to the next thread, which is more time-consuming.

        for (int i = 0; i < 20; i++)
        {
            int j = i;
            Task t = Task.Run(() =>
            {
                Console.Write($"{j},");
            });
        }


        Results: 0,2,1,3,4,6,9,10,11,12,8,14,15,16,7,18,19,5,17,13,
        the above results are complete and perfectly reflected Threads have delays and are executed concurrently, so the time is relatively fast.
        j is a local variable that is reassigned on each loop iteration. Since each task refers to the current value of j, they print different values. This is because each task creates a closure inside the Task.Run method that captures the value of j in the current loop iteration.
        
        For the variable i, it is declared outside the loop, so each task shares the same i variable. Since the tasks may start executing before the loop ends, they print the same value, which is the final value of i at the end of the loop.
    
    
 

Guess you like

Origin blog.csdn.net/dzweather/article/details/132901837