天天看點

C#_異步程式設計了解異步異步方法異步方法定義異步方法調用阻塞非阻塞與同步異步的差別

了解異步

同步任務(一般指方法):一個應用程式調用某個方法,等到其執行完才進行下一步操作。

異步任務(一般指方法):一個程式調用某一方法,在處理完成前就傳回該方法,不需要等待方法完成就可以繼續執行後面的代碼。

異步的好處在于非阻塞(調用線程不會贊同執行去等待子線程完成),是以我們把一些不需要立即使用結果、較耗時的任務設為異步執行,可以提高程式的運作效率。

異步方法

微軟極力推薦基于Task任務的async和await關鍵字實作方法的異步調用,也稱為基于任務的異步模式,簡稱TAP(Task-base asynchronous pattern)。C#5.0中的async和await關鍵字隻是編譯器功能,C#編譯器在内部還是會使用Task類建立代碼的。

異步方法定義

方法上要有async關鍵字,放在通路修飾符後面。

方法名最好以Async結尾,表示這是一個異步方法,從方法名就能識别出來。

方法的傳回值類型隻能是3種:Task、Task、void。

方法體代碼中要包含1個或多個await表達式,表示需要異步執行的任務,如果方法體中不存在await表達式,則與普通方法一樣執行。await後面跟一個Task.Run(…)或者直接調用其他的一個異步方法。

public async void Method1Async()
        {
            await Task.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine($"I am Method1Async {Thread.CurrentThread.ManagedThreadId}");
                }
            });
        }

        public async Task Method2Async()
        {
            await Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine($"I am Method2Async  {Thread.CurrentThread.ManagedThreadId}");
                }
            });
        }

        public async Task<string> Method3Async()
        {
            string res = await Task<string>.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine($"I am Method3Async  {Thread.CurrentThread.ManagedThreadId}");
                }
                return "I am Method3Async";
            });
            return res;
        }
           

異步方法調用

調用void傳回類型的異步方法

static void Main(string[] args)
    {
        Foo foo = new Foo();

        Console.WriteLine($"begin Method1Async {Thread.CurrentThread.ManagedThreadId}");
        foo.Method1Async();//執行到這句的時候不會阻塞,而是會繼續執行下面的代碼
        Console.WriteLine($"end Method1Async {Thread.CurrentThread.ManagedThreadId}");
    }
           
output:
begin Method1Async 1
I am Method1Async CurrentThread:4 0
I am Method1Async CurrentThread:4 1
I am Method1Async CurrentThread:4 2
I am Method1Async CurrentThread:4 3
I am Method1Async CurrentThread:4 4
I am Method1Async CurrentThread:4 5
I am Method1Async CurrentThread:4 6
I am Method1Async CurrentThread:4 7
I am Method1Async CurrentThread:4 8
I am Method1Async CurrentThread:4 9
end Method1Async 1
I am Method1Async CurrentThread:4 10
I am Method1Async CurrentThread:4 11
I am Method1Async CurrentThread:4 12
I am Method1Async CurrentThread:4 13
I am Method1Async CurrentThread:4 14
I am Method1Async CurrentThread:4 15
I am Method1Async CurrentThread:4 16
I am Method1Async CurrentThread:4 17
I am Method1Async CurrentThread:4 18
I am Method1Async CurrentThread:4 19
I am Method1Async CurrentThread:4 20
I am Method1Async CurrentThread:4 21
I am Method1Async CurrentThread:4 22
I am Method1Async CurrentThread:4 23
I am Method1Async CurrentThread:4 24
I am Method1Async CurrentThread:4 25
I am Method1Async CurrentThread:4 26
I am Method1Async CurrentThread:4 27
I am Method1Async CurrentThread:4 28
           

我們可以看到,異步方法中的列印内容還沒有執行100次就結束了,這是因為在執行異步方法的時候不會阻塞,而是會繼續執行異步方法後面的代碼,是以我們看到“end Method1Async 1”是在中間列印的。這句話列印完了之後Main方法就執行完畢了,程式結束,是以最終異步方法沒有執行完畢就終止了。建議在正式項目中不要使用void傳回類型的異步方法。

調用Task傳回類型的異步方法

static void Main(string[] args)
        {
            Foo foo = new Foo();
            
            Console.WriteLine($"begin Method2Async {Thread.CurrentThread.ManagedThreadId}");
            Task task = foo.Method2Async();//異步方法不會阻塞 會立刻執行下面的代碼

            Console.WriteLine($"Task Id:{task.Id}  Task Status:{task.Status}");
            task.Wait();//在這裡會進行無限等待,但是主線程是釋放的,也不會阻塞(控制台還可以移動),隻到待異步方法的完成後繼續執行下面的代碼。
            //task.Wait(1000);//隻等待10000ms,即使異步方法沒有結束也會繼續執行下面的代碼
            Console.WriteLine($"Task Id:{task.Id}  Task Status:{task.Status}");//可以看看異步方法結束後的狀态

            Console.WriteLine($"end Method2Async {Thread.CurrentThread.ManagedThreadId}");
        }
           
output:
begin Method2Async 1
I am Method2Async CurrentThread:4 0
Task Id:2  Task Status:WaitingForActivation
I am Method2Async CurrentThread:4 1
I am Method2Async CurrentThread:4 2
I am Method2Async CurrentThread:4 3
I am Method2Async CurrentThread:4 4
I am Method2Async CurrentThread:4 5
I am Method2Async CurrentThread:4 6
I am Method2Async CurrentThread:4 7
I am Method2Async CurrentThread:4 8
I am Method2Async CurrentThread:4 9
Task Id:2  Task Status:RanToCompletion
end Method2Async 1
           

由于傳回的是Task類型,是以我們可以擷取與任務相關的資訊(Status、Id等資訊),甚至可以執行Wait方法、WaitAny和WaitAll等方法。

值得一提的是Wait()方法,如果需要等待異步方法執行完畢則需要調用Wait()方法。

調用Task傳回類型的異步方法

static void Main(string[] args)
        {
            Foo foo = new Foo();
            
            Console.WriteLine($"begin Method1Async {Thread.CurrentThread.ManagedThreadId}");
            Task<string> task = foo.Method3Async();
            Console.WriteLine(task.Result);//如果沒有在異步方法沒有完成的時候調用Result屬性,那麼在此處會自動Wait(),直到異步方法執行完成。

            //task.Wait();//在這裡會進行無限等待,但是主線程是釋放的,也不會阻塞(控制台還可以移動),隻到待異步方法的完成後繼續執行下面的代碼。
            //task.Wait(1000);//隻等待10000ms,即使異步方法沒有結束也會繼續執行下面的代碼
            Console.WriteLine($"Task Id:{task.Id}  Task Status:{task.Status}");//可以看看異步方法結束後的狀态

            Console.WriteLine($"end Method3Async {Thread.CurrentThread.ManagedThreadId}");
        }
           

Task中T類型就是Result屬性的類型,也就是異步方法中return的類型。如果在異步方法執行過程中調用Result屬性,那麼就相當于調用了Wait()方法,再調用Result屬性得到異步方法的傳回結果。

值得一提的是Task類繼承自Task類型,是以T在ask類中的屬性和方法,Task對象也可以直接調用。

異步方法中就是簡單新開了一個線程嗎?

要回答上述問題,我們将第二個異步方法修改為如下:

public async Task Method2Async()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"before TaskRun CurrentThread:{Thread.CurrentThread.ManagedThreadId} {i}");
            }

            await Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine($"I am Method2Async CurrentThread:{Thread.CurrentThread.ManagedThreadId} {i}");
                }
            });

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"after TaskRun CurrentThread:{Thread.CurrentThread.ManagedThreadId} {i}");
            }
        }
           

在Main方法中執行如下:

static void Main(string[] args)
        {
            Foo foo = new Foo();
            Console.WriteLine($"begin Method2Async CurrentThread:{Thread.CurrentThread.ManagedThreadId}");
            Task task = foo.Method2Async();//異步方法不會阻塞 會立刻執行下面的代碼

            Console.WriteLine($"Task Id:{task.Id}  Task Status:{task.Status}");
            task.Wait();//在這裡會進行無限等待,但是主線程是釋放的,也不會阻塞(控制台還可以移動),隻到待異步方法的完成後繼續執行下面的代碼。
            //task.Wait(1000);//隻等待10000ms,即使異步方法沒有結束也會繼續執行下面的代碼
            Console.WriteLine($"Task Id:{task.Id}  Task Status:{task.Status}");//可以看看異步方法結束後的狀态

            Console.WriteLine($"end Method2Async CurrentThread:{Thread.CurrentThread.ManagedThreadId}");
        }
           

列印結果為:

output:
begin Method2Async CurrentThread:1
before TaskRun CurrentThread:1 0
before TaskRun CurrentThread:1 1
before TaskRun CurrentThread:1 2
before TaskRun CurrentThread:1 3
before TaskRun CurrentThread:1 4
before TaskRun CurrentThread:1 5
before TaskRun CurrentThread:1 6
before TaskRun CurrentThread:1 7
before TaskRun CurrentThread:1 8
before TaskRun CurrentThread:1 9
Task Id:2  Task Status:WaitingForActivation
I am Method2Async CurrentThread:4 0
I am Method2Async CurrentThread:4 1
I am Method2Async CurrentThread:4 2
I am Method2Async CurrentThread:4 3
I am Method2Async CurrentThread:4 4
I am Method2Async CurrentThread:4 5
I am Method2Async CurrentThread:4 6
I am Method2Async CurrentThread:4 7
I am Method2Async CurrentThread:4 8
I am Method2Async CurrentThread:4 9
after TaskRun CurrentThread:4 0
after TaskRun CurrentThread:4 1
after TaskRun CurrentThread:4 2
after TaskRun CurrentThread:4 3
after TaskRun CurrentThread:4 4
after TaskRun CurrentThread:4 5
after TaskRun CurrentThread:4 6
after TaskRun CurrentThread:4 7
after TaskRun CurrentThread:4 8
after TaskRun CurrentThread:4 9
Task Id:2  Task Status:RanToCompletion
end Method2Async CurrentThread:1
           

我們可以看到,在異步方法中:await之前的代碼塊與調用線程相同的,await中的代碼塊是開啟了一個任務,會線上程池中去取一個線程執行這個任務,但是await之後的代碼塊是線程池中的線程相同的,并不是調用線程。。。。

在遇到awiat關鍵字之前,程式是按照代碼順序自上而下以同步方式執行的。

在遇到await關鍵字之後,系統做了以下工作:

  1. 異步方法将被挂起
  2. 将控制權傳回給調用者
  3. 使用線程池中的線程(而非額外建立新的線程)來計算await表達式的結果,是以await不會造成程式的阻塞
  4. 完成對await表達式的計算之後,若await表達式後面還有代碼則由執行await表達式的線程(不是調用方所在的線程)繼續執行這些代碼

是以,在調用:

後,并不會立刻執行下面的代碼塊,而是進入異步方法中,執行await表達式之前的代碼塊(是以我們看到執行這一塊代碼的線程與調用線程相同),直到遇到await表達式後,異步方法将被挂起,才會将控制權傳回給調用線程,繼續進行調用線程中的代碼。同時,會使用線程池中的線程(并不是建立新的線程)來執行await表達式,是以不會造成程式的阻塞。

當該線程執行完await表達式後,若後面還有代碼,則由該線程繼續執行。(是以我們看到執行這一塊的線程與await表達式中的線程相同)

是以,很多地方可以看到以下的寫法:

public async Task Method2Async()
        {
            await Task.Yield();//可等待的空任務,是以await後的語句,都是線程池中排程的線程執行的,并不是調用異步方法的線程執行的。
            //就是一個切換線程的操作。。。

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"Method2Async Run CurrentThread:{Thread.CurrentThread.ManagedThreadId} {i}");
            }
        }
           

就是通過

await Task.Yield()

來切換線程。

阻塞非阻塞與同步異步的差別

同步和異步

同步和異步關注的是消息通信機制。所謂同步,就是在發出一個調用後主動等待這個調用的結果。

而異步則相反,調用在發出之後,不會立刻的到結果,而是在調用發出後,被調用者通過狀态、通知來通知調用者,或者通過回調函數處理這個調用。

阻塞和非阻塞

阻塞和非阻塞關注的是程式在等待調用結果(消息、傳回值)時的狀态。阻塞調用是指調用結果傳回之前,目前線程會被挂起。調用線程隻有在得到結果之後才會傳回。

非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞目前線程。

繼續閱讀