前言
最近在學習Web Api框架的時候接觸到了async/await,這個特性是.NET 4.5引入的,因爲以前對於異步編程不是很瞭解,因此花費了一些時間學習一下相關的知識,並整理成這篇博客,若是在閱讀的過程當中發現不對的地方,歡迎你們指正。python
同步編程與異步編程
一般狀況下,咱們寫的C#代碼就是同步的,運行在同一個線程中,從程序的第一行代碼到最後一句代碼順序執行。而異步編程的核心是使用多線程,經過讓不一樣的線程執行不一樣的任務,實現不一樣代碼的並行運行。正則表達式
前臺線程與後臺線程
關於多線程,早在.NET2.0時代,基礎類庫中就提供了Thread實現。默認狀況下,實例化一個Thread建立的是前臺線程,只要有前臺線程在運行,應用程序的進程就一直處於運行狀態,以控制檯應用程序爲例,在Main方法中實例化一個Thread,這個Main方法就會等待Thread線程執行完畢才退出。而對於後臺線程,應用程序將不考慮其是否執行完畢,只要應用程序的主線程和前臺線程執行完畢就能夠退出,退出後全部的後臺線程將被自動終止。來看代碼應該更清楚一些:編程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Threading;
using
System.Threading.Tasks;
namespace
ConsoleApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"主線程開始"
);
//實例化Thread,默認建立前臺線程
Thread t1 =
new
Thread(DoRun1);
t1.Start();
//能夠經過修改Thread的IsBackground,將其變爲後臺線程
Thread t2 =
new
Thread(DoRun2) { IsBackground =
true
};
t2.Start();
Console.WriteLine(
"主線程結束"
);
}
static
void
DoRun1()
{
Thread.Sleep(500);
Console.WriteLine(
"這是前臺線程調用"
);
}
static
void
DoRun2()
{
Thread.Sleep(1500);
Console.WriteLine(
"這是後臺線程調用"
);
}
}
}
|
運行上面的代碼,能夠看到DoRun2方法的打印信息「這是後臺線程調用」將不會被顯示出來,由於應用程序執行完主線程和前臺線程後,就自動退出了,全部的後臺線程將被自動終止。這裏後臺線程設置了等待1.5s,假如這個後臺線程比前臺線程或主線程提早執行完畢,對應的信息「這是後臺線程調用」將能夠被成功打印出來。多線程
Task
.NET 4.0推出了新一代的多線程模型Task。async/await特性是與Task緊密相關的,因此在瞭解async/await前必須充分了解Task的使用。這裏將以一個簡單的Demo來看一下Task的使用,同時與Thread的建立方式作一下對比。框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
namespace
TestApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"主線程啓動"
);
//.NET 4.5引入了Task.Run靜態方法來啓動一個線程
Task.Run(() => { Thread.Sleep(1000); Console.WriteLine(
"Task1啓動"
); });
//Task啓動的是後臺線程,假如要在主線程中等待後臺線程執行完畢,能夠調用Wait方法
Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine(
"Task2啓動"
); });
task.Wait();
Console.WriteLine(
"主線程結束"
);
}
}
}
Task的使用
|
首先,必須明確一點是Task啓動的線程是後臺線程,不過能夠經過在Main方法中調用task.Wait()方法,使應用程序等待task執行完畢。Task與Thread的一個重要區分點是:Task底層是使用線程池的,而Thread每次實例化都會建立一個新的線程。這裏能夠經過這段代碼作一次驗證:異步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
namespace
TestApp
{
class
Program
{
static
void
DoRun1()
{
Console.WriteLine(
"Thread Id ="
+ Thread.CurrentThread.ManagedThreadId);
}
static
void
DoRun2()
{
Thread.Sleep(50);
Console.WriteLine(
"Task調用Thread Id ="
+ Thread.CurrentThread.ManagedThreadId);
}
static
void
Main(
string
[] args)
{
for
(
int
i = 0; i < 50; i++)
{
new
Thread(DoRun1).Start();
}
for
(
int
i = 0; i < 50; i++)
{
Task.Run(() => { DoRun2(); });
}
//讓應用程序不當即退出
Console.Read();
}
}
}
Task底層使用線程池
|
運行代碼,能夠看到DoRun1()方法每次的Thread Id都是不一樣的,而DoRun2()方法的Thread Id是重複出現的。咱們知道線程的建立和銷燬是一個開銷比較大的操做,Task.Run()每次執行將不會當即建立一個新線程,而是到CLR線程池查看是否有空閒的線程,有的話就取一個線程處理這個請求,處理完請求後再把線程放回線程池,這個線程也不會當即撤銷,而是設置爲空閒狀態,可供線程池再次調度,從而減小開銷。async
Task<TResult>
Task<TResult>是Task的泛型版本,這兩個之間的最大不一樣是Task<TResult>能夠有一個返回值,看一下代碼應該一目瞭然:ide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
namespace
TestApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"主線程開始"
);
Task<
string
> task = Task<
string
>.Run(() => { Thread.Sleep(1000);
return
Thread.CurrentThread.ManagedThreadId.ToString(); });
Console.WriteLine(task.Result);
Console.WriteLine(
"主線程結束"
);
}
}
}
Task<TResult>的使用
|
Task<TResult>的實例對象有一個Result屬性,當在Main方法中調用task.Result的時候,將等待task執行完畢並獲得返回值,這裏的效果跟調用task.Wait()是同樣的,只是多了一個返回值。異步編程
async/await 特性
通過前面的鋪墊,終於迎來了這篇文章的主角async/await,仍是先經過代碼來感覺一下這兩個特性的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
namespace
TestApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"-------主線程啓動-------"
);
Task<
int
> task = GetLengthAsync();
Console.WriteLine(
"Main方法作其餘事情"
);
Console.WriteLine(
"Task返回的值"
+ task.Result);
Console.WriteLine(
"-------主線程結束-------"
);
}
static
async Task<
int
> GetLengthAsync()
{
Console.WriteLine(
"GetLengthAsync Start"
);
string
str = await GetStringAsync();
Console.WriteLine(
"GetLengthAsync End"
);
return
str.Length;
}
static
Task<
string
> GetStringAsync()
{
return
Task<
string
>.Run(() => { Thread.Sleep(2000);
return
"finished"
; });
}
}
}
async/await 用法
|
首先來看一下async關鍵字。async用來修飾方法,代表這個方法是異步的,聲明的方法的返回類型必須爲:void或Task或Task<TResult>。返回類型爲Task的異步方法中無需使用return返回值,而返回類型爲Task<TResult>的異步方法中必須使用return返回一個TResult的值,如上述Demo中的異步方法返回一個int。
再來看一下await關鍵字。await必須用來修飾Task或Task<TResult>,並且只能出如今已經用async關鍵字修飾的異步方法中。
一般狀況下,async/await必須成對出現纔有意義,假如一個方法聲明爲async,但卻沒有使用await關鍵字,則這個方法在執行的時候就被看成同步方法,這時編譯器也會拋出警告提示async修飾的方法中沒有使用await,將被做爲同步方法使用。瞭解了關鍵字async\await的特色後,咱們來看一下上述Demo在控制檯會輸入什麼吧。
輸出的結果已經很明確地告訴咱們整個執行流程了。GetLengthAsync異步方法剛開始是同步執行的,因此」GetLengthAsync Start」字符串會被打印出來,直到遇到第一個await關鍵字,真正的異步任務GetStringAsync開始執行,await至關於起到一個標記/喚醒點的做用,同時將控制權放回給Main方法,」Main方法作其餘事情」字符串會被打印出來。以後因爲Main方法須要訪問到task.Result,因此就會等待異步方法GetLengthAsync的執行,而GetLengthAsync又等待GetStringAsync的執行,一旦GetStringAsync執行完畢,就會回到await GetStringAsync這個點上執行往下執行,這時」GetLengthAsync End」字符串就會被打印出來。
固然,咱們也可使用下面的方法完成上面控制檯的輸出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
namespace
TestApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"-------主線程啓動-------"
);
Task<
int
> task = GetLengthAsync();
Console.WriteLine(
"Main方法作其餘事情"
);
Console.WriteLine(
"Task返回的值"
+ task.Result);
Console.WriteLine(
"-------主線程結束-------"
);
}
static
Task<
int
> GetLengthAsync()
{
Console.WriteLine(
"GetLengthAsync Start"
);
Task<
int
> task = Task<
int
>.Run(() => {
string
str = GetStringAsync().Result;
Console.WriteLine(
"GetLengthAsync End"
);
return
str.Length; });
return
task;
}
static
Task<
string
> GetStringAsync()
{
return
Task<
string
>.Run(() => { Thread.Sleep(2000);
return
"finished"
; });
}
}
}
不使用async\await
|
對比兩種方法,是否是async\await關鍵字的原理其實就是經過使用一個線程完成異步調用嗎?答案是否認的。async關鍵字代表能夠在方法內部使用await關鍵字,方法在執行到await前都是同步執行的,運行到await處就會掛起,並返回到Main方法中,直到await標記的Task執行完畢,才喚醒回到await點上,繼續向下執行。更深刻點的介紹能夠查看文章末尾的參考文獻。
async/await 實際應用
微軟已經對一些基礎類庫的方法提供了異步實現,接下來將實現一個例子來介紹一下async/await的實際應用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Web;
using
System.Threading;
using
System.Threading.Tasks;
using
System.Net;
namespace
TestApp
{
class
Program
{
static
void
Main(
string
[] args)
{
Console.WriteLine(
"開始獲取博客園首頁字符數量"
);
Task<
int
> task1 = CountCharsAsync(
"http://www.cnblogs.com"
);
Console.WriteLine(
"開始獲取百度首頁字符數量"
);
Task<
int
> task2 = CountCharsAsync(
"http://www.baidu.com"
);
Console.WriteLine(
"Main方法中作其餘事情"
);
Console.WriteLine(
"博客園:"
+ task1.Result);
Console.WriteLine(
"百度:"
+ task2.Result);
}
static
async Task<
int
> CountCharsAsync(
string
url)
{
WebClient wc =
new
WebClient();
string
result = await wc.DownloadStringTaskAsync(
new
Uri(url));
return
result.Length;
}
}
}
Demo
|
參考文獻:<IIIustrated C# 2012> 關於async/await的FAQ