C# 多線程學(xué)習(xí)之基礎(chǔ)入門
線程(英語(yǔ):thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)。進(jìn)程是資源分配的基本單位。所有與該進(jìn)程有關(guān)的資源,都被記錄在進(jìn)程控制塊PCB中。以表示該進(jìn)程擁有這些資源或正在使用它們。本文以一些簡(jiǎn)單的小例子,簡(jiǎn)述如何將程序由同步方式,一步一步演變成異步多線程方式,僅供學(xué)習(xí)分享使用,如有不足之處,還請(qǐng)指正。
同步方式
業(yè)務(wù)場(chǎng)景:用戶點(diǎn)擊一個(gè)按鈕,然后做一個(gè)耗時(shí)的業(yè)務(wù)。同步方式代碼如下所示:
private void btnSync_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnSync_Click同步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 5; i++) { string name = string.Format("{0}_{1}", "btnSync_Click", i); this.DoSomethingLong(name); } Console.WriteLine("************btnSync_Click同步方法 結(jié)束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); } /// <summary> /// 模擬做一些長(zhǎng)時(shí)間的工作 /// </summary> /// <param name="name"></param> private void DoSomethingLong(string name) { Console.WriteLine("************DoSomethingLong 開(kāi)始 name= {0} 線程ID= {1} 時(shí)間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); //CPU計(jì)算累加和 long rest = 0; for (int i = 0; i < 1000000000; i++) { rest += i; } Console.WriteLine("************DoSomethingLong 結(jié)束 name= {0} 線程ID= {1} 時(shí)間 = {2} 結(jié)果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest); }
同步方式輸出結(jié)果,如下所示:
通過(guò)對(duì)以上示例進(jìn)行分析,得出結(jié)論如下:
- 同步方式按順序依次執(zhí)行。
- 同步方式業(yè)務(wù)和UI采用采用同一線程,都是主線程。
- 同步方式如果執(zhí)行操作比較耗時(shí),前端UI會(huì)卡住,無(wú)法響應(yīng)用戶請(qǐng)求。
- 同步方式比較耗時(shí)【本示例9.32秒】
異步多線程方式
如何優(yōu)化同步方式存在的問(wèn)題呢?答案是由同步方式改為異步異步多線程方式。代碼如下所示:
private void btnAsync_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync_Click異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); Action<string> action = new Action<string>(DoSomethingLong); for (int i = 0; i < 5; i++) { string name = string.Format("{0}_{1}", "btnAsync_Click", i); action.BeginInvoke(name,null,null); } Console.WriteLine("************btnAsync_Click異步方法 結(jié)束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); }
異步方式出結(jié)果,如下所示:
通過(guò)對(duì)以上示例進(jìn)行分析,得出結(jié)論如下:
- 異步方式不是順序執(zhí)行,即具有無(wú)序性。
- 異步方式采用多線程方式,和UI不是同一個(gè)線程,所以前端UI不會(huì)卡住。
- 異步多線程方式執(zhí)行時(shí)間短,響應(yīng)速度快。
通過(guò)觀察任務(wù)管理器,發(fā)現(xiàn)同步方式比較耗時(shí)間,異步方式比較耗資源【本例是CPU密集型操作】,屬于以資源換性能。同步方式和異步方式的CPU利用率,如下圖所示:
異步多線程優(yōu)化
通過(guò)上述例子,發(fā)現(xiàn)由于采用異步的原因,線程還未結(jié)束,但是排在后面的語(yǔ)句就先執(zhí)行,所以統(tǒng)計(jì)的程序執(zhí)行總耗時(shí)為0秒。為了優(yōu)化此問(wèn)題,采用async與await組合方式執(zhí)行,代碼如下所示:
private async void btnAsync2_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync_Click2異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); await DoAsync(); Console.WriteLine("************btnAsync_Click2異步方法 結(jié)束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); } /// <summary> /// 異步方法 /// </summary> /// <returns></returns> private async Task DoAsync() { Action<string> action = new Action<string>(DoSomethingLong); List<IAsyncResult> results = new List<IAsyncResult>(); for (int i = 0; i < 5; i++) { string name = string.Format("{0}_{1}", "btnAsync_Click", i); IAsyncResult result = action.BeginInvoke(name, null, null); results.Add(result); } await Task.Run(()=> { while (true) { for (int i = 0; i < results.Count; i++) { var result = results[i]; if (result.IsCompleted) { results.Remove(result); break; } } if (results.Count < 1) { break; } Thread.Sleep(200); } }); }
經(jīng)過(guò)優(yōu)化,執(zhí)行結(jié)果如下所示:
通過(guò)異步多線程優(yōu)化后的執(zhí)行結(jié)果,進(jìn)行分析后得出的結(jié)論如下:
- Action的BeginInvoke,會(huì)返回IAsyncResult接口,通過(guò)接口可以判斷是否完成。
- 如果有多個(gè)Action的多線程調(diào)用,可以通過(guò)List方式進(jìn)行。
- async與await組合,可以實(shí)現(xiàn)異步調(diào)用,防止線程阻塞。
通過(guò)以上方式,采用異步多線程的方式,共耗時(shí)3.26秒,比同步方式的9.32秒,提高了2.85倍,并非線性增加。且每次執(zhí)行的總耗時(shí)會(huì)上下浮動(dòng),并非固定值。
異步回調(diào)
?上述async與await組合,是一種實(shí)現(xiàn)異步調(diào)用的方式,其實(shí)Action本身也具有回調(diào)函數(shù)【AsyncCallback】,通過(guò)回調(diào)函數(shù)一樣可以實(shí)現(xiàn)對(duì)應(yīng)功能。具體如下所示:
/// <summary> /// 異步回調(diào) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnAsync3_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync_Click3異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); Action action = DoAsync3; AsyncCallback asyncCallback = new AsyncCallback((ar) => { if (ar.IsCompleted) { Console.WriteLine("************btnAsync_Click3異步方法 結(jié)束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); } }); action.BeginInvoke(asyncCallback, null); } private void DoAsync3() { Action<string> action = new Action<string>(DoSomethingLong); List<IAsyncResult> results = new List<IAsyncResult>(); for (int i = 0; i < 5; i++) { string name = string.Format("{0}_{1}", "btnAsync_Click3", i); IAsyncResult result = action.BeginInvoke(name, null, null); results.Add(result); } while (true) { for (int i = 0; i < results.Count; i++) { var result = results[i]; if (result.IsCompleted) { results.Remove(result); break; } } if (results.Count < 1) { break; } Thread.Sleep(200); } }
異步回調(diào)執(zhí)行示例,如下所示:
通過(guò)對(duì)異步回調(diào)方式執(zhí)行結(jié)果進(jìn)行分析,結(jié)論如下所示:
- 通過(guò)觀察線程ID可以發(fā)現(xiàn),由于對(duì)循環(huán)計(jì)算的功能進(jìn)行了封裝,為一個(gè)獨(dú)立的函數(shù),所以在Action通過(guò)BeginInvoke發(fā)起時(shí),又是一個(gè)新的線程。
- 通過(guò)async和await在通過(guò)Task.Run方式返回時(shí),也會(huì)重新生成新的線程。
- 通過(guò)回調(diào)函數(shù),可以保證異步線程的執(zhí)行順序。
- 通過(guò)Thread.Sleep(200)的方式進(jìn)行等待,會(huì)有一定時(shí)間范圍延遲。
異步信號(hào)量
信號(hào)量方式是通過(guò)BeginInvoke返回值IAsyncResult中的異步等待AsyncWaitHandle觸發(fā)信號(hào)WaitOne,可以實(shí)現(xiàn)信號(hào)的實(shí)時(shí)響應(yīng),具體代碼如下:
private void btnAsync4_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync_Click4異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); Action action = DoAsync3; var asyncResult = action.BeginInvoke(null, null); //此處中間可以做其他的工作,然后在最后等待線程的完成 asyncResult.AsyncWaitHandle.WaitOne(); Console.WriteLine("************btnAsync_Click4異步方法 結(jié)束,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); }
信號(hào)量示例截圖如下所示:
通過(guò)對(duì)異步信號(hào)量方式的測(cè)試結(jié)果進(jìn)行分析,得出結(jié)論如下:
- 信號(hào)量方式會(huì)造成線程的阻塞,且會(huì)造成前端界面卡死。
- 信號(hào)量方式適用于異步方法和等待完成之間還有其他工作需要處理的情況。
- WaitOne可以設(shè)置超時(shí)時(shí)間【最多可等待時(shí)間】。
異步多線程返回值
上述示例的委托都是無(wú)返回值類型的,那么對(duì)于有返回值的函數(shù),如何獲取呢?答案就是采用Func。示例如下所示:
private void btnAsync5_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync5_Click異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); string name = string.Format("{0}_{1}", "btnAsync_Click5", 0); Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn); IAsyncResult asyncResult = func.BeginInvoke(name, null, null); //此處中間可以做其他的工作,然后在最后等待線程的完成 int result = func.EndInvoke(asyncResult); Console.WriteLine("************btnAsync5_Click異步方法 結(jié)束,線程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId,result); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); } private int DoSomethingLongAndReturn(string name) { Console.WriteLine("************DoSomethingLong 開(kāi)始 name= {0} 線程ID= {1} 時(shí)間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); //CPU計(jì)算累加和 long rest = 0; for (int i = 0; i < 1000000000; i++) { rest += i; } Console.WriteLine("************DoSomethingLong 結(jié)束 name= {0} 線程ID= {1} 時(shí)間 = {2} 結(jié)果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest); return DateTime.Now.Day; }
采用Func方式的EndInvoke,可以獲取返回值,示例如下:
通過(guò)對(duì)Func方式的EndInvoke方法的示例進(jìn)行分析,得出結(jié)論如下所示:
- 在主線程中調(diào)用EndInvoke,會(huì)進(jìn)行阻塞,前端頁(yè)面卡死。
- Func的返回值是泛型類型,可以返回任意類型的值。
異步多線程返回值回調(diào)
為了解決以上獲取返回值時(shí),前端頁(yè)面卡死的問(wèn)題,可以采用回調(diào)函數(shù)進(jìn)行解決,如下所示:
private void btnAsync6_Click(object sender, EventArgs e) { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine("************btnAsync6_Click異步方法 開(kāi)始,線程ID= {0}************", Thread.CurrentThread.ManagedThreadId); string name = string.Format("{0}_{1}", "btnAsync_Click6", 0); Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn); AsyncCallback callback = new AsyncCallback((asyncResult) => { int result = func.EndInvoke(asyncResult); Console.WriteLine("************btnAsync6_Click異步方法 結(jié)束,線程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId, result); watch.Stop(); Console.WriteLine("************總耗時(shí)= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00")); }); func.BeginInvoke(name, callback, null); }
采用回調(diào)方式,示例截圖如下:
通過(guò)對(duì)回調(diào)方式的示例進(jìn)行分析,得出結(jié)論如下:
- 異步回調(diào)函數(shù)中調(diào)用EndInvoke,可以直接返回,不再阻塞。
- 異步回調(diào)方式,前端UI線程不再卡住。?
以上就是C# 多線程學(xué)習(xí)之基礎(chǔ)入門的詳細(xì)內(nèi)容,更多關(guān)于C# 多線程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#進(jìn)行圖像處理的常見(jiàn)方法(Bitmap,BitmapData,IntPtr)使用詳解
這篇文章主要為大家詳細(xì)介紹了C#進(jìn)行圖像處理的幾個(gè)常見(jiàn)方法(Bitmap,BitmapData,IntPtr)具體使用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2024-01-01C#實(shí)現(xiàn)判斷一個(gè)時(shí)間點(diǎn)是否位于給定時(shí)間區(qū)間的方法
這篇文章主要介紹了C#實(shí)現(xiàn)判斷一個(gè)時(shí)間點(diǎn)是否位于給定時(shí)間區(qū)間的方法,涉及C#針對(duì)時(shí)間的轉(zhuǎn)換與判定相關(guān)技巧,需要的朋友可以參考下2015-08-08C#實(shí)現(xiàn)類似新浪微博長(zhǎng)URL轉(zhuǎn)短地址的方法
這篇文章主要介紹了C#實(shí)現(xiàn)類似新浪微博長(zhǎng)URL轉(zhuǎn)短地址的方法,涉及C#操作正則表達(dá)式的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04C#使用自定義的泛型節(jié)點(diǎn)類實(shí)現(xiàn)二叉樹(shù)類
這篇文章主要為大家詳細(xì)介紹了C#如何使用自定義的泛型節(jié)點(diǎn)類 Node<T>實(shí)現(xiàn)二叉樹(shù)類BinaryTree<T>及其方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03C# 使用Fiddler捕獲本地HttpClient發(fā)出的請(qǐng)求操作
這篇文章主要介紹了C# 使用Fiddler捕獲本地HttpClient發(fā)出的請(qǐng)求操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10詳解C#中的接口屬性以及屬性訪問(wèn)器的訪問(wèn)限制
這篇文章主要介紹了詳解C#中的接口屬性以及屬性訪問(wèn)器的訪問(wèn)限制,文中講到了使用訪問(wèn)器實(shí)現(xiàn)接口時(shí)添加訪問(wèn)修飾符的問(wèn)題,需要的朋友可以參考下2016-01-01C#實(shí)現(xiàn)窗體間傳遞數(shù)據(jù)實(shí)例
這篇文章主要介紹了C#實(shí)現(xiàn)窗體間傳遞數(shù)據(jù)實(shí)例,需要的朋友可以參考下2014-07-07