C# 異步多線程(Task await/async)理解


前言

本篇按自己的理解,對異步多線程的一些知識點進行記錄,順便聊聊.NetFramework中常用類之間的關系。
旨在幫助各位同學理清異步編程的學習路線,並不是個具體的使用教程。

基礎知識

線程是歸屬於操作系統的控制流,並不是由代碼生成,代碼只負責請求資源,由CPU處理請求在操作系統中獲得線程。(這是粗劣的個人理解,但是知道這點就能解釋為什么多線程很多反常識的現象)

無序性

多線程相對於單線程,很明顯的一個特點就是無序性、不可預測性。

  • 啟動無序:
    代碼順序開啟多個線程,但是線程啟動仍然是無序的。
    原因:CLR順序向操作系統請求多個線程,這些請求幾乎同時發出,CPU隨機處理這些請求分配線程,所以哪個線程先開啟是無序的。

  • 執行時間不確定
    即使是單線程,執行同一個代碼段,時間也是不確定的。
    原因:設計操作系統的調度策略 以及CPU分片。

  • 結束無序

常用類

隨着.NetFramework不同版本對於線程的抽象不斷演化,類型也逐漸豐富。
大致歷史:
Thread-->ThreadPool-->Task/TaskFactory-->Parallel
Thread是初代NetFramework里的對象,擁有最高自由度的線程操作,所以使用不當會造成嚴重錯誤(比如可以new一萬個線程造成電腦死機)
ThreadPool抽象對於多線程的發展起到了里程碑的作用,后續的模型都基於此發展起來。
Task/TaskFactory是目前最流行的對象,網絡上詳細的教程很多,大家自行學習即可。
可以參考https://www.cnblogs.com/wyy1234/p/9172467.html
Parallel其實和Task很像,Task不能操作主線程,Parallel在運行時主線程也參與計算。

await/async

專門聊一聊await/async,其實他們和前面幾個不是同一層級的,await/async本質只是語法糖,並沒有產生新的線程類型對象。
await/async需要與Task一起使用,await只有在async方法中才能使用,他們本質上是實現線程之間的調度,當調用線程遇到await Task后,會直接返回不繼續運行之后的代碼(同時阻塞調用線程) ,等Task運行結束后,由子線程繼續運行未完成代碼
(在沒有await的情況下,由於Task是非阻塞的,這段代碼本來應該由調度線程直接執行),相當於,await Task之后的代碼變成了Task的回調函數,效果與task.continueWith("后續代碼")一致。
通過await/async這個語法糖,可以用同步編碼書寫異步過程,提高程序可讀性的同時降低了編碼難度。

ps:寫過js的同學其實會比較好理解,這玩意和promise是一樣的玩意,就是語法稍微不同。

以下是簡單的測試代碼

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static  void Main(string[] args)
        {
            Console.WriteLine("當前111Main主線程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
            var t1 = AsyncGetsum();
            Console.WriteLine("子線程執行AsyncGetsum 主線程不阻塞 繼續執行");
            Console.WriteLine("開始等待t1.Result結果 主線程阻塞");
            Console.WriteLine(t1.Result);  //會阻塞主線程

            Console.WriteLine(" Task.Delay(10000) 開始");
            Task.Delay(10000);//不會阻塞主線程
            Console.WriteLine(" Task.Delay(10000) 結束");


            Console.WriteLine("當前222Main主線程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
            var t = ToDoWithTimeOut();
            Console.WriteLine(t.Result);


            Console.ReadKey();
        }

        private static async Task<int> AsyncGetsum()
        {
            Console.WriteLine("准備 AsyncGetsum");
            await Task.Delay(10000); //遇到await 返回main函數 , 之后的代碼變成回調  Delay之后再執行 相當於回調
            Console.WriteLine("等待了10秒 AsyncGetsum開始執行");

            int sum = 0;
            for (int i = 0; i <= 10; i++)
            {
                Console.WriteLine("當前AsyncGetsum線程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
                sum += i;
                System.Diagnostics.Debug.WriteLine("sum += " + i);
                await Task.Delay(50);
            }
            return sum;
        }

        private static async Task<string> ToDoAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(3));
            return "To Do Success!";
        }

        public static async Task<string> ToDoWithTimeOut()
        {
            var toDoTask = ToDoAsync();
            var timeOutTask = Task.Delay(TimeSpan.FromSeconds(2));

            //var completedTask = Task.WhenAny(toDoTask, timeOutTask);
            var completedTask = await Task.WhenAny(toDoTask, timeOutTask);
            if (completedTask == timeOutTask)
            {
                return "No";
            }
            return await toDoTask;
        }

    }
}

線程安全

多線程中另外一塊需要注意的就是線程安全,單線程中正常運行的代碼很肯能在多線程中就會出錯,特別是在多線程對於同一個對象進行修改的時候。

Lock

解決線程安全問題,最常見的方法就是加鎖
最標准的寫法 ---> private static readonly object lick = new object()
通過鎖定內存的引用地址 讓對象只會同時被一個線程調用來確保線程安全
(順便提一點,鎖定內存只在多線程中有用,單線程是隨意進入的,所以類似遞歸函數中出現lock(this)這種寫法是不會產生死鎖的!)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM