首頁 > 軟體

C#多執行緒TPL模式高階用法探祕

2022-03-22 13:01:25

一、引言

我們先來看下面的一個小范例:一個Winfrom程式,介面上有一個按鈕,有兩個非同步方法,點選按鈕呼叫兩個非同步方法,彈出執行順序,程式碼如下:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TPLDemoSln
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 按鈕點選事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void btnStart_Click(object sender, EventArgs e)
        {
            string i1 = await F1Async();
            MessageBox.Show("i1=" + i1);
            string i2 = await F2Async();
            MessageBox.Show("i2=" + i2);
        }

        /// <summary>
        /// 非同步方法F1
        /// </summary>
        /// <returns></returns>
        private Task<string> F1Async()
        {
            MessageBox.Show("F1 Start");
            return Task.Run<string>(() => 
            {
                // 休眠1秒
                Thread.Sleep(1000);
                MessageBox.Show("F1 Run");
                return "F1";
            });
        }

        /// <summary>
        /// 非同步方法F2
        /// </summary>
        /// <returns></returns>
        private Task<string> F2Async()
        {
            MessageBox.Show("F2 Start");
            return Task.Run<string>(() =>
            {
                // 休眠2秒
                Thread.Sleep(2000);
                MessageBox.Show("F2 Run");
                return "F2";
            });
        }
    }
}

在上面的程式碼中,Task.Run()是用來把一個程式碼段包裝為Task<T>的方法,Run中委託的程式碼體就是非同步任務執行的邏輯,最後return返回值。

執行程式,可以得到如下的輸出順序:

F1 Start->F1 Run->i1=F1->F2 Start->F2 Run->i2=F2。

我們對按鈕事件進行修改,修改為下面的程式碼,在看看執行順序:

/// <summary>
/// 按鈕點選事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnStart_Click(object sender, EventArgs e)
{
    //string i1 = await F1Async();
    //MessageBox.Show("i1=" + i1);
    //string i2 = await F2Async();
    //MessageBox.Show("i2=" + i2);

    Task<string> task1 = F1Async();
    Task<string> task2 = F2Async();
    string i1 = await task1;
    MessageBox.Show("i1=" + i1);
    string i2 = await task2;
    MessageBox.Show("i2=" + i2);
}

再次執行程式,檢視輸出順序:

F1 Start->F2 Start->F1 Run->i1=F1->F2 Run->i2=F2。

可以看出兩次的執行順序不一致。這是什麼原因呢?

這是因為並不是到了await才開始執行Task非同步任務,執行到Task<string> task1=F1Async()這句程式碼的時候,F1Async非同步任務就開始執行了。同理,執行到下一句程式碼就開始執行F2Async非同步任務了。await是為了保證執行到這裡的時候非同步任務一定執行完。執行到await的時候,如果非同步任務還沒有執行,那麼就等待非同步任務執行完。如果非同步任務已經執行完了,那麼就直接獲取非同步任務的返回值。

我們上面解釋的原因是否正確呢?我們可以做下面的一個實驗,來驗證上面說的原因,我們把按鈕事件裡面的await註釋掉,看看非同步方法還會不會執行,程式碼如下:

/// <summary>
/// 按鈕點選事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnStart_Click(object sender, EventArgs e)
{
    //string i1 = await F1Async();
    //MessageBox.Show("i1=" + i1);
    //string i2 = await F2Async();
    //MessageBox.Show("i2=" + i2);

    Task<string> task1 = F1Async();
    Task<string> task2 = F2Async();
    // 並不是到了await才開始執行Task非同步任務,這裡是保證非同步任務一定執行完
    // 程式碼執行到這裡的時候,如果沒有執行完就等待執行完,如果已經執行完,就直接獲取返回值
    // 這裡註釋掉await程式碼段,檢視非同步方法是否會執行
    //string i1 = await task1;
    //MessageBox.Show("i1=" + i1);
    //string i2 = await task2;
    //MessageBox.Show("i2=" + i2);
}

執行程式,發現非同步方法還是會執行,這就說明我們上面解釋的原因是正確的。感興趣的可以使用Reflector反編譯檢視內部實現的原理,主要是MoveNext()方法內部。

我們可以得到下面的結論:

  • 只要方法是Task<T>型別的返回值,都可以用await來等待呼叫獲取返回值。
  • 如果一個返回Task<T>型別的方法被標記了async,那麼只要方法內部直接return T這個型別的範例就可以了。
  • 一個返回Task<T>型別的方法如果沒有被標記為async,那麼需要方法內部直接return一個Task的範例。

上面說的第二點看下面的程式碼

/// <summary>
/// 方法標記為async 直接返回一個int型別的數值即可
/// </summary>
/// <returns></returns>
private async Task<int> F3Async()
{
    return 2;
}

上面的第三點可以看下面的程式碼:

/// <summary>
/// 方法沒有被標記為async,直接返回一個Task
/// </summary>
/// <returns></returns>
private Task<int> F4Async()
{
    return Task.Run<int>(() => 
    {
        return 2;
    });
}

二、TPL高階

我們做一些總結:

1、如果方法內部有await,則方法必須標記為async。await和async是成對出現的,只有await沒有async程式會報錯。只有async沒有await,程式會按照同步方法執行。

2、ASP.NET MVC中的Action方法和WinForm中的事件處理方法都可以標記為async,控制檯的Main()方法不能被標記為async。對於不能標記為async的方法怎麼辦呢?我們可以使用Result屬性來獲取值,看下面程式碼:

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

namespace AsyncDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 範例化物件
            HttpClient client = new HttpClient();
            // 呼叫非同步的Get方法
            Task<HttpResponseMessage> taskMsg = client.GetAsync("http://www.baidu.com");
            // 通過Result屬性獲取返回值
            HttpResponseMessage msg = taskMsg.Result;
            Task<string> taskRead = msg.Content.ReadAsStringAsync();
            string html = taskRead.Result;
            Console.WriteLine(html);
            Console.ReadKey();
        }
    }
}

不建議使用這種方式,這樣體現不出非同步帶來的好處,而且使用Result屬性,有可能會帶來上下文切換造成的死鎖。

下面我們來看看建立Task的方法。

 1、如果返回值就是一個立即可以隨手得到的值,那麼就用Task.FromResult()。看下面程式碼:

static Task<int> TestAsync()
{
    //return Task.Run<int>(() => 
    //{
    //    return 5;
    //});

    // 簡便寫法
    return Task.FromResult(3);
}

2、如果是一個需要休息一會的任務(比如下載失敗則過5秒鐘後重試。主執行緒不休息,和Thread.Sleep不一樣),那麼就用Task.Delay()。

3、Task.Factory.FromAsync()會把IAsyncResult轉換為Task,這樣APM風格的API也可以用await來呼叫。

4、編寫非同步方法的簡化寫法。如果方法宣告為async,那麼可以直接return具體的值,不用在建立Task,由編譯器建立Task,看下面的程式碼:

static async Task<int> Test2Async()
{
    // 複雜寫法
    //return await Task.Run<int>(() => 
    //{
    //    return 5;
    //});

    // 下面是簡化寫法,直接返回
    return 6;
}

到此這篇關於C#多執行緒TPL模式高階用法探祕的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com