C# Tasks Async Await

2021-02-20 07:36发布

最近一段时间在Youtube上面看了不少关于计算机方面的编程视频,其中UP主AngelSix的C#视频个人感觉讲得还可以,C# Tasks Async Await这个视频讲解了C#中的任务以及结合Async/Await的用法。Github仓库对应的代码地址为:TasksInConsole
不过个人感觉微软的官方文档讲解得更加详细,官方网址为:使用Async和Await的任务异步编程(TAP)模型 C#
这节视频的相关代码如下:

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace TasksInConsole
{
    class Program
    {
        #region Private Members

        /// <summary>
        /// The event finished callback for the Thread event example
        /// </summary>
        private static event Action EventFinished = () => { };

        /// <summary>
        /// Whether to run the thread examples
        /// </summary>
        private static bool RunThreadExamples = false;

        #endregion

        static void Main(string[] args)
        {
            // Log it
            Log("Hello World!");

            //
            //   Author:         Luke Malpass
            //   License:        MIT
            //   Support Me:     https://www.patreon.com/angelsix
            //   Source Code:    http://www.github.com/angelsix/youtube/Tasks
            //   Website:        http://www.angelsix.com
            //   Contact:        contact@angelsix.com
            //
            //
            //    What is Asynchronous
            //   ======================
            //
            //     Asynchronous is if you start something, and don't wait while its happening. 
            //     It literally means to not occur at the same time.
            //
            //     This means not that our code returns early, but rather it doesn't sit there
            //     blocking the code while it waits (doesn't block the thread)
            //

            //
            //    Issues with Threads
            //   =====================
            //
            //     Threads are asynchronous, as they naturally do something while the calling thread 
            //     that made it doesn't wait for it.
            //

            #region Threads are asynchronous

            if (RunThreadExamples)
            {
                // Log it
                Log("Before first thread");

                // Start new thread
                new Thread(() =>
                {
                    // Sleep a little
                    Thread.Sleep(500);

                    // Log it
                    Log("Inside first thread");

                }).Start();

                // Log it
                Log("After first thread");

                // Wait for work to finish
                Thread.Sleep(1000);

                Console.WriteLine("---------------------------------------------");
            }

            #endregion

            //     What's the issue with Threads? 
            //     
            //      1. Expensive to make
            //      2. Not natural to be able to resume after a thread has finished to do something 
            //         related to the thread that created it
            //     
            //     Issue 1 was solved with a ThreadPool. However issue 2 is still an issue for threads, 
            //     and is one reason why Tasks were made.In order to resume work after some 
            //     asynchronous operation has occurred we could with a Thread:
            //     
            //      1. Block your code waiting for it (no better than just doing it on same thread)
            //

            #region Blocking Wait

            if (RunThreadExamples)
            {
                // Log it
                Log("Before blocking thread");

                // Create new thread
                var blockingThread = new Thread(() =>
                {
                    // Sleep a little
                    Thread.Sleep(500);

                    // Log it
                    Log("Inside blocking thread");
                });

                // Start thread
                blockingThread.Start();

                // Block and wait
                blockingThread.Join();

                // Log
                Log("After blocking thread");
                Console.WriteLine("---------------------------------------------");
            }

            #endregion

            //      2. Constantly poll for completion, waiting for a bool flag to say done (inefficient, slow)

            #region Polling Wait

            if (RunThreadExamples)
            {
                // Log it
                Log("Before polling thread");

                // Create poll flag
                var pollComplete = false;

                // Create thread
                var pollingThread = new Thread(() =>
                {
                    // Log it
                    Log("Inside polling thread");

                    // Sleep a little
                    Thread.Sleep(500);

                    // Set flag complete
                    pollComplete = true;
                });

                // Start thread
                pollingThread.Start();

                // Poll for completion
                while (!pollComplete)
                {
                    // Log it
                    Log("Polling....");

                    // Sleep a little
                    Thread.Sleep(100);
                }

                // Log it
                Log("After polling thread");
                Console.WriteLine("---------------------------------------------");
            }

            #endregion

            //      3. Event-based callbacks (lose the calling thread on callback, and causes nesting)

            #region Event-based Wait

            if (RunThreadExamples)
            {
                // Log it
                Log("Before event thread");

                // Create thread
                var eventThread = new Thread(() =>
                {
                    // Log it
                    Log("Inside event thread");

                    // Sleep a little
                    Thread.Sleep(500);

                    // Fire completed event
                    EventFinished();
                });

                // Hook into callback event
                EventFinished += () =>
                {
                    // Log it
                    Log("Event thread callback on complete");
                };

                // Start thread
                eventThread.Start();

                // Log it
                Log("After event thread");

                // Wait for work to finish
                Thread.Sleep(1000);

                Console.WriteLine("---------------------------------------------");
            }

            #endregion

            #region Event-based Wait Method

            if (RunThreadExamples)
            {
                // Log it
                Log("Before event method thread");

                // Call event callback style method
                EventThreadCallbackMethod(() =>
                {
                    // Log it
                    Log("Event thread callback on complete");
                });

                // Log it
                Log("After event method thread");

                // Wait for work to finish
                Thread.Sleep(1000);

                Console.WriteLine("---------------------------------------------");
            }

            #endregion

            //     
            //     However that makes every time we want to do something asynchronous a lot of code
            //     and not easy to follow.
            //

            //
            //    What is a Task
            //   ================
            //
            //     A Task encapsulates the promise of an operation completing in the future
            //

            //
            //    Tasks, Async and Await
            //   ========================
            //
            //     Async in C# is mainly 2 words. async and await
            //
            //     The point is to allow easy and clean asynchronous code to be written without complex or messy code.
            //

            #region Sync vs Async Method

            // Log it
            Log("Before sync thread");

            // Website to fetch
            var website = "http://www.google.co.uk";

            // Download the string
            WebDownloadString(website);

            // Log it
            Log("After sync thread");

            Console.WriteLine("---------------------------------------------");

            // Log it
            Log("Before async thread");

            // Download the string asynchronously
            var downloadTask = WebDownloadStringAsync(website);

            // Log it
            Log("After async thread");

            // Wait for task to complete
            downloadTask.Wait();

            Console.WriteLine("---------------------------------------------");

            var task = Task.Run(async () =>
            {
                // Log it
                Log("Before async await thread");

                // Download the string asynchronously
                await WebDownloadStringAsync(website);

                // Log it
                Log("After async await thread");

                Console.WriteLine("---------------------------------------------");
            });

            // Wait the main task
            task.Wait();

            #endregion

            //     Async and await are always used together. A method or lambda tagged with 
            //     async can then await any Task
            //
            //     When you await something, the thread which called the await is free to then return to 
            //     what it was doing, while in parallel the task inside the await is now run on another thread. 
            //     
            //     Once the task is done, it returns either to the original calling thread, or carries on, 
            //     on another thread to do the work that codes after the await.
            //
            //
            //
            //    Async Analogy
            //   ===============
            //
            //     Imagine you go to Starbucks and the entire shop is run by one person.
            //     His name is Mr UI Thread. You walk in and ask Mr Thread for a Vanilla Latte. 
            //     He obliges and starts to make your coffee. 
            //
            //     He puts the milk into the container and turns on the hot steam, and proceeds 
            //     to stand there and wait for the milk to reach 70 degrees.
            //
            //     During this time you remember you wanted a muffin as well, so you shout over
            //     to Mr Thread and ask for a muffin... but he ignores you. He is blocked 
            //     waiting for the milk to boil.
            //     
            //     Several minutes goes by and 3 more customers have come in and are waiting 
            //     to be served. Finally the milk is finished and he completes the Latte. 
            //     Returning to you. You are a little annoyed at being ignored for minutes
            //     and decide to leave your muffin. 
            //
            //     Then he continues to serve one customer at a time, doing one job at a time.
            //     Not a good situation.
            //     
            //     This is what happens with a single threaded application.
            //     
            //     Now in order to improve business Mr Thread employs 2 new members of staff
            //     called Mrs and Mrs Worker Thread. The pair work well independently and 
            //     as Mr Thread takes orders from the customers, he asks Mrs Worker Thread 
            //     to complete the order, and then without waiting for Mrs Worker Thread to
            //     finish the drink, proceeds to serve the next customer.
            //     
            //     Once Mrs Worker Thread has finished a drink, instead of having to take 
            //     the drinks to the customers she asks Mr Worker Thread to serve the drinks 
            //     and then without waiting she proceeds to start the next order.
            //     
            //     The business is now a well-oiled, multi-threaded business.
            //

            //
            //    The Synchronous part in Tasks
            //   ===============================
            //
            //

            #region The Synchronous Part of Tasks

            // Run some work to show the synchronous parts of the call
            Task.Run(async () =>
            {
                // Log it
                Log($"Before DoWork thread");

                // Do work
                // This will return a Task and run the lines of code inside the method
                // up until the point at which the first await is hit
                var doWorkTask = DoWorkAsync("me");

                // Await the task
                // This will then spin off to a new thread and come with the result
                await doWorkTask;

                // Log it
                Log($"After DoWork thread");

            }).Wait();

            Console.WriteLine("---------------------------------------------");

            #endregion

            //
            //    Async Return Types
            //   ====================
            //
            //     You can only return void, Task or Task<T> for a method marked as async, as the method is 
            //     not complete when it returns, so no other result is valid.
            //

            #region Method 1 Getting Result of Async From Sync

            // Get the task
            var doWorkResultTask = DoWorkAndGetResultAsync("Return this");

            // Wait for it
            doWorkResultTask.Wait();

            // Get the result
            var doWorkResult = doWorkResultTask.Result;

            Console.WriteLine("---------------------------------------------");

            #endregion

            #region Method 2 Getting Result of Async From Sync

            Task.Run(async () =>
            {
                var doWorkResult2 = await DoWorkAndGetResultAsync("Return this 2");

            }).Wait();

            Console.WriteLine("---------------------------------------------");

            #endregion

            //
            //    Async Keyword
            //   ===============
            //
            //     The async keyword is not actually added to the method declaration signature, 
            //     the only effect is has is to change the compiled code. 
            //
            //     That's why interfaces cannot declare async, as it isn't a declarative statement, 
            //     its a compilation statement to change the flow of the function.
            //

            //
            //    Consuming Async Methods
            //   =========================
            //
            //     The best way to consume or call a method that returns a task is to be async yourself
            //     in the caller method, to ultimately awaiting it. 
            //
            //     By that definition async methods are naturally contagious.
            //

            #region Consuming Wait

            // Store the taks 
            var workResultTask = DoWorkAndGetResultAsync("Consume Wait");

            // Wait for it 
            workResultTask.Wait();

            // Get the result
            var workResult = workResultTask.Result;

            Console.WriteLine("---------------------------------------------");

            #endregion

            #region Consuming via Task

            // Declare the result
            var workResultViaTask = default(string);

            // Store the taks 
            Task.Run(async () =>
            {
                // Get result
                workResultViaTask = await DoWorkAndGetResultAsync("Consume via Task");

            }).Wait();

            Console.WriteLine("---------------------------------------------");

            #endregion

            //
            //    What happens during an Async call
            //   ===================================
            //
            //     Code inside a function that returns a Task runs its code on the callers thread 
            //     up until the first line that calls await. At this point in time:
            //
            //      1. The current thread executing your code is released (making your code asynchronous).
            //         This means from a normal point of view, your function has returned at this point
            //         (it has return the Task object).
            //
            //      2. Once the task you have awaited completes, your method should appear to continue
            //         from where it left off, as if it never returned from the method, so resume on 
            //         the line below the await.
            //     
            //     To achieve this, C# at the point of reaching the await call:
            //     
            //      1. Stores all local variables in the scope of the method 
            //      2. Stores the parameters of your method
            //      3. The "this" variable to store all class-level variables
            //      4. Stores all contexts(Execution, Security, Call)
            //     
            //     And on resuming to the line after the await, restores all of these values as if nothing
            //     had changed. All of this information is stored on the garbage collection heap.
            //

            //
            //    What is happening with threads during an async call
            //   =====================================================
            //
            //     As you call a method that returns a `Task` and uses `async`, inside the method all code, 
            //     up until the first `await` statement, is run like a normal function on the thread that 
            //     called it.
            //
            //     Once it hits the `await` the function returns the `Task` to the caller, and does its work 
            //     thats inside the `await` call on a new thread (or existing).
            //
            //     Once its done, and effectively "after" the `await` line, execution returns to a certain 
            //     thread.
            //     
            //     That thread is determined by first checking if the thread has an synchronization context 
            //     and if it does it asks that what thread to return to. For UI threads this will return work
            //     to the UI thread itself. 
            //

            // Console application has no synchronization context
            var syncContext = SynchronizationContext.Current;

            //
            //     For normal threads that have no synchronization context, the code after the `await` 
            //     typically, but not always, continues on the same thread that the inner work was being done 
            //     on, but has no requirement to resume on any specific thread.
            //     
            //     Typically if you use `ContinueWith` instead of `await`, the code inside `ContinueWith` runs 
            //     on a different thread than the inner task was running on, and using `await` typically  
            //     continues on the same thread.
            //     

            // Show ContinueWith typically changing thread ID's
            DoWorkAsync("ContinueWith").ContinueWith(t =>
            {
                Log("ContinueWith Complete");
            }).Wait();

            Console.WriteLine("---------------------------------------------");

            //
            //     This also means after every await the next line is typically on a new thread if there is no 
            //     synchronization context.
            //
            //    **********************************************************************************************
            // 
            //      An exception is if you use `ConfigureAwait(false)` then the SynchronizationContext is 
            //      totally  ignored and the resuming thread is treated as if there were no context.
            //
            //      Resuming on the original thread via the synchronization context is an expensive thing 
            //      (takes time) and so if you choose to not care about resuming on the same thread and want
            //      to save time you can use `ConfigureAwait` to remove that overhead
            //
            //    **********************************************************************************************
            //

            //
            //    Exceptions in Async calls
            //   ===========================
            //
            //     Any exceptions thrown that are not caught by the method itself are thrown into the Task 
            //     objects value `IsFaulted` and the `Exception` property.
            //
            //     If you do not await the Task, the exception will not throw on your calling thread.
            //

            #region Throw on Calling Thread, Without Awaiting

            Log("Before ThrowAwait");

            var crashedTask = ThrowAwait(true);

            // Did it crash?
            var isFaulted = crashedTask.IsFaulted;

            // The exception
            Log(crashedTask.Exception.Message);

            Log("After ThrowAwait");

            #endregion

            //
            //     If you await, the exception will rethrow onto the caller thread that awaited it.
            //     
            //     The exception to the rule is a method with async void. As it cannot be awaited, any
            //     exceptions that occur in an async void method are re-thrown like this:
            //     
            //      1. If there is a synchronization context the exception is Post back to the caller thread. 
            //      2. If not, it is thrown on the thread pool
            //

            #region Throw on Calling Thread, Without Awaiting

            Log("Before ThrowVoid");

            ThrowAwaitVoid(true);

            Log("After ThrowVoid");

            #endregion

            Console.ReadLine();
        }

        #region Helper Methods

        /// <summary>
        /// Output a message with the current thread ID appended
        /// </summary>
        /// <param name="message"></param>
        private static void Log(string message)
        {
            // Write line
            Console.WriteLine($"{message} [{Thread.CurrentThread.ManagedThreadId}]");
        }

        #endregion

        #region Thread Methods

        /// <summary>
        /// Shows an event-based thread callback via a method
        /// </summary>
        /// <param name="completed">The callback to call once the work is complete</param>
        private static void EventThreadCallbackMethod(Action completed)
        {
            // Start a new thread
            new Thread(() =>
            {
                // Log it
                Log("Inside event thread method");

                // Sleep
                Thread.Sleep(500);

                // Fire completed event
                completed();

            }).Start();
        }

        #endregion

        #region Task Example Methods

        /// <summary>
        /// Downloads a string from a website URL sychronously
        /// </summary>
        /// <param name="url">The URL to download</param>
        private static void WebDownloadString(string url)
        {
            // Synchronous pattern
            var webClient = new WebClient();
            var result = webClient.DownloadString(url);

            // Log
            Log($"Downloaded {url}. {result.Substring(0, 10)}");
        }

        /// <summary>
        /// Downloads a string from a website URL asychronously
        /// </summary>
        /// <param name="url">The URL to download</param>
        private static async Task WebDownloadStringAsync(string url)
        {
            // Asynchronous pattern
            var webClient = new WebClient();
            var result = await webClient.DownloadStringTaskAsync(new Uri(url));

            // Log
            Log($"Downloaded {url}. {result.Substring(0, 10)}");
        }

        /// <summary>
        /// Does some work asynchronously for somebody
        /// </summary>
        /// <param name="forWho">Who we are doing the work for</param>
        /// <returns></returns>
        private static async Task DoWorkAsync(string forWho)
        {
            // Log it
            Log($"Doing work for {forWho}");

            // Start a new task (so it runs on a different thread)
            await Task.Run(async () =>
            {
                // Log it
                Log($"Doing work on inner thread for {forWho}");

                // Wait 
                await Task.Delay(500);

                // Log it
                Log($"Done work on inner thread for {forWho}");
            });

            // Log it
            Log($"Done work for {forWho}");
        }

        /// <summary>
        /// Does some work asynchronously for somebody, and return a result
        /// </summary>
        /// <param name="forWho">Who we are doing the work for</param>
        /// <returns></returns>
        private static async Task<string> DoWorkAndGetResultAsync(string forWho)
        {
            // Log it
            Log($"Doing work for {forWho}");

            // Start a new task (so it runs on a different thread)
            await Task.Run(async () =>
            {
                // Log it
                Log($"Doing work on inner thread for {forWho}");

                // Wait 
                await Task.Delay(500);

                // Log it
                Log($"Done work on inner thread for {forWho}");
            });

            // Log it
            Log($"Done work for {forWho}");

            // Return what we received
            return forWho;
        }

        /// <summary>
        /// Throws an exception inside a task
        /// </summary>
        /// <param name="before">Throws the exception before an await</param>
        /// <returns></returns>
        private static async Task ThrowAwait(bool before)
        {
            if (before)
                throw new ArgumentException("Oopps");

            await Task.Delay(1);

            throw new ArgumentException("Oopps");
        }

        /// <summary>
        /// Throws an exception inside a void async before awaiting
        /// </summary>
        /// <param name="before">Throws the exception before an await</param>
        /// <returns></returns>
        private static async void ThrowAwaitVoid(bool before)
        {
            if (before)
                throw new ArgumentException("Oopps");

            await Task.Delay(1);

            throw new ArgumentException("Oopps");
        }
        #endregion
    }
}


整个视频的源代码可以从他的Github仓库下载:

git clone https://github.com/angelsix/youtube.git


本文同步分享在 博客“雪域迷影”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签: