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