C#異步的世界【下】。本站提示廣大學習愛好者:(C#異步的世界【下】)文章只能為提供參考,不一定能成為您想要的結果。以下是C#異步的世界【下】正文
接上篇:《C#異步的世界【上】》
上篇主要分析了async\await之前的一些異步模式,今天說異步的主要是指C#5的async\await異步。在此為了方便的表述,我們稱async\await之前的異步為“舊異步”,async\await為“新異步”。
新異步的使用只能說新異步的使用太簡單(如果僅僅只是說使用)
方法加上async修飾符,然後使用await關鍵字執行異步方法,即可。對就是如此簡單。像使用同步方法邏輯一樣使用異步。
public async Task<int> Test()
{
var num1 = await GetNumber(1);
var num2 = await GetNumber(num1);
var task = GetNumber(num2);
//或者
var num3 = await task;
return num1 + num2 + num3;
}
新異步的優勢
在此之前已經有了多種異步模式,為什麼還要引入和學習新的async\await異步呢?當然它肯定是有其獨特的優勢。
我們分兩個方面來分析:WinForm、WPF等單線程UI程序和Web後台服務程序。
對於WinForm、WPF等單線程UI程序代碼1(舊異步)
private void button1_Click(object sender, EventArgs e)
{
var request = WebRequest.Create("https://github.com/");
request.BeginGetResponse(new AsyncCallback(t =>
{
//(1)處理請求結果的邏輯必須寫這裡
label1.Invoke((Action)(() => { label1.Text = "[舊異步]執行完畢!"; }));//(2)這裡跨線程訪問UI需要做處理
}), null);
}
代碼2(同步)
private void button3_Click(object sender, EventArgs e)
{
HttpClient http = new HttpClient();
var htmlStr = http.GetStringAsync("https://github.com/").Result;
//(1)處理請求結果的邏輯可以寫這裡
label1.Text = "[同步]執行完畢!";//(2)不在需要做跨線程UI處理了
}
代碼3(新異步)
private async void button2_Click(object sender, EventArgs e)
{
HttpClient http = new HttpClient();
var htmlStr = await http.GetStringAsync("https://github.com/");
//(1)處理請求結果的邏輯可以寫這裡
label1.Text = "[新異步]執行完畢!";//(2)不在需要做跨線程UI處理了
}
新異步的優勢:
是的,說得再多還不如看看實際效果圖來得實際:(新舊異步UI線程沒有阻塞,同步阻塞了UI線程)

【思考】:舊的異步模式是開啟了一個新的線程去執行,不會阻塞UI線程。這點很好理解。可是,新的異步看上去和同步區別不大,為什麼也不會阻塞界面呢?
【原因】:新異步,在執行await表達式前都是使用UI線程,await表達式後會啟用新的線程去執行異步,直到異步執行完成並返回結果,然後再回到UI線程(據說使用了SynchronizationContext)。所以,await是沒有阻塞UI線程的,也就不會造成界面的假死。
【注意】:我們在演示同步代碼的時候使用了Result。然,在UI單線程程序中使用Result來使異步代碼當同步代碼使用是一件很危險的事(起碼對於不太了解新異步的同學來說是這樣)。至於具體原因稍候再分析(哎呀,別跑啊)。
對於Web後台服務程序也許對於後台程序的影響沒有單線程程序那麼直觀,但其價值也是非常大的。且很多人對新異步存在誤解。
【誤解】:新異步可以提升Web程序的性能。
【正解】:異步不會提升單次請求結果的時間,但是可以提高Web程序的吞吐量。
1、為什麼不會提升單次請求結果的時間?
其實我們從上面示例代碼(雖然是UI程序的代碼)也可以看出。

2、為什麼可以提高Web程序的吞吐量?
那什麼是吞吐量呢,也就是本來只能十個人同時訪問的網站現在可以二十個人同時訪問了。也就是常說的並發量。
還是用上面的代碼來解釋。[代碼2] 阻塞了UI線程等待請求結果,所以UI線程被占用,而[代碼3]使用了新的線程請求,所以UI線程沒有被占用,而可以繼續響應UI界面。
那問題來了,我們的Web程序天生就是多線程的,且web線程都是跑的線程池線程(使用線程池線程是為了避免不斷創建、銷毀線程所造成的資源成本浪費),而線程池線程可使用線程數量是一定的,盡管可以設置,但它還是會在一定范圍內。如此一來,我們web線程是珍貴的(物以稀為貴),不能濫用。用完了,那麼其他用戶請求的時候就無法處理直接503了。
那什麼算是濫用呢?比如:文件讀取、URL請求、數據庫訪問等IO請求。如果用web線程來做這個耗時的IO操作那麼就會阻塞web線程,而web線程阻塞得多了web線程池線程就不夠用了。也就達到了web程序最大訪問數。
此時我們的新異步橫空出世,解放了那些原本處理IO請求而阻塞的web線程(想偷懶?沒門,干活了。)。通過異步方式使用相對廉價的線程(非web線程池線程)來處理IO操作,這樣web線程池線程就可以解放出來處理更多的請求了。
不信?下面我們來測試下:
【測試步驟】:
1、新建一個web api項目
2、新建一個數據訪問類,分別提供同步、異步方法(在方法邏輯執行前後讀取時間、線程id、web線程池線程使用數)
public class GetDataHelper
{
/// <summary>
/// 同步方法獲取數據
/// </summary>
/// <returns></returns>
public string GetData()
{
var beginInfo = GetBeginThreadInfo();
using (HttpClient http = new HttpClient())
{
http.GetStringAsync("https://github.com/").Wait();//注意:這裡是同步阻塞
}
return beginInfo + GetEndThreadInfo();
}
/// <summary>
/// 異步方法獲取數據
/// </summary>
/// <returns></returns>
public async Task<string> GetDataAsync()
{
var beginInfo = GetBeginThreadInfo();
using (HttpClient http = new HttpClient())
{
await http.GetStringAsync("https://github.com/");//注意:這裡是異步等待
}
return beginInfo + GetEndThreadInfo();
}
public string GetBeginThreadInfo()
{
int t1, t2, t3;
ThreadPool.GetAvailableThreads(out t1, out t3);
ThreadPool.GetMaxThreads(out t2, out t3);
return string.Format("開始:{0:mm:ss,ffff} 線程Id:{1} Web線程數:{2}",
DateTime.Now,
Thread.CurrentThread.ManagedThreadId,
t2 - t1);
}
public string GetEndThreadInfo()
{
int t1, t2, t3;
ThreadPool.GetAvailableThreads(out t1, out t3);
ThreadPool.GetMaxThreads(out t2, out t3);
return string.Format(" 結束:{0:mm:ss,ffff} 線程Id:{1} Web線程數:{2}",
DateTime.Now,
Thread.CurrentThread.ManagedThreadId,
t2 - t1);
}
}
3、新建一個web api控制器
[HttpGet]
public async Task<string> Get(string str)
{
GetDataHelper sqlHelper = new GetDataHelper();
switch (str)
{
case "異步處理"://
return await sqlHelper.GetDataAsync();
case "同步處理"://
return sqlHelper.GetData();
}
return "參數不正確";
}
4、發布web api程序,部署到本地iis(同步鏈接:http://localhost:803/api/Home?str=同步處理 異步鏈接:http://localhost:803/api/Home?str=異步處理)
5、接著上面的winform程序裡面測試請求:(同時發起10個請求)

private void button6_Click(object sender, EventArgs e)
{
textBox1.Text = "";
label1.Text = "";
Task.Run(() =>
{
TestResultUrl("http://localhost:803/api/Home?str=同步處理");
});
}
private void button5_Click(object sender, EventArgs e)
{
textBox1.Text = "";
label1.Text = "";
Task.Run(() =>
{
TestResultUrl("http://localhost:803/api/Home?str=異步處理");
});
}
public void TestResultUrl(string url)
{
int resultEnd = 0;
HttpClient http = new HttpClient();
int number = 10;
for (int i = 0; i < number; i++)
{
new Thread(async () =>
{
var resultStr = await http.GetStringAsync(url);
label1.Invoke((Action)(() =>
{
textBox1.AppendText(resultStr.Replace(" ", "\r\t") + "\r\n");
if (++resultEnd >= number)
{
label1.Text = "全部執行完畢";
}
}));
}).Start();
}
}
View Code
6、重啟iis,並用浏覽器訪問一次要請求的鏈接地址(預熱)
7、啟動winform程序,點擊“訪問同步實現的Web”:


8、重復6,然後重新啟動winform程序點擊“訪問異步實現的Web”

看到這些數據有什麼感想?
數據和我們前面的【正解】完全吻合。仔細觀察,每個單次請求用時基本上相差不大。 但是步驟7"同步實現"最高投入web線程數是10,而步驟8“異步實現”最高投入web線程數是3。
也就是說“異步實現”使用更少的web線程完成了同樣的請求數量,如此一來我們就有更多剩余的web線程去處理更多用戶發起的請求。
接著我們還發現同步實現請求前後的線程ID是一致的,而異步實現前後線程ID不一定一致。再次證明執行await異步前釋放了主線程。
【結論】:
【圖解】:

我們在分析UI單線程程序的時候說過,要慎用異步的Result屬性。下面我們來分析:
private void button4_Click(object sender, EventArgs e)
{
label1.Text = GetUlrString("https://github.com/").Result;
}
public async Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url);
}
}
代碼 GetUlrString("https://github.com/").Result 的Result屬性會阻塞(占用)UI線程,而執行到GetUlrString方法的 await異步的時候又要釋放UI線程。此時矛盾就來了,由於線程資源的搶占導致死鎖。
且Result屬性和.Wait()方法一樣會阻塞線程。此等問題在Web服務程序裡面一樣存在。(區別:UI單次線程程序和web服務程序都會釋放主線程,不同的是Web服務線程不一定會回到原來的主線程,而UI程序一定會回到原來的UI線程)
我們前面說過,.net為什麼會這麼智能的自動釋放主線程然後等待異步執行完畢後又回到主線程是因為SynchronizationContext的功勞。
但這裡有個例外,那就是控制台程序裡面是沒有SynchronizationContext的。所以這段代碼放在控制台裡面運行是沒有問題的。
static void Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
GetUlrString("https://github.com/").Wait();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
public async static Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return await http.GetStringAsync(url);
}
}
打印出來的都是同一個線程ID
使用AsyncHelper在同步代碼裡面調用異步但可是,可但是,我們必須在同步方法裡面執行異步怎辦?辦法肯定是有的
我們首先定義一個AsyncHelper靜態類:
static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> func)
{
_myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
}
然後調用異步:
private void button7_Click(object sender, EventArgs e)
{
label1.Text = AsyncHelper.RunSync(() => GetUlrString("https://github.com/"));
}
這樣就不會死鎖了。
ConfigureAwait除了AsyncHelper我們還可以使用Task的ConfigureAwait方法來避免死鎖
private void button7_Click(object sender, EventArgs e)
{
label1.Text = GetUlrString("https://github.com/").Result;
}
public async Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url).ConfigureAwait(false);
}
}
ConfigureAwait的作用:使當前async方法的await後續操作不需要恢復到主線程(不需要保存線程上下文)。

關於新異步裡面拋出異常的正確姿勢。我們先來看下面一段代碼:
private async void button8_Click(object sender, EventArgs e)
{
Task<string> task = GetUlrStringErr(null);
Thread.Sleep(1000);//一段邏輯。。。。
textBox1.Text = await task;
}
public async Task<string> GetUlrStringErr(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new Exception("url不能為空");
}
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url);
}
}
調試執行執行流程:

在執行完118行的時候竟然沒有把異常拋出來?這不是逆天了嗎。非得在等待await執行的時候才報錯,顯然119行的邏輯執行是沒有什麼意義的。讓我們把異常提前拋出:

提取一個方法來做驗證,這樣就能及時的拋出異常了。有朋友會說這樣的太坑爹了吧,一個驗證還非得另外寫個方法。接下來我們提供一個沒有這麼坑爹的方式:

在異步函數裡面用匿名異步函數進行包裝,同樣可以實現及時驗證。
感覺也不比前種方式好多少...可是能怎麼辦呢。
異步的實現上面簡單分析了新異步能力和屬性。接下來讓我們繼續揭秘異步的本質,神秘的外套下面究竟是怎麼實現的。
首先我們編寫一個用來反編譯的示例:
class MyAsyncTest
{
public async Task<string> GetUrlStringAsync(HttpClient http, string url, int time)
{
await Task.Delay(time);
return await http.GetStringAsync(url);
}
}
反編譯代碼:
點擊看大圖
為了方便閱讀,我們把編譯器自動命名的類型重命名。
GetUrlStringAsync 方法變成了如此模樣:
public Task<string> GetUrlStringAsync(HttpClient http, string url, int time)
{
GetUrlStringAsyncdStateMachine stateMachine = new GetUrlStringAsyncdStateMachine()
{
_this = this,
http = http,
url = url,
time = time,
_builder = AsyncTaskMethodBuilder<string>.Create(),
_state = -1
};
stateMachine._builder.Start(ref stateMachine);
return stateMachine._builder.Task;
}
方法簽名完全一致,只是裡面的內容變成了一個狀態機 GetUrlStringAsyncdStateMachine 的調用。此狀態機就是編譯器自動創建的。下面來看看神秘的狀態機是什麼鬼:
private sealed class GetUrlStringAsyncdStateMachine : IAsyncStateMachine
{
public int _state;
public MyAsyncTest _this;
private string _str1;
public AsyncTaskMethodBuilder<string> _builder;
private TaskAwaiter taskAwaiter1;
private TaskAwaiter<string> taskAwaiter2;
//異步方法的三個形參都到這裡來了
public HttpClient http;
public int time;
public string url;
private void MoveNext()
{
string str;
int num = this._state;
try
{
TaskAwaiter awaiter;
MyAsyncTest.GetUrlStringAsyncdStateMachine d__;
string str2;
switch (num)
{
case 0:
break;
case 1:
goto Label_00CD;
default:
//這裡是異步方法 await Task.Delay(time);的具體實現
awaiter = Task.Delay(this.time).GetAwaiter();
if (awaiter.IsCompleted)
{
goto Label_0077;
}
this._state = num = 0;
this.taskAwaiter1 = awaiter;
d__ = this;
this._builder.AwaitUnsafeOnCompleted<TaskAwaiter, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter, ref d__);
return;
}
awaiter = this.taskAwaiter1;
this.taskAwaiter1 = new TaskAwaiter();
this._state = num = -1;
Label_0077:
awaiter.GetResult();
awaiter = new TaskAwaiter();
//這裡是異步方法await http.GetStringAsync(url);的具體實現
TaskAwaiter<string> awaiter2 = this.http.GetStringAsync(this.url).GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_00EA;
}
this._state = num = 1;
this.taskAwaiter2 = awaiter2;
d__ = this;
this._builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter2, ref d__);
return;
Label_00CD:
awaiter2 = this.taskAwaiter2;
this.taskAwaiter2 = new TaskAwaiter<string>();
this._state = num = -1;
Label_00EA:
str2 = awaiter2.GetResult();
awaiter2 = new TaskAwaiter<string>();
this._str1 = str2;
str = this._str1;
}
catch (Exception exception)
{
this._state = -2;
this._builder.SetException(exception);
return;
}
this._state = -2;
this._builder.SetResult(str);
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
明顯多個異步等待執行的時候就是在不斷調用狀態機中的MoveNext()方法。經驗來至我們之前分析過的IEumerable,不過今天的這個明顯復雜度要高於以前的那個。猜測是如此,我們還是來驗證下事實:
在起始方法 GetUrlStringAsync 第一次啟動狀態機 stateMachine._builder.Start(ref stateMachine);

確實是調用了 MoveNext 。因為_state的初始值是-1,所以執行到了下面的位置:

繞了一圈又回到了 MoveNext 。由此,我們可以現象成多個異步調用就是在不斷執行MoveNext直到結束。
說了這麼久有什麼意思呢,似乎忘記了我們的目的是要通過之前編寫的測試代碼來分析異步的執行邏輯的。
再次貼出之前的測試代碼,以免忘記了。

反編譯後代碼執行邏輯圖:

當然這只是可能性較大的執行流程,但也有 awaiter.Iscompleted 為 true 的情況。其他可能的留著大家自己去琢磨吧。
在此,異步的分析到此結束。由於博主能力有限,某些地方描述不正確,某些避重就輕了。請大家多多包含。當然,歡迎補充!
本文已同步至索引目錄:《C#基礎知識鞏固》
本文demo:https://github.com/zhaopeiym/BlogDemoCode
【推薦】
http://www.cnblogs.com/wisdomqq/archive/2012/03/29/2417723.html