這是2011年 談C# 編譯器編譯前的程式碼擴展行為 的續篇,當年該文章由C# 1.0討論到4.0,中間也過了好多年,今年終於興起來寫個續篇了,如果你沒看過前篇,建議看這篇前先瀏覽一下,該文中提及的東西至今仍然未過時,語言的東西不比特定技術,很少會發生Breaking Changes或是整個Feature移除,頂多只是改善。
導讀
這是2011年 談C# 編譯器編譯前的程式碼擴展行為 的續篇,當年該文章由C# 1.0討論到4.0,中間也過了好多年,今年終於興起來寫個續篇了,如果你沒看過前篇,建議看這篇前先瀏覽一下,該文中提及的東西至今仍然未過時,語言的東西不比特定技術,很少會發生Breaking Changes或是整個Feature移除,頂多只是改善,如果對Task及Thread Pool不熟,可以參考 The Parallel Programming Of .NET Framework 4.0(2) -Task Library這系列。
這次續篇我沒打算一次寫完,大概會分成5.0、6.0/7.0三篇,不過間隔應該不會太久,因為相較於6.0/7.0,5.0才是最複雜的。
C# 5.0
C# 5.0新增兩個語言特性,一個是async/await語法,另一個是Caller Information,Caller Information屬於Runtime與Attribute的合作,並不算程式碼擴展,所以本文不會提及。
async/ await
C# 4.0的主軸是Dynamic Programming,C# 5.0的主軸則是Asynchronous Programming,主要目的在於將C# 打造成非同步語言,所謂的非同步指的是讓設計師可以藉助語言及平台的支援,讓程式充分運用多核心CPU的功能,最大幅度降低無謂的阻塞狀態,呃…..白話點說就是讓你的程式可以在同樣的時間內處理更多工作,當然,前提是你得先搞懂async/await是怎麼回事,內部是如何運作,那怎麼樣叫做通透呢? 很簡單,當你看到一段async/await程式碼時,能正確的描述其中的脈絡,這很簡單是嗎? 至少對我而言,這東西一點都不簡單。讓我們由最簡單的一個Console例子開始看。
public static async void DoWork1()
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
過了這麼多年,相信看不懂這程式片段的人不多,這是標準的async/await寫法,那麼這個程式與下面這個程式有不同嗎?
public static void DoWork2()
{
WebClient client = new WebClient();
var content = client.DownloadString("http://www.google.com");
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
大概98%以上的人都知道,DoWork2是一個阻塞行為,因為client.DownloadString會等待網路資料的回傳,DoWork1則是藉助await,不等待,但她如何達到不等待,脈絡如何? 我們等下再談。
接著是下面這個程式,很貼近DoWork1,幾近是DoWork1的非async版。
public static void DoWork3()
{
WebClient client = new WebClient();
client.DownloadStringCompleted += (s, args)=>
{
var content = args.Result;
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
};
client.DownloadStringAsync(new Uri("http://www.google.com"));
}
你真的可以退100步想,DoWork1 = DoWork3。接著是下面這個。
public static void DoWork4()
{
WebRequest wq = WebRequest.Create("http://www.google.com");
wq.BeginGetResponse((state) =>
{
var resp = wq.EndGetResponse(state);
using (var sr = new StreamReader(resp.GetResponseStream()))
{
var content = sr.ReadToEnd();
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
}, null);
}
請問,你可以回答出DoWork3 與 DoWork4的脈絡是相同的嗎? 等下分曉,再看一個。
public static void DoWork5()
{
HttpClient client = new HttpClient();
client.GetStringAsync("http://www.google.com").ContinueWith((result) =>
{
if (result.Result.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
});
}
DoWork5與DoWork1是一樣的嗎? DoWork5與DoWork4是一樣的嗎?林志玲跟波多野差在哪? (呃,跳tone了….)。
其實它們的結果都是一樣的,但脈絡不同,簡單的說,就是運用Thread的手法不同,也因為是這些不同,才使得async/await強大,但也使得其變得複雜。
讓我們利用Thread ID來追蹤它們的脈絡,首先是DoWork1_1(DoWork1)。
public static async void DoWork1_1()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
var content = await client.GetStringAsync("http://www.google.com");
Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
結果是這樣。
before:9
after:15
found
由await為分界,切開成為兩個Thread,這是編譯器擴展手法所致,我們後面再談。再看DoWork2的版本。
public static void DoWork2_1()
{
WebClient client = new WebClient();
Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
var content = client.DownloadString("http://www.google.com");
Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
這是阻塞版本,所以兩個Thread一定是一樣的。
before:9
after:9
found
看DoWork3。
public static void DoWork3_1()
{
WebClient client = new WebClient();
Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
client.DownloadStringCompleted += (s, args) =>
{
Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
var content = args.Result;
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
};
client.DownloadStringAsync(new Uri("http://www.google.com"));
}
before:9
after:21
found
一樣是兩條Thread,看DoWork4。
public static void DoWork4_1()
{
WebRequest wq = WebRequest.Create("http://www.google.com");
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId);
wq.BeginGetResponse((state) =>
{
var resp = wq.EndGetResponse(state);
using (var sr = new StreamReader(
resp.GetResponseStream()))
{
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId);
var content = sr.ReadToEnd();
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
}, null);
}
before:9
after:14
found
一樣,看DoWork5。
public static void DoWork5_1()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId);
client.GetStringAsync(
"http://www.google.com").ContinueWith((result) =>
{
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId);
if (result.Result.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
});
}
before:9
after:11
found
還是一樣,所以除了DoWork2是阻塞寫法外,其他DoWork1、3、4、5都是兩條Thread,那麼它們都是一樣的嗎? 為了證明她們是不是一樣的,我們再寫另一組來測試,就概念上而言,非同步的寫法其實跟Thread Pool有很大關係,因為非同步多半會用到Thread,而.NET在處理非同步的時候會利用Thread Pool來最有效利用Thread。下面這個例子設定了最大的Thread Pool所能開出的最大Thread,並利用ThreadStatic來針對每個Thread標記(也就是說,在每個Thread中都有一個_tag變數,彼此不干擾)。
[ThreadStatic]
private static volatile int _tag;
public static void SetEnviorment()
{
ThreadPool.SetMinThreads(50, 12);
ThreadPool.SetMaxThreads(50, 12);
for (int i = 0; i < 50; i++)
{
ThreadPool.QueueUserWorkItem((s) =>
{
_tag = -1;
Thread.Sleep(2000);
}, null);
}
Thread.Sleep(4000);
}
這裡我們設定Thread Pool最大的Threads數量是50條,後面的12指的是IO Completion Thread,這是一個很特別的概念,我們後面會詳細討論。再設好數量後,這裡利用迴圈為這50條Threads裡面的_tag變數設值,這個用途是後面使用async/await或是傳統非同步作業時,可以辨識該Thread是來自於Thread Pool或不是。完成後看DoWork1的第三版本。
public static async void DoWork1_2()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
var content = await client.GetStringAsync("http://www.google.com");
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
注意,要呼叫前得先呼叫SetEnviorment來限制Thread Pool。
static void Main(string[] args)
{
SetEnviorment();
_tag = -3;
DoWork1_2();
Console.ReadLine();
}
能猜到結果嗎?
before:9,-3
after:62,0
found
第一行的tag是-3,這是因為我們在主Thread設定tag為-3,接著await之後出現的數字很神奇,她是0,明顯不是來自Thread Pool,事實上,她是IO Completion Thread,不過後面再談。不過如果你以為await之後tag一定是0,那麼就錯了,看下面的這版。
public static async void DoWork1_3()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
await Task.Delay(1000);
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
}
before:9,-3
after:10,-1
視await後面呼叫的方法,await之後不一定都是來自Thread Pool,也不一定都是來自IO Completion Thread。
基本上DoWork2是阻塞,所以我們直接跳DoWork3第三版本。
public static void DoWork3_2()
{
WebClient client = new WebClient();
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
client.DownloadStringCompleted += (s, args) =>
{
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
var content = args.Result;
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
};
client.DownloadStringAsync(new Uri("http://www.google.com"));
}
結果是啥呢?
before:10,-3
after:42,-1
found
所以,我們之前假設DoWork1 = DoWork3是錯的,兩者後面的Thread不同,一個是來自IO Completion Thread,一個是來自Thread Pool。
看DoWork4。
public static void DoWork4_2()
{
WebRequest wq = WebRequest.Create("http://www.google.com");
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
wq.BeginGetResponse((state) =>
{
var resp = wq.EndGetResponse(state);
using (var sr = new StreamReader(resp.GetResponseStream()))
{
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
var content = sr.ReadToEnd();
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
}, null);
}
before:9,-3
after:62,0
found
這與DoWork1大致相同,都是來自於IO Completion Thread,看DoWork5。
public static void DoWork5_2()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
client.GetStringAsync(
"http://www.google.com").ContinueWith((result) =>
{
Console.WriteLine("after:" +
Thread.CurrentThread.ManagedThreadId + "," + _tag);
if (result.Result.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
});
}
before:9,-3
after:10,-1
found
是不是開始覺得怪怪的了? 讓我們總結一下。
DoWork1以await為分界,兩條Thread,一條是Caller Thread,一條是IO Completion Thread。
DoWork3是兩條Thread,一條是Caller Thread,一條是Thread Pool Thread。
DoWork4是兩條Thread,一條是Caller Thread,一條是IO Completion Thread。
DoWork5是兩條Thread,一條是Caller Thread,一條是Thread Pool Thread。
所以,DoWork1 不等於DoWork3,也不等於DoWork5。
因此,我們可以分成兩種狀態,DoWork1、DoWork4脈絡是同樣的,DoWork3、5脈絡是一樣的。
那麼哪一種寫法正確? 這很難說,視乎情況,以Console來說,DoWork1大概是C# 5.0中最好的寫法,但如果換成Windows Form、WPF、ASP.NET就不一定,因為async/await和WebClient有個機制會捕捉Caller Context,這會使得DoWork1等於DoWork3(但不會等於DoWork5)。
public async void DoWork1()
{
HttpClient client = new HttpClient();
listBox1.Items.Add("before:" + Thread.CurrentThread.ManagedThreadId);
var content = await client.GetStringAsync("http://www.google.com");
listBox1.Items.Add("after:" + Thread.CurrentThread.ManagedThreadId);
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
public void DoWork3()
{
WebClient client = new WebClient();
listBox1.Items.Add("before:" + Thread.CurrentThread.ManagedThreadId);
client.DownloadStringCompleted += (s, args) =>
{
listBox1.Items.Add("after:" + Thread.CurrentThread.ManagedThreadId);
var content = args.Result;
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
};
client.DownloadStringAsync(new Uri("http://www.google.com"));
}
這兩個函式在Windows Form、WPF還有ASP.NET的結果是一樣的,這是因為WebClient還有await在非同步之前會捕捉SynchronizationContext,完成後會嘗試Post到SynchronizationContext去執行,簡單的說,就是所謂的UI Thread。
就結果而言,在Windows Form/WPF/ASP.NET中,如果後續動作與UI無關的話,DoWork5才是最好的寫法,可以省下捕捉 SynchronizationContext的動作及帶來的後遺症,不過缺點就是要寫成像WebClient非同步那樣,很不好懂,幸運的是其實他有另一種寫法。
private async void button2_Click(object sender, EventArgs e)
{
HttpClient client = new HttpClient();
MessageBox.Show("Before : " +
Thread.CurrentThread.ManagedThreadId.ToString());
var content = await client.GetStringAsync(
"http://www.google.com").ConfigureAwait(false);
MessageBox.Show("After : " +
Thread.CurrentThread.ManagedThreadId.ToString());
}
ConfigureAwait等於false的話,就不會捕捉SynchronizationContext了。
IO Completion Thread
通常,看到這邊你應該有點頭昏了,但同時也浮出了一些關鍵疑問,第一個是IO Completion Thread,要了解這個東西,讓我們由DoWork1開始。
public static async void DoWork1()
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
首先,GetStringAsync有沒有開Thread? 簡單的回答是沒有,在OS層級中有一個機制叫做IOCP,可以由下面的連結取得詳細介紹。
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx
簡略的說,發送IO請求可以是同步,也可以是不同步,當需要使用不同步時,就會進入IOCP的流程,以這個例子來說,GetStringAsync發出IO請求並標記為非同步,並且在結構中放入一個tag,此時這個IO動作會立刻返回,這意味著不會有回傳值,因為IO根本還沒做,當IO做完後,OS會觸發一個中斷,此時取得的結構包含了tag與回傳值,接著交給IO Completion Handling Thread,這裡的IO Completion Handling Thread會取出回傳值然後依據tag取出callback,開出IO Completion Thread來執行,也就是await 後面所在的Thread,用一張圖來解釋。
這裡要注意,IO Completion Thread Handling一開始就存在了,她是一個Thread,一個IO Completion Thread Handling可以處理上萬個IO Completion Event,所以別誤為她是GetStringAsync開出來的Thread,而且他也不是Thread Pool所管轄的。
使用IOCP的目的很簡單,就是榨取Call IO後等待IO回傳的時間,一般如果是同步,發出IO就會進入等待IO回傳,但如果是非同步,這裡不會有任何Thread等待回傳,不要以為IO Completion Thread Handling是,因為一個IO Completion Thread Handling可以服務上萬個IO請求,所以不能認為GetStringAsync會對應一個IO Completion Thread Handling,是一個IO Completion Thread Handling可以對應上萬個GetStringAsync。
所以,我們可以這樣說,當await之後的動作是IO動作,那麼不會有多餘的Thread等待IO動作結束,當IO動作結束後,有一個IO Completion Thread會開出來(IO Completion Thread通常在另一個Thread Pool)。但如果await之後的不是IO動作(例如Task.Delay),那麼就會有一個Thread在等待其回傳。
當處於WPF/Windows From/ASP.NET等有SynchronizationContext情況下,IO Completion Thread 會直接把callback Post到SynchronizationContext的Thread,但對我們而言,她是隱形的。
SynchronizationContext
async/await的出生有很大一部分是為了解決當非同步動作發生後,前後兩個Thread不同而帶來的困擾,通常在UI就會觸發不能在Main Thread以外的Thread存取UI控制項的例外。
所以如果把ConfigureAwait(false)拿掉,就不會出現這個例外,這是因為await會捕捉SynchronizationContext,在完成後把callback Post到同樣的Thread來執行,等同下面這樣。
public async void DoWork1_1()
{
HttpClient client = new HttpClient();
listBox1.Items.Add("before:" +
Thread.CurrentThread.ManagedThreadId);
var context = SynchronizationContext.Current;
var content = await client.GetStringAsync(
"http://www.google.com").ConfigureAwait(false);
context.Post((state) =>
{
listBox1.Items.Add("after:" +
Thread.CurrentThread.ManagedThreadId);
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}, null);
}
但這也帶來困擾,因為不一定每次的await都會存取UI,所以盡可能在不用存取UI時使用ConfigureAwait(false),這可以讓你的程式不會因為要回到Main Thread而排隊或是形成阻塞,另一個這樣做的好處是你可以自行控制存取UI的區段,如果依賴await,那麼await後的動作全部都會在Main Thread中排隊,但如果你使用ConfigureAwait(false),就可以將存取UI那塊自己排入Post,得到最大的非同步。當然,如果Caller 沒擁有SynchronizationContext,那這些都不需要考慮,直接await不加ConfigureAwait即可。
那麼怎麼樣可以形成阻塞?看下面的例子。
private async Task<string> GetWebResult2()
{
HttpClient client = new HttpClient();
var content =
await client.GetStringAsync("http://www.google.com");
if (content.Contains("body"))
return "html found";
else
return "html not found";
}
private void button1_Click(object sender, EventArgs e)
{
var content = GetWebResult2().Result;
MessageBox.Show(content.Length.ToString());
}
這其實不是正常的寫法,通常在使用await的時候,我們不應該直接取用Result這個屬性值,這意味著你馬上要取得回傳值,所以Result的get函式會等待直到回傳值到達,以這個例子來說,await之後嘗試將callback排入SynchronizationContext的Thread,但是取Result的get函式阻塞在SynchronizationContext的Thread,所以死結就發生了。
你有兩個選擇,一是使用ConfigureAwait(false),不嘗試回到SynchronizationContext的Thread,後遺症是你不能直接存取UI控制項,另一個是把Caller也標記為async,並一併使用await,如下。
private async void button3_Click(object sender, EventArgs e)
{
var content = await GetWebResult2();
MessageBox.Show(content.Length.ToString());
}
其實很簡單,就是不要直接取Result,除非你知道你在做什麼。
async/await的擴展
呃,終於來到這裡了,接下來來談談async/await的擴展手法,以下面這個例子來說。
private async void button1_Click(object sender, EventArgs e)
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
if (content.Contains("body"))
button1.Text = "found";
else
button1.Text = "not found";
}
會被擴展為下面這樣。
private void button1s_Click(object sender, EventArgs e)
{
d__1 stateMachine = new d__1()
{
__this = this,
sender = sender,
e = e,
t__builder = AsyncVoidMethodBuilder.Create(),
_1__state = -1
};
stateMachine.t__builder.Start<d__1> (ref stateMachine);
}
sealed class d__1 : IAsyncStateMachine
{
public int _1__state;
public Form1 __this;
private string s__3;
public AsyncVoidMethodBuilder t__builder;
private TaskAwaiter<string> u__1;
private HttpClient client_5__1;
private string content5__2;
public EventArgs e;
public object sender;
public void MoveNext()
{
int num = this._1__state;
try
{
TaskAwaiter<string> awaiter;
if (num != 0)
{
this.client_5__1 = new HttpClient();
awaiter = this.client_5__1.GetStringAsync("http://www.google.com").GetAwaiter();
if (!awaiter.IsCompleted)
{
this._1__state = num = 0;
this.u__1 = awaiter;
Form1.d__1 stateMachine = this;
this.t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Form1.d__1>(
ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = this.u__1;
this.u__1 = new TaskAwaiter<string>();
this._1__state = num = -1;
}
string result = awaiter.GetResult();
awaiter = new TaskAwaiter<string>();
this.s__3 = result;
this.content5__2 = this.s__3;
this.s__3 = null;
if (this.content5__2.Contains("body"))
{
this.__this.button1.Text = "found";
}
else
{
this.__this.button1.Text = "not found";
}
}
catch (Exception exception)
{
this._1__state = -2;
this.t__builder.SetException(exception);
return;
}
this._1__state = -2;
this.t__builder.SetResult();
}
[DebuggerHidden]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
我把擴展後的語法修正,所以這是可以編譯的程式碼,你可以逐步的追蹤來尋其脈絡,下圖是大概的流程。
更精確點,要補上Capture Context部分。
如果要更精確的模擬其行為,下面的程式碼趨近於真實行為。
public sealed class d__1simulate : IAsyncStateMachine
{
public int _1__state;
public Form1 __this;
private WebRequest client5__1;
private SynchronizationContext context;
public AsyncVoidMethodBuilder t__builder;
private string content5__2;
public EventArgs e;
public object sender;
public void MoveNext()
{
int num = this._1__state;
try
{
if (num != 0)
{
context = SynchronizationContext.Current;
this.client5__1 = WebRequest.Create("http://www.google.com");
this.client5__1.BeginGetResponse((state) =>
{
var resp = this.client5__1.EndGetResponse(state);
using (var sr = new StreamReader(resp.GetResponseStream()))
{
this.content5__2 = sr.ReadToEnd();
}
if (context != null)
context.Post((s) =>
{
MoveNext();
}, null);
else //simulate io completion thread
Task.Run(() =>
{
MoveNext();
});
}, null);
if (true) //simulate, actually is check Awaiter is complete or not.
{
this._1__state = num = 0;
return;
}
}
else
{
this._1__state = num = -1;
}
if (this.content5__2.Contains("body"))
{
this.__this.button2.Text = "found";
}
else
{
this.__this.button2.Text = "not found";
}
}
catch (Exception exception)
{
this._1__state = -2;
return;
}
this._1__state = -2;
}
[DebuggerHidden]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
private void button2_Click_1(object sender, EventArgs e)
{
d__1simulate d = new d__1simulate()
{
__this = this,
sender = sender,
e = e,
_1__state = -1
};
d.MoveNext();
}
我把HttpClient移除,改用WebRequest,也就是把HttpClient一併模擬了,連同AVoidMethodBuilder也一起模擬,這更能貼近於真實行為,不過請注意我用Task.Run來處理沒有Context的狀態,這實際上是IO Completion Thread,但在.NET裡,我們無法建立出IO Completion Thread。
再來一次
現在再回頭來看看一開始的例子,看看能不能一眼看出Thread的脈絡。
public static async void DoWork1()
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
這裡有一個IO Completion Thread會產生,在沒有SynchronizationContext的情況下(Console),await後面的程式碼是執行在IO Completion Thread 中,在有SynchronizationContext情況下(WPF/WinForm/ASP.NET)會有一個IO Completion Thread產生,隨即把執行權交給UI Thread。
public static void DoWork3()
{
WebClient client = new WebClient();
client.DownloadStringCompleted += (s, args)=>
{
var content = args.Result;
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
};
client.DownloadStringAsync(new Uri("http://www.google.com"));
}
這個例子在沒有SynchronizationContext的情況下(Console),會有一個IO Completion Thread產生,但是他隨即會把執行權交給Thread Pool所產生的Thread,在有SynchronizationContext情況下(WPF/WinForm/ASP.NET)會有一個IO Completion Thread產生,隨即把執行權交給UI Thread。
public static void DoWork4()
{
WebRequest wq = WebRequest.Create("http://www.google.com");
wq.BeginGetResponse((state) =>
{
var resp = wq.EndGetResponse(state);
using (var sr = new StreamReader(resp.GetResponseStream()))
{
var content = sr.ReadToEnd();
if (content.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
}
}, null);
}
這個例子在會有一個IO Completion Thread產生,程式碼是執行在IO Completion Thread中。
public static void DoWork5()
{
HttpClient client = new HttpClient();
client.GetStringAsync("http://www.google.com").ContinueWith((result) =>
{
if (result.Result.Contains("body"))
Console.WriteLine("found");
else
Console.WriteLine("not found");
});
}
這個例子會有一個IO Completion Thread產生,但是他隨即會把執行權交給Thread Pool所產生的Thread。
public static async void DoWork1_3()
{
HttpClient client = new HttpClient();
Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId + "," + _tag);
await Task.Delay(1000);
Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId + "," + _tag);
}
這個例子在沒有SynchronizationContext情況下,會產生一個Thread Pool Thread,執行Delay動作,完成後產生一個Thread Pool Thread來執行await後面的程式碼,在有SynchronizationContext情況下,await區段後面的程式碼會執行在UI Thread中。
async but no await
當你把一個Method標示為async時,不管有沒有使用await,編譯器都會擴展這個Method,以下面這個例子來說。
static async void Test()
{
}
會擴展成這樣。
[AsyncStateMachine(typeof(<Test>d__0)), DebuggerStepThrough]
private static void Test()
{
<Test>d__0 stateMachine = new <Test>d__0();
stateMachine.<>t__builder = AsyncVoidMethodBuilder.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<<Test>d__0>(ref stateMachine);
}
簡單的說,編譯器會擴展async Method,然後逐一尋找裡面的await區段來處理。
還有一種很詭異的寫法。
static async Task<int> Test()
{
return 15;
}
擴展後是下面這樣。
………………….
private void MoveNext()
{
int num2;
int num = this.<>1__state;
try
{
num2 = 15;
}
catch (Exception exception)
{
this.<>1__state = -2;
this.<>t__builder.SetException(exception);
return;
}
this.<>1__state = -2;
this.<>t__builder.SetResult(num2);
}
………………..
[AsyncStateMachine(typeof(<Test>d__0)), DebuggerStepThrough]
private static Task<int> Test()
{
<Test>d__0 stateMachine = new <Test>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<<Test>d__0>(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
只要在Test中沒有任何的await,也沒有呼叫任何會產生Thread的函式,不管怎麼寫,呼叫Test都不會有任何的Thread產生,這基本上就是一段無意義的寫法,所以將一個函式標示為async不代表會有Thread產生,呼叫await也不代表會有Thread產生,端看整個脈絡而定。
所以呢?
async/await在WPF/WinForm/ASP.NET與Console的行為會有些微不同,主要是SynchronizationContext的捕捉與否。
async/await在處理非同步IO時,不會等待IO的回傳,IO Completion Handling不算是等待,因為一個IO Completion Handling可以等待上萬個非同步IO。
async/await在處理非IO動作時,視其後續的的呼叫函式而定,正常情況下至少會有一個Thread,以Task.Delay來說,內部是一個System.Threading.Timer,在這個Thread結束時會透過ThreadPool執行指定的動作,也就是await後面的那段,這個脈絡跟你使用ContinueWith是一樣的(Task.Delay開出一個來自ThreadPool的Thread,結束後開出另一個ThreadPool的Thread來執行await區段或是ContinueWith指定的動作)。
async/await在處理IO動作時,沒有SynchronizationContext下,IO完成後的是一個IO Completion Thread,嚴格上來說不是我們可以控制的Thread,不來自Thread Pool。
async/await在處理IO 動作時,沒有SynchronizationContext下,同時所能處理的量依據IO Completion Thread數量而定,預設是1000,而Thread Pool則視乎OS而定及.NET 版本而定,在我的電腦,.NET Framework 4.6是1023。
這意味著當數量超過時就會排隊,下面這個例子可以模擬IO Completion Thread排隊的狀態。
[ThreadStatic]
private static volatile int _tag;
public static void SetEnviorment()
{
ThreadPool.SetMinThreads(50, 12);
ThreadPool.SetMaxThreads(50, 12);
for (int i = 0; i < 50; i++)
{
ThreadPool.QueueUserWorkItem((s) =>
{
_tag = -1;
Thread.Sleep(2000);
}, null);
}
Thread.Sleep(4000);
}
private static int _count = 0;
public static async void DoWork6()
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
Console.WriteLine(_count);
Interlocked.Increment(ref _count);
Thread.Sleep(TimeSpan.FromMinutes(1));
}
public static void DoWork6_Call()
{
for (int i = 0; i < 24; i++)
DoWork6();
}
static void Main(string[] args)
{
int max, iomx;
SetEnviorment();
DoWork6_Call();
Console.ReadLine();
}
另外,只要回傳是Task<T>,就可以用await,但Task不等於Thread。
最後,async可以用在delegate,例如下面這樣。
Task.Run(async()=>
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync("http://www.google.com");
});
另外,在5.0時 catch區段不支援async/await,這些應該大家早就知道了。
到底怎麼用?
如果不是WinForm/WPF/ASP.NET這種有SynchronizationContext的環境下,運用await不需考慮太多,是非同步IO時後面會由IO Completion Thread接手,非IO動作會由Thread Pool接手。
如果是WinForm/WPF/ASP.NET,建議多思考使用ConfigureAwait(false)的可能性,也就是最小化排入UI Thread的部分,當然,真的也不需要過度憂慮,畢竟這本來就是設計成方便UI類型寫法的,如果都不用也很浪費,除非真的很需要由這個地方榨出效能。