天天看點

.Net Core異步程式設計.Net Core異步程式設計

.Net Core異步程式設計

一. 用async關鍵字修飾的方法

  1. 異步方法的傳回值一般是Task,T是真正的傳回值類型,Task.慣例:異步方法名字最好以Async結尾,這樣友善我們隻要是async
  2. 即使方法沒有傳回值,也最好吧傳回值聲明為非泛型的Task.
  3. 調用泛型方法時,一般在方法前加上await關鍵字,這樣拿到的傳回值就是泛型指定的T類型;
  4. 異步方法的"傳染性": 一個方法中如果await調用,則這個方法也必須修飾為async
static async Task Main(string[] args)
{
	string fileName = "d:/1.txt";
    File.Delete(fileName);
    File.WriteAllTextAsync(fileName,"hello async");
    string s = await File.ReadAllTextAsync(fileName);
    Console.WriteLine(s);
}
           
static async Task Main(string[] args)
{
    /*string fileName = @"F:\a\1.txt";
            File.WriteAllText(fileName, "hello");
            string s = File.ReadAllText(fileName);
            Console.WriteLine(s);*/
    string fileName = @"F:\a\1.txt";
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
    {
        sb.AppendLine("hello");
    }
    //這裡如果不加await,會顯示異常 因為還沒寫入完成就開始讀取,讀取是獨占的
    await File.WriteAllTextAsync(fileName, sb.ToString());

    string s = await File.ReadAllTextAsync(fileName);

    //Task<string> t = File.ReadAllTextAsync(fileName);
    //string s = await t;
    Console.WriteLine(s);
}
           

如果同樣的功能,既有同步方法,又有異步方法,那麼首先使用異步方法. .Net5中,很多架構的方法也都支援異步:Main,WinForm時間處理函數.

對于不支援異步方法該怎麼辦?

Wait()(無傳回值); Result(有傳回值) 風險:死鎖,盡量不用
static async Task Main(string[] args)
{
    int l = await DownloadHtmlAsync("https://www.youzack.com", @"F:\a\1.txt");
    Console.WriteLine("ok" + l);
}

/*static async Task DownloadHtmlAsync(string url, string fileame)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string html = await httpClient.GetStringAsync(url);
                await File.WriteAllTextAsync(fileame, html);
            }
        }*/

static async Task<int> DownloadHtmlAsync(string url, string fileame)
{
    using (HttpClient httpClient = new HttpClient())
    {
        string html = await httpClient.GetStringAsync(url);
        await File.WriteAllTextAsync(fileame, html);
        return html.Length;
    }
}
           

二. await關鍵字修飾的方法

await調用的等待期間,.NET會把目前的線程傳回給線程池,等異步方法調用執行完畢後,架構會從線程池在取出來一個線程執行後續的代碼.

Thread.CurrentThread.ManageThreadId 獲得目前線程Id

驗證:在耗時異步(寫入大字元串)操作前後分别列印線程Id

static async Task Main(string[] args)
{
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++)
    {
        sb.Append("XXXXXXXXXXXXXXXXX");
    }

    await File.WriteAllTextAsync(@"F:\a\1.txt", sb.ToString());
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
           

優化:到要等待的時候,如果發現已經執行結束了,那就沒必要再切換線程了,剩下的代碼就繼續在之前的線程上就行執行了

三.異步方法不等于多線程

static async Task Main(string[] args)
{
    Console.WriteLine("之前" + Thread.CurrentThread.ManagedThreadId);
    double r = await CalcAsync(5000);
    Console.WriteLine($"r = {r}");
    Console.WriteLine("之後" + Thread.CurrentThread.ManagedThreadId);
}

public static async Task<double> CalcAsync(int n)
{
    Console.WriteLine("CalcAsync," + Thread.CurrentThread.ManagedThreadId);
    double result = 0;
    Random rand = new Random();
    for (int i = 0; i < n*n; i++)
    {
        result += rand.NextDouble();
    }
    return result;
}
           

結論: 異步方法的代碼并不會自動在新線程中執行,除非把代碼放到新線程中執行.

把要執行的代碼以委托的形式傳遞給Task().Run().這樣就會從線程池中出去一個線程執行我們的委托

await Task.Run(()=>{
    //耗時操作代碼,可以傳回return傳回值
})
           

四. 沒有async的異步方法

async方法缺點:
  1. 異步方法會生成一個類,運作效率沒有普通方法高
  2. 可能占用非常多的線程
static async Task Main(string[] args)
 {
     string s = await ReadAysnc(1);
     Console.WriteLine(s);
 }

/*static async Task<string> ReadAysnc(int num)
{
     if (num == 1)
     {
       	string s = await File.ReadAllTextAsync(@"F:\a\1.txt");
        return s;
      }else if (num == 2)
      {
         string s = await File.ReadAllTextAsync(@"F:\a\2.txt");
         return s;
       }
         else
       {
         throw new ArgumentException();
       }
}*/

static Task<string> ReadAysnc(int num)
{
    if (num == 1)
    {
        return File.ReadAllTextAsync(@"F:\a\1.txt");
    }
    else if (num == 2)
    {
        return File.ReadAllTextAsync(@"F:\a\2.txt");
    }
    else
    {
        throw new ArgumentException();
    }
}
           

從上面代碼分析來看,隻甩手Task,不"拆完了再裝"反編譯上面的代碼: 隻是普通方法調用

優點: 運作效率更高,不會造成線程浪費

傳回值為Task的不一定都要标注async,标注async隻是讓我們可以更友善的await而已

如果一個異步方法隻是對别的異步方法調用的轉發,并沒有太多複雜的邏輯(比如等待A的結果,再調用B;把A調用的傳回值拿到内部做一些處理再傳回),那麼就可以去掉async關鍵字.

五.暫停調用方法使用

如果想在異步方法中暫停一段時間,不要用Thread.Sleep(),因為它會阻塞調用線程,而要用await Task.Delay().

舉例:下載下傳一個網址,3秒後下載下傳另一個

namespace 異步休息
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string s1 = await httpClient.GetStringAsync("https://www.youzack.com");
                textBox1.Text = s1.Substring(0, 100);
                //Thread.Sleep(3000);
                await Task.Delay(3000);
                string s2 = await httpClient.GetStringAsync("https://www.baidu.com");
                textBox1.Text = s2.Substring(0, 100);
            }
        }
    }
}
           

在控制台中沒看到差別,但是放到WinForm程式中就看到差別了,ASP.NET Core中也看到差別,但是Sleep()會降低并發.

六.CancellationToken參數

有時需要提前終止任務,比如:請求逾時,使用者取消請求.

很多異步方法都有CancellationToken參數,使用者獲得提前終止執行的信号.

CancellationToken結構體

none:空
bool isCancellationRequested 是否取消
(*)Register(Action callback) 注冊取消監聽
ThrowCancellationRequested() 如果任務被取消,執行到這句話就抛異常

CancellationTokenSource

CancelAfter()逾時後發出取消信号
Cancel()發出取消信号
CancellationToken Token

為"下載下傳一個網址N次"的方法增加取消功能,分别用GetStringAsync + isCancellationRequested,GetStringAsync + ThrowIfCancellationRequested(),帶 CancellationToken的GetStringAsync ()分别實作.取消分用逾時,使用者敲按鍵(不能await)實作.

class Program
{
    static async Task Main1(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            cts.CancelAfter(5000);
            await Download2sAsync("https://www.baidu.com", 100, cts.Token);
            while (Console.ReadLine() != "q")
            {

            }
            cts.Cancel();
            Console.ReadLine();
        }
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        cts.CancelAfter(5000);
        await Download2sAsync("https://www.baidu.com", 100,cts.Token);
    }

    /*static async Task Download1sAsync(string url, int n)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                for (int i = 0; i < n; i++)
                {
                    string html = await httpClient.GetStringAsync(url);
                    Console.WriteLine($"{DateTime.Now}:{html}");
                }
            }
        }*/

    static async Task Download2sAsync(string url, int n,CancellationToken cancellationToken)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                string html = await httpClient.GetStringAsync(url);
                Console.WriteLine($"{DateTime.Now}:{html}");
                await Task.Delay(500);
                if (cancellationToken.IsCancellationRequested)
                {
                    Console.WriteLine("請求被取消");
                    break;
                }
            }
        }
    }
}
           

ASP.NET Core開發中,一般不需要自己處理CancellationToken,CancellationTokenSource這些,隻要做到"能轉發CancellationToken就轉發即可".ASP.NET Core會對于使用者請求中斷進行處理.

七.Task類的重要方法

  1. Task WhenAny(IEnumerable tasks)等,任何一個Task完成,Task就完成
  2. Task<TResult[]> WhenAll(params Task[] tasks)等,所有Task完成,Task才完成,用于等待多個任務執行結束,但是不在乎他們的執行順序.
  3. FromResult()建立普通數值的Task對象.

八.其他問題

1.接口中的異步方法:

async是提示編譯器為異步方法中的await代碼進行分段處理的,而一個異步方法是否修飾了async對于方法的調用者來講沒差別的,是以對于接口中的方法或者抽象方法不能修飾為async
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }

    interface ITest
    {
        Task<int> GetCharCount(string file);
    }

    class Test : ITest
    {
        public async Task<int> GetCharCount(string file)
        {
            string s = await File.ReadAllTextAsync(file);
            return s.Length;
        }
    }
}
           

2.異步與yield

回顧:yield return不僅能夠簡化資料的傳回,而且可以讓資料處理"流水線化",提升性能

static IEnumerable<string> Test1()
{
    List<string> list = new List<string>();
    list.Add("hello1");
    list.Add("hello2");
    list.Add("hello3");
    return list;
}

static IEnumerable<string> Test2()
{
    yield return "hello1";
    yield return "hello2";
    yield return "hello3";
}
           

效果是一樣的.

在舊版C#中,async方法中不能用yield.從C# 8.0開始,吧傳回值聲明為IAsyncEnumerable(不要帶Task),然後周遊的時候await foreach()即可

static async Task Main(string[] args)
{
    await foreach (var s in Test3())
    {
        Console.WriteLine(s);
    }
}
static async IAsyncEnumerable<string> Test3()
{
    yield return "hello1";
    yield return "hello2";
    yield return "hello3";
}
           

ASP.NET Core和控制台項目中沒有SynchronizationContext,是以不用管ConfigureAwait(false)等.不要同步,異步混用

繼續閱讀