C# 多線程學習之基礎入門

線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。進程是資源分配的基本單位。所有與該進程有關的資源,都被記錄在進程控制塊PCB中。以表示該進程擁有這些資源或正在使用它們。本文以一些簡單的小例子,簡述如何將程序由同步方式,一步一步演變成異步多線程方式,僅供學習分享使用,如有不足之處,還請指正。

同步方式

業務場景:用戶點擊一個按鈕,然後做一個耗時的業務。同步方式代碼如下所示:

private void btnSync_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnSync_Click同步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnSync_Click", i);
        this.DoSomethingLong(name);
    }
    Console.WriteLine("************btnSync_Click同步方法 結束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}


/// <summary>
/// 模擬做一些長時間的工作
/// </summary>
/// <param name="name"></param>
private void DoSomethingLong(string name)
{
    Console.WriteLine("************DoSomethingLong 開始 name= {0} 線程ID= {1} 時間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
    //CPU計算累加和
    long rest = 0;
    for (int i = 0; i < 1000000000; i++)
    {
        rest += i;
    }
    Console.WriteLine("************DoSomethingLong 結束 name= {0} 線程ID= {1} 時間 = {2} 結果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);

}

同步方式輸出結果,如下所示:

通過對以上示例進行分析,得出結論如下:

  1. 同步方式按順序依次執行。
  2. 同步方式業務和UI采用采用同一線程,都是主線程。
  3. 同步方式如果執行操作比較耗時,前端UI會卡住,無法響應用戶請求。
  4. 同步方式比較耗時【本示例9.32秒】

異步多線程方式

如何優化同步方式存在的問題呢?答案是由同步方式改為異步異步多線程方式。代碼如下所示:

private void btnAsync_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action<string> action = new Action<string>(DoSomethingLong);
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click", i);
        action.BeginInvoke(name,null,null);
    }
    Console.WriteLine("************btnAsync_Click異步方法 結束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

異步方式出結果,如下所示:

通過對以上示例進行分析,得出結論如下:

  1. 異步方式不是順序執行,即具有無序性。
  2. 異步方式采用多線程方式,和UI不是同一個線程,所以前端UI不會卡住。
  3. 異步多線程方式執行時間短,響應速度快。

通過觀察任務管理器,發現同步方式比較耗時間,異步方式比較耗資源【本例是CPU密集型操作】,屬於以資源換性能。同步方式和異步方式的CPU利用率,如下圖所示:

異步多線程優化

通過上述例子,發現由於采用異步的原因,線程還未結束,但是排在後面的語句就先執行,所以統計的程序執行總耗時為0秒。為瞭優化此問題,采用async與await組合方式執行,代碼如下所示:

private async void btnAsync2_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click2異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    await DoAsync();
    Console.WriteLine("************btnAsync_Click2異步方法 結束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

/// <summary>
/// 異步方法
/// </summary>
/// <returns></returns>
private async Task DoAsync() {
    Action<string> action = new Action<string>(DoSomethingLong);
    List<IAsyncResult> results = new List<IAsyncResult>();
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click", i);
        IAsyncResult result = action.BeginInvoke(name, null, null);
        results.Add(result);
    }
    await Task.Run(()=> {
        while (true)
        {
            for (int i = 0; i < results.Count; i++) {
                var result = results[i];
                if (result.IsCompleted) {
                    results.Remove(result);
                    break;
                }
            }
            if (results.Count < 1) {
                break;
            }
            Thread.Sleep(200);

        }
    });

}

經過優化,執行結果如下所示:

通過異步多線程優化後的執行結果,進行分析後得出的結論如下:

  1. Action的BeginInvoke,會返回IAsyncResult接口,通過接口可以判斷是否完成。
  2. 如果有多個Action的多線程調用,可以通過List方式進行。
  3. async與await組合,可以實現異步調用,防止線程阻塞。

通過以上方式,采用異步多線程的方式,共耗時3.26秒,比同步方式的9.32秒,提高瞭2.85倍,並非線性增加。且每次執行的總耗時會上下浮動,並非固定值。

異步回調

 上述async與await組合,是一種實現異步調用的方式,其實Action本身也具有回調函數【AsyncCallback】,通過回調函數一樣可以實現對應功能。具體如下所示:

/// <summary>
/// 異步回調
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAsync3_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click3異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action action = DoAsync3;
    AsyncCallback asyncCallback = new AsyncCallback((ar) =>
    {
        if (ar.IsCompleted)
        {
            Console.WriteLine("************btnAsync_Click3異步方法 結束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
            watch.Stop();
            Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
        }
    });
    action.BeginInvoke(asyncCallback, null);

}

private void DoAsync3()
{
    Action<string> action = new Action<string>(DoSomethingLong);
    List<IAsyncResult> results = new List<IAsyncResult>();
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format("{0}_{1}", "btnAsync_Click3", i);
        IAsyncResult result = action.BeginInvoke(name, null, null);
        results.Add(result);
    }

    while (true)
    {
        for (int i = 0; i < results.Count; i++)
        {
            var result = results[i];
            if (result.IsCompleted)
            {
                results.Remove(result);
                break;
            }
        }
        if (results.Count < 1)
        {
            break;
        }
        Thread.Sleep(200);

    }
}

異步回調執行示例,如下所示:

通過對異步回調方式執行結果進行分析,結論如下所示:

  1. 通過觀察線程ID可以發現,由於對循環計算的功能進行瞭封裝,為一個獨立的函數,所以在Action通過BeginInvoke發起時,又是一個新的線程。
  2. 通過async和await在通過Task.Run方式返回時,也會重新生成新的線程。
  3. 通過回調函數,可以保證異步線程的執行順序。
  4. 通過Thread.Sleep(200)的方式進行等待,會有一定時間范圍延遲。

異步信號量

信號量方式是通過BeginInvoke返回值IAsyncResult中的異步等待AsyncWaitHandle觸發信號WaitOne,可以實現信號的實時響應,具體代碼如下:

private void btnAsync4_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync_Click4異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    Action action = DoAsync3;
    var asyncResult = action.BeginInvoke(null, null);
    //此處中間可以做其他的工作,然後在最後等待線程的完成
    asyncResult.AsyncWaitHandle.WaitOne();
    Console.WriteLine("************btnAsync_Click4異步方法 結束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    watch.Stop();
    Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

信號量示例截圖如下所示:

通過對異步信號量方式的測試結果進行分析,得出結論如下:

  1. 信號量方式會造成線程的阻塞,且會造成前端界面卡死。
  2. 信號量方式適用於異步方法和等待完成之間還有其他工作需要處理的情況。
  3. WaitOne可以設置超時時間【最多可等待時間】。

異步多線程返回值

上述示例的委托都是無返回值類型的,那麼對於有返回值的函數,如何獲取呢?答案就是采用Func。示例如下所示:

private void btnAsync5_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync5_Click異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    string name = string.Format("{0}_{1}", "btnAsync_Click5", 0);
    Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
    IAsyncResult asyncResult = func.BeginInvoke(name, null, null);
    //此處中間可以做其他的工作,然後在最後等待線程的完成
    int result = func.EndInvoke(asyncResult);
    Console.WriteLine("************btnAsync5_Click異步方法 結束,線程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId,result);
    watch.Stop();
    Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}

private int DoSomethingLongAndReturn(string name)
{
    Console.WriteLine("************DoSomethingLong 開始 name= {0} 線程ID= {1} 時間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
    //CPU計算累加和
    long rest = 0;
    for (int i = 0; i < 1000000000; i++)
    {
        rest += i;
    }
    Console.WriteLine("************DoSomethingLong 結束 name= {0} 線程ID= {1} 時間 = {2} 結果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
    return DateTime.Now.Day;
}

采用Func方式的EndInvoke,可以獲取返回值,示例如下:

通過對Func方式的EndInvoke方法的示例進行分析,得出結論如下所示:

  1. 在主線程中調用EndInvoke,會進行阻塞,前端頁面卡死。
  2. Func的返回值是泛型類型,可以返回任意類型的值。

異步多線程返回值回調

為瞭解決以上獲取返回值時,前端頁面卡死的問題,可以采用回調函數進行解決,如下所示:

private void btnAsync6_Click(object sender, EventArgs e)
{
    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();
    Console.WriteLine("************btnAsync6_Click異步方法 開始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
    string name = string.Format("{0}_{1}", "btnAsync_Click6", 0);
    Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
    AsyncCallback callback = new AsyncCallback((asyncResult) =>
    {
        int result = func.EndInvoke(asyncResult);
        Console.WriteLine("************btnAsync6_Click異步方法 結束,線程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId, result);
        watch.Stop();
        Console.WriteLine("************總耗時= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
    });
    func.BeginInvoke(name, callback, null);
}

采用回調方式,示例截圖如下:

通過對回調方式的示例進行分析,得出結論如下:

  1. 異步回調函數中調用EndInvoke,可以直接返回,不再阻塞。
  2. 異步回調方式,前端UI線程不再卡住。 

以上就是C# 多線程學習之基礎入門的詳細內容,更多關於C# 多線程的資料請關註WalkonNet其它相關文章!

推薦閱讀: