天天看點

C#異步程式設計基礎入門總結

C#異步程式設計基礎入門總結

參考網址:https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/

1.前言

*.NET Framework提供了執行異步操作的三種模式:

異步程式設計模型(APM)模式(也稱為IAsyncResult的模式),其中異步操作要求Begin和End方法(例如,BeginWrite和EndWrite異步寫入操作)。這種模式不再被推薦用于新開發。有關更多資訊,請參閱異步程式設計模型(APM)。

基于事件的異步模式(EAP),它需要一個具有Async字尾的方法,并且還需要一個或多個事件,事件處理程式委托類型和被EventArg派生類型。EAP在.NET Framework 2.0中引入。不再推薦新的開發。有關更多資訊,請參閱基于事件的異步模式(EAP)。

基于任務的異步模式(TAP),它使用單一方法來表示異步操作的啟動和完成。TAP在.NET Framework 4中引入,是.NET Framework中推薦的異步程式設計方法。C#中的async和等待關鍵字,Visual Basic語言中的Async和Await運算符為TAP添加語言支援。有關更多資訊,請參閱基于任務的異步模式(TAP)。*

2.異步的應用場景

在計算機程式的運作中,計算是需要一定的時間的,在運算時間過長的任務時,比如上傳大檔案、讀取檔案流、資料庫操作、httprequest等,如果是同步(synvronous)必須等待該任務執行完成才能繼續下一個任務。使用異步(asynchronous)操作,會開啟新的線程,不會等待異步操作完成才去執行後面的程式,相比異步程式設計優點:1.就是出現長時間處理程式時,不會卡界面,使用者仍然可以操作UI界面2.提高程式運作效率,節約CPU資源,提供系統吞吐量。

3.程序和線程的關系

這個面試的時候基本上都會問到,簡而言之就是:

一個程式都會有一個程序和一個線程,程序是由CPU進行排程配置設定資源的,有一個完整的虛拟位址空間,不依賴線程獨立存在,反之線程是由程序來排程配置設定的,隻是程序的一部分,沒有自己的位址空間,與程序内的其他線程一起共享該程序的所有資源。打個簡單的比方就像是線程就好比是人體的寄生蟲,不能獨立存在,必須依靠人(程序)的營養(資源)來生存(執行)

4.異步和多線程的差別

異步是相對同步而言的,我們知道異步是開啟了新線程,但是和多線程不是一個概念,異步相當于一個人的“大腦”能夠做試卷,又能夠看電影,同時處理兩件以上不同的事情。多線程好比多個人做不同的事情。

異步操作的本質

c#中異步和多線程的差別是什麼呢?異步和多線程兩者都可以達到避免調用線程阻塞的目的,進而提高軟體的可響應性。 所有的程式最終都會由計算機硬體來執行,無須消耗CPU時間的I/O操作正是異步操作的硬體基礎。

線程的本質

線程不是一個計算機硬體的功能,而是作業系統提供的一種邏輯功能,線程本質上是程序中一段并發運作的代碼,是以線程需要作業系統投入CPU資源來運作和排程

異步操作的優缺點

因為異步操作無須額外的線程負擔,并且使用回調的方式進行處理,在設計良好的情況下,處理函數可以不必使用共享變量(即使無法完全不用,最起碼可以減少 共享變量的數量),減少了死鎖的可能。當然異步操作也并非完美無暇。編寫異步操作的複雜程度較高,程式主要使用回調方式進行處理,與普通人的思維方式有些出入,而且難以調試。

多線程的優缺點

  多線程的優點很明顯,線程中的處理程式依然是順序執行,符合普通人的思維習慣,是以程式設計簡單。但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔。并且線程間的共享變量可能造成死鎖的出現。

适用範圍

當需要執行I/O操作時,使用異步操作比使用線程+同步 I/O操作更合适。I/O操作不僅包括了直接的檔案、網絡的讀寫,還包括資料庫操作、Web Service、HttpRequest以及.net Remoting等跨程序的調用。

而線程的适用範圍則是那種需要長時間CPU運算的場合,例如耗時較長的圖形處理和算法執行。但是往往由于使用線程程式設計的簡單和符合習慣,是以很多朋友往往會使用線程來執行耗時較長的I/O操作。這樣在隻有少數幾個并發操作的時候還無傷大雅,如果需要處理大量的并發操作時就不合适了。

5.C#異步方式之一( BeginInvoke、EndInvoke方法)

方式1:使用回調方法完成異步委托

先來看個例子,委托的異步調用,這個例子首先定義一個string類型的傳回值、string類型的參數的委托。雖然這中模式不推薦被使用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class

Program

{

delegate

string

SayHi(

string

name);

//定義委托

static

void

Main(

string

[] args)

{

SayHi sayhi = 

new

SayHi(SayHiName);

//執行個體化委托

sayhi(

"科比"

);

//一般的直接調用

sayhi.Invoke(

"張林"

);

//使用Invoke方法同步調用

//異步調用

sayhi.BeginInvoke(

"杜蘭特"

, (IAsyncResult ar) =>

{

sayhi.EndInvoke(ar);

Console.WriteLine(

"打招呼成功結束"

);

}, 

null

);

}

public

static

string

SayHiName(

string

name)

{

return

"how are you"

+name + 

"?"

;

}

}

  

前兩種調用委托的方式都是同步的,BeginInvoke方法的傳回值是IAsyncResult類型的

該方法的參數由兩部分組成,前面(n)個參數是委托的參數,倒數第二個參數也表示一個委托,該委托是.net系統定義的委托(和func、action類似),檢視AsyncCallback的定義如圖:

作用就是:作為執行調用的回調方法,值得注意的是,在回調方法中,必須調用EndInvoke方法結束異步調用,EndInvoke是擷取異步調用的結果

上面的例子調試的結果如圖:

方式2:使用輪詢

我們把BeginInvoke的委托參數為null,使用輪詢的方式

Func<

string

string

> func = 

delegate

(

string

name)

{

Thread.Sleep(2000);

return

"how are you"

+ name + 

""

;

};

IAsyncResult ar = func.BeginInvoke(

"張林"

,

null

,

null

);

int

i = 1;

while

(!ar.IsCompleted)

{

Console.WriteLine(200*i);

i++;

Thread.Sleep(200);

}

string

result = func.EndInvoke(ar);

Console.WriteLine(result);

結果如圖:

6.C#異步方式之二 await async

async和await是一對關鍵字,它是.net 4.5的特性。在實際工作中使用友善靈活,主要原因就是可以像寫同步方法那樣去異步程式設計,代碼結構清晰,不用關心如何實作異步的程式設計。

這裡其實要注意的是,之前剛說了異步是開啟新的線程來實作的,但是await 和async兩個關鍵字并沒有開啟新的線程,為了證明這一點,下面建了一個winform的程式,異步擷取圖檔并顯示到picturebox上。

public

Form1()

{

InitializeComponent();

this

.label1.Text = 

"主線程Id:"

+Thread.CurrentThread.ManagedThreadId;

}

private

async 

void

button1_Click(

object

sender, EventArgs e)

{

string

imageUrl = 

"https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=3850265187,1181041963&fm=173&s=62E19A4722716A371EB097FB03009015&w=218&h=146&img.JPEG"

;

HttpClient client = 

new

HttpClient();

var

response =await  client.GetAsync(imageUrl);

if

(response.StatusCode == System.Net.HttpStatusCode.OK)

{

var

stream =await response.Content.ReadAsStreamAsync();

Image  image = Bitmap.FromStream(stream,

true

);

this

.label2.Text = 

"線程Id:"

+ Thread.CurrentThread.ManagedThreadId;

this

.pictureBox1.BackgroundImage = image;

}

}

其實不用看圖就已經知道答案了,程式運作時不報異常,就已經說明一點:await async兩個關鍵根本建立新的線程。這個涉及到異步更新UI到主線程,就不多說了。

結果如圖:

async await方法的使用說明:

  • 傳回類型: void 、Task、Task<泛型類型>
  • async、await不會建立新的線程,實作等待的效果,必須同時使用
  • 使用該方法的方法主體也要用async關鍵字

異步方法事例:

private

static

async Task<

int

> GetValueAsync(

int

a)

{

//Task.run 開啟了新的線程

await  Task.Run(() =>

{

Thread.Sleep(2000); 

//模拟耗時

Console.WriteLine(

"GetValueAsync方法結束,線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

return

a * a;

});

Console.WriteLine(

"線程ID:"

+ Thread.CurrentThread.ManagedThreadId+

"異步方法結束"

);

return

a * a;

}

調用異步方法:

private

async 

void

button1_Click(

object

sender, EventArgs e)

{

int

result =await GetValueAsync(5);

this

.label1.Text = 

"異步計算的結果"

+ result + 

"線程ID:"

+ Thread.CurrentThread.ManagedThreadId;

}<br>

7.C#異步方式之三 淺談Task

  前面剛剛了解到async await是.net 4.5出的特性,Task是.net4.0新出的特性,用來處理異步程式設計的,其實我們要知道真正實作的異步操作還是Task新增線程來實作的,但是不代表說開一個Task,就開一個線程,有可能是幾個Task在一個線程上運作的,他們并不是一一對應的關系,充分利用線程,下面的事例就已經能夠說明這一點 

Task建立

Task建立有兩種方式一種通過任務工廠指派立即運作,一種是直接執行個體化。下面這個例子建立了10個Task

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

static

void

Main(

string

[] args)

{

//啟用線程池中的線程異步執行

Task t1 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task1啟動...線程ID:"

+Thread.CurrentThread.ManagedThreadId);

});

Task t2 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task2啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

//new 執行個體化啟動

Task t3 = 

new

Task(() =>

{

Console.WriteLine(

"Task3啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

t3.Start();

Task t4 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task4啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t5 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task5啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t6 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task6啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t7 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task7啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t8 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task8啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t9 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task9啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t10 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task10啟動...線程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Console.ReadLine();

}

建立的10個Task,我們從結果中也證明了Task和線程并不是一一對應的關系,結果如圖:

Task構造函數

Task狀态

我們建立一個task,調用他的Start、Wait方法

static

void

Main(

string

[] args)

{

var

task = 

new

Task(()=> {

Console.WriteLine(

"Task建立成功"

);

});

Console.WriteLine(

"task未開始:"

+task.Status);

task.Start();

Console.WriteLine(

"task已經開始:"

+task.Status);

task.Wait();

Console.WriteLine(

"task已經等待:"

+task.Status);

}

我們從圖中可以知道,Task的生命周期如下:

Created:在已經執行個體化未Start之前的狀态

WaittingToRun:表示等待配置設定線程給Task執行

RanToCompletion:任務執行完畢

Task等待任務結果

1.Task.WaitAll從這個字面意思就知道等待所有任務執行完成,和上面例子Wait方法等待一個任務執行完成很相似,我們來看一個代碼:

var

task1 = 

new

Task(() =>

{

System.Threading.Thread.Sleep(3000);

Console.WriteLine(

"task1Created"

);

});

var

task2 = 

new

Task(() =>

{

System.Threading.Thread.Sleep(3000);

Console.WriteLine(

"task2Created"

);

});

task1.Start();

task2.Start();

Task.WaitAll(task1, task2);

Console.WriteLine(

"所有任務執行完!"

);

Console.Read();

結果輸出:

task1Created

task2Created

所有任務執行完

除了WaitAll方法還有這些常用的方法

  • Task.WaitAny:等待任何一個任務向下執行
  • Task.ContinueWith等待第一個Task完成自動啟動,觸發下一個Task,也就是當做任務完成時觸發的回調方法
  • Task.GetAwaiter().OnCompleted(Action action) :GetAwaiter 方法擷取任務的等待者,調用OnCompleted事件,任務完成時觸發

Task任務取消

static

void

Main(

string

[] args)

{

var

source = 

new

CancellationTokenSource();

var

token = source.Token;

Task t1 = Task.Run(() =>

{

Thread.Sleep(2000);

if

(token.IsCancellationRequested)

{

Console.WriteLine(

"任務已取消"

);

}

Thread.Sleep(1000);

},token);

Console.WriteLine(t1.Status);

//取消任務

source.Cancel();

Console.WriteLine(t1.Status);

Console.ReadLine();

}