一個例子形象地理解同步與異步

請看一個示例:

同步方式請求接口

請求一次接口耗時大約100多毫秒

代碼

一個for循環,循環500次,調用方法Request,Request方法中一個while(true)無限循環,同步方式請求url獲取數據。
代碼點評:要是寫一個while(true)沒問題,這是想運行500個while(true),這代碼是錯誤的,行不通。應該使用Thread或者Task.Run加TaskCreationOptions.LongRunning參數。
這當然是有問題的代碼,請看下面運行截圖,只有第一個while(true)在執行,其它的499個while(true)根本沒有執行機會。

static int num = 0;
static ConcurrentDictionary<int, object> dict = new ConcurrentDictionary<int, object>();

static void Main(string[] args)
{
    CalcSpeed();

    for (int i = 0; i < 500; i++)
    {
        Request(i);
    }

    Console.WriteLine($"Main函數結束");
    Console.ReadLine();
}

static void Request(int index)
{
    dict.TryAdd(index, null);

    while (true)
    {
        string url = "http://localhost:5028/Test/TestGet";
        string result = HttpUtil.HttpGet(url);
        Interlocked.Increment(ref num);
    }
}

static void CalcSpeed()
{
    _ = Task.Factory.StartNew(() =>
    {
        Stopwatch sw = Stopwatch.StartNew();
        while (true)
        {
            Thread.Sleep(2000);
            double speed = num / sw.Elapsed.TotalSeconds;
            ThreadPool.GetMaxThreads(out int w1, out int c1);
            ThreadPool.GetAvailableThreads(out int w2, out int c2);
            Console.WriteLine($"有 {dict.Count.ToString().PadLeft(3)} 個 while(true) 在執行,線程池活動線程數:{(w1 - w2).ToString().PadRight(3)}  速度:{speed:#### ####.0} 次/秒");
        }
    }, TaskCreationOptions.LongRunning);
}

運行截圖

說明

代碼中沒有創建線程,也沒有使用Task.Run,請求一次接口耗時大約100多毫秒,while(true)在主線程中執行,平均1秒請求接口不到10次。
注意:只有第一個while(true)在執行。

修改1:在Request函數中添加一行代碼Thread.Sleep(1);

代碼

static void Request(int index)
{
    dict.TryAdd(index, null);

    while (true)
    {
        string url = "http://localhost:5028/Test/TestGet";
        string result = HttpUtil.HttpGet(url);
        Interlocked.Increment(ref num);

        Thread.Sleep(1);
    }
}

運行截圖

說明

沒什麼用,速度還變慢了一點。
依然是有問題的代碼。
依然只有第一個while(true)在執行。

修改2:在Request函數中添加一行代碼await Task.Delay(1);

VS自動在void Request前面添加了async關鍵字

代碼

static async void Request(int index)
{
    dict.TryAdd(index, null);

    while (true)
    {
        string url = "http://localhost:5028/Test/TestGet";
        string result = HttpUtil.HttpGet(url);
        Interlocked.Increment(ref num);

        await Task.Delay(1);
    }
}

運行截圖

說明

速度快多了,並且越來越快。
有多個while(true)在執行,並且在執行的while(true)數量越來越多,最終會達到500個。
這是比較神奇的地方,僅僅加了一行await Task.Delay(1);同步方法Request就變成了異步方法。
在執行await Task.Delay(1);這一行時,其它while(true)得到了執行機會,你們可以驗證一下。
同步請求分別在不同的線程中執行,你們可以打印線程ID驗證一下。

修改3:前面使用的是HttpUtil.HttpGet同步請求,修改爲異步請求,await Task.Delay(1);這一行也不需要了

代碼

static async void Request(int index)
{
    dict.TryAdd(index, null);

    while (true)
    {
        string url = "http://localhost:5028/Test/TestGet";
        var httpClient = HttpClientFactory.GetClient();
        string result = await (await httpClient.GetAsync(url)).Content.ReadAsStringAsync();
        Interlocked.Increment(ref num);
    }
}

運行截圖

說明

速度非常快。
異步的優勢體現出來了。

修改4:有沒有人會認爲修改2,把同步代碼用Task.Run包一下,速度會更快?

代碼

static async void Request(int index)
{
    dict.TryAdd(index, null);

    while (true)
    {
        await Task.Run(() =>
        {
            string url = "http://localhost:5028/Test/TestGet";
            string result = HttpUtil.HttpGet(url);
            Interlocked.Increment(ref num);
        });

        await Task.Delay(1);
    }
}

運行截圖

說明

線程飢餓,全部阻塞,沒有返回結果,速度是0。

總結

通過這個例子形象地體會一下同步與異步,以及爲什麼要使用異步。
如果你寫的代碼是異步的,但是調用的IO接口又是同步的,這比真正的異步效率要差很多,但比同步代碼有所提升。
針對修改2,有人會說,這代碼有問題,後面的while(true)會延遲好久纔會執行。但是如果for循環的數量是少量的,程序啓動時的一點延遲是允許的,就沒有問題,
修改代碼如下:

for (int i = 0; i < 20; i++)
{
    Request(i);
}

運行截圖:

說明:
20個while(true)都在運行,比一個while(true)要快很多。
當然,沒必要這麼寫了,直接new 20個Thread就可以。
但如果for循環就是500次,而且需要調用的IO接口又是同步的,那麼就老老實實寫500個new Thread。
如果非要用異步,設置一下線程池的大小,大於500,避免線程飢餓。

ThreadPool.SetMinThreads(800, 800);
ThreadPool.SetMinThreads(600, 600);

你會發現,不能想當然,依然有問題,這時強行用異步就很容易寫出BUG了。
最後,再好好體會一下上面的例子。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章