剛接觸線程的時候,感覺這個東西好神奇。雖然不是很明白,就感覺它很牛逼。 參考了一些大佬寫的文章: https://www.cnblogs.com/yilezhu/p/10555849.html這個大佬寫的文章,我還是很喜歡的 https://www.cnblogs.com/mushroom/p/45 ...
剛接觸線程的時候,感覺這個東西好神奇。雖然不是很明白,就感覺它很牛逼。
參考了一些大佬寫的文章:
https://www.cnblogs.com/yilezhu/p/10555849.html這個大佬寫的文章,我還是很喜歡的
https://www.cnblogs.com/mushroom/p/4575417.html
多線程是.NET開發非常重要的一塊,很多開發者對多線程幾乎不用/很畏懼/不明所以,寫代碼的時候,沒有考慮到多線程的場景。
什麼是進程?
電腦概念,程式在伺服器運行占據全部電腦資源的綜合,是一種虛擬的概念。
當一個程式開始運行時,它就是一個進程,進程包括運行中的程式和程式所使用到的記憶體和系統資源。
而一個進程又是由多個線程所組成的。
什麼是線程?
電腦概念,進程在響應操作時最小單位,也包括CPU、記憶體、網路、硬碟IO。
線程是程式中的一個執行流,每個線程都有自己的專有寄存器(棧指針、程式計數器等),但代碼區是共用的,即不同的線程可以執行同樣的函數。
什麼是多線程?
電腦概念,一個進程有多個線程同時運行。
多線程是指程式中包含多個執行流,即在一個程式中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程式創建多個並行執行的線程來完成各自的任務。
一個進程會包含很多個線程;線程是隸屬於某個進程,進程毀了線程也就沒了。
句柄:其實就是個long數字,是操作系統表示應用程式。
C#裡面的多線程?
Thread類,是C#語言對線程對象的一個封裝。
為什麼可以多線程?
1、多個CPU的核可以並行工作,多個模擬線程
四核八線程,這裡面的線程值的是模擬核
2、CPU的分片,1S的處理能力分成1000份,操作系統調度著去響應不同的任務。從巨集觀角度來說,感覺就是多個任務在併發執行;從微觀角度來說,一個物理CPU同一時刻,只能為一個任務服務。
同步方法:
發起調用,完成後才繼續下一行;非常符合開發思維,有序執行。
簡單來說,就是誠心誠意請人吃飯,比如邀請bingle吃飯,但是bingle要忙一會,那就等著bingle完成後再一起去吃飯。
非同步方法:
發起調用,不等待完成,直接進入下一行,啟動一個新線程開完成方法的計算。
簡單來說,就是客氣一下的請人吃飯,比如要邀請bingle吃飯,但是bingle要忙一會,那你就忙著吧,我先去吃飯了,你忙完了自己去吃飯吧。
同步方法的代碼:
private void btnSync_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); int l = 3; int m = 4; int n = l + m; for (int i = 0; i < 5; i++) { string name = string.Format($"btnSync_Click_{i}"); this.DoSomethingLong(name); } Console.WriteLine($"****************btnSync_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); } /// <summary> /// 一個比較耗時耗資源的私有方法 /// </summary> /// <param name="name"></param> private void DoSomethingLong(string name) { Console.WriteLine($"****************DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1_000_000_000; i++) { lResult += i; } //Thread.Sleep(2000); Console.WriteLine($"****************DoSomethingLong End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); }View Code
調用後,是這個樣子的結果;
在這段期間內,界面是卡死的,無法拖動。
非同步方法的代碼:
private void btnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; //action.Invoke("btnAsync_Click_1"); //action("btnAsync_Click_1"); //委托自身需要的參數+2個非同步參數 //action.BeginInvoke("btnAsync_Click_1", null, null); for (int i = 0; i < 5; i++) { string name = string.Format($"btnAsync_Click_{i}"); action.BeginInvoke(name, null, null); } Console.WriteLine($"****************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }View Code
調用之後的結果是這個樣子的:
期間,界面不是卡死的,可以隨意拖動。只是界面依然是主線程執行,在裡面開啟了子線程去執行其他的方法。
同步方法與非同步方法的區別:
同步方法:
主線程(UI線程),忙著計算,無暇他顧,界面是卡死的。
非同步方法:
主線程閑置,計算任務交給子線程完成,改善用戶體驗,winform點幾個按鈕,不至於卡死;web開發,也是一樣需要的,發個簡訊通知,或者下載個Excel,都交給非同步線程去做。
同步方法比較慢,因為只有一個線程計算,非同步方法快,因為有多個線程併發計算。多線程其實就是用資源換性能。
什麼時候用多線程?
1、一個訂單表很耗時間,能不能用多線程去優化下性能呢?
答案是不能的,因為這就是一個操作,沒法並行。
2、需要查詢資料庫/調用介面/讀硬碟文件/做數據計算,能不能用多線程優化下性能?
這個是可以的。因為多個任務可以並行的,但是多線程並不是越多越好,因為資源有限,而且調度有損耗,多線程儘量避免使用。
我們來看下,上面調用後的執行順序:
同步方法有序進行,但是非同步方法啟動無序。因為線程資源是向操作系統申請的,由操作系統的調度決策決定,所以啟動是無序的。同一個任務用一個線程,執行時間也是不確定的,是CPU分片導致的。
使用多線程請一定小心,很多事不是想當然的,尤其是多線程操作時間有序要求的時候(async await可以解決這個問題)。那能不能通過延遲一點啟動來控制順序?或者預測下結束順序?這些都是不靠譜的。就算通過大量的測試,得到的執行順序和預期的順序總是相同的,但是只要有概率是不同的,總會發生這種情況。
並行:多核之間叫並行。
併發:CPU分片的併發。
回調:將後續動作通過回調參數傳遞進去,子線程完成計算後,去調用這個回調委托。
代碼:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; AsyncCallback callback = ar => { Console.WriteLine($"btnAsyncAdvanced_Click計算成功了。。。。ThreadId is{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }; action.BeginInvoke("btnAsyncAdvanced_Click", callback, null); }View Code
執行結果:
回調傳參:
代碼:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; //1 回調:將後續動作通過回調參數傳遞進去,子線程完成計算後,去調用這個回調委托 IAsyncResult asyncResult = null;//是對非同步調用操作的描述 AsyncCallback callback = ar => { Console.WriteLine($"{object.ReferenceEquals(ar, asyncResult)}"); Console.WriteLine($"btnAsyncAdvanced_Click計算成功了。。。。{ar.AsyncState}。{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }; asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "bingle");View Code
看下結果,bingle這個參數傳遞過來了
通過IsComplate等待,卡界面--主線程在等待,邊等待邊提示
////2 通過IsComplate等待,卡界面--主線程在等待,邊等待邊提示 ////( Thread.Sleep(200);位置變了,少了一句99.9999) int i = 0; while (!asyncResult.IsCompleted) { if (i < 9) { Console.WriteLine($"bingle{++i * 10}%...."); } else { Console.WriteLine($"bingle99.999999%...."); } Thread.Sleep(200); } Console.WriteLine("已經完成!");
WaitOne等待,即時等待 限時等待
asyncResult.AsyncWaitHandle.WaitOne();//直接等待任務完成 asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待任務完成 asyncResult.AsyncWaitHandle.WaitOne(1000);//最多等待1000ms,超時就不等了
//4 EndInvoke 即時等待, 而且可以獲取委托的返回值 一個非同步操作只能End一次 action.EndInvoke(asyncResult);//等待某次非同步調用操作結束
Thread類
上面介紹過,Thread是C#對線程對象的一個封裝。
Thread:C#對線程對象的一個封裝
Thread方法很多很強大,但是也太過強大,而且沒有限制
ParameterizedThreadStart method = o => this.DoSomethingLong("btnThread_Click"); Thread thread = new Thread(method); thread.Start("123");//開啟線程,執行委托的內容
下麵這些,是Obselte的api //thread.Suspend();//暫停 //thread.Resume();//恢復 真的不該要的,暫停不一定馬上暫停;讓線程操作太複雜了 //thread.Abort(); ////線程是電腦資源,程式想停下線程,只能向操作系統通知(線程拋異常), ////會有延時/不一定能真的停下來
線程等待,有以下寫法:
while (thread.ThreadState != ThreadState.Stopped) { Thread.Sleep(200);//當前線程休息200ms }
//2 Join等待 thread.Join();//運行這句代碼的線程,等待thread的完成 thread.Join(1000);//最多等待1000ms
thread.Priority = ThreadPriority.Highest;最高優先順序,有限執行,但不代表優先完成。是指說在極端情況下,還有意外發生,不能通過這個來控制線程的執行先後順序。
thread.IsBackground = false;//預設是false 前臺線程,進程關閉,線程需要計算完後才退出 //thread.IsBackground = true;//關閉進程,線程退出
基於Thread可以封裝一個回調,回調:啟動子線程去執行動作A----不阻塞---A執行完成後子線程會執行動作B
代碼:
private void ThreadWithCallBack(ThreadStart threadStart, Action actionCallback) { //Thread thread = new Thread(threadStart); //thread.Start(); //thread.Join();//錯了,因為方法被阻塞了 //actionCallback.Invoke(); //上面那種方式錯了, 應該先用threadStart,再調用callback ThreadStart method = new ThreadStart(() => { threadStart.Invoke(); actionCallback.Invoke(); }); new Thread(method).Start(); }
調用測試一下:
ThreadStart threadStart = () => this.DoSomethingLong("btnThread_Click"); Action actionCallBack = () => { Thread.Sleep(2000); Console.WriteLine($"This is Calllback {Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }; this.ThreadWithCallBack(threadStart, actionCallBack);
基於Thread封裝一個帶返回值的方法:
private Func<T> ThreadWithReturn<T>(Func<T> func) { T t = default(T); ThreadStart threadStart = new ThreadStart(() => { t = func.Invoke(); }); Thread thread = new Thread(threadStart); thread.Start(); return new Func<T>(() => { thread.Join(); //thread.ThreadState return t; }); }
調用:
Func<int> func = () => { Thread.Sleep(5000); return DateTime.Now.Year; }; Func<int> funcThread = this.ThreadWithReturn(func);//非阻塞 Console.WriteLine("do something else/////"); Console.WriteLine("do something else/////"); Console.WriteLine("do something else/////"); Console.WriteLine("do something else/////"); Console.WriteLine("do something else/////"); int iResult = funcThread.Invoke();//阻塞 Console.WriteLine(iResult);
在調用的時候funcThread.Invoke(),這裡發生了阻塞。既要不阻塞,又要計算結果?不可能!
線程池:
Thread,功能繁多,反而不好,就好像給4歲小孩一把熱武器,反而會造成更大的傷害,對線程數量時沒有管控的。
在.NET Framework2.0,出現了線程池。如果某個對象創建和銷毀代價比較高,同時這個對象還可以反覆使用,就需要一個池子。保存多個這樣的對象,需要用的時候從池子裡面獲取,用完之後不用銷毀,放回池子(享元模式)。這樣可以節約資源提升性能;此外,還能管控總數量,防止濫用。ThreadPool的線程都是後臺線程。
ThreadPool最簡單的使用:
ThreadPool.QueueUserWorkItem(o => this.DoSomethingLong("btnThreadPool_Click1")); ThreadPool.QueueUserWorkItem(o => this.DoSomethingLong("btnThreadPool_Click2"), "bingle");
//等待 ManualResetEvent mre = new ManualResetEvent(false); //false---關閉---Set打開---true---WaitOne就能通過 //true---打開--ReSet關閉---false--WaitOne就只能等待 ThreadPool.QueueUserWorkItem(o => { this.DoSomethingLong("btnThreadPool_Click1"); mre.Set(); }); Console.WriteLine("Do Something else..."); Console.WriteLine("Do Something else..."); Console.WriteLine("Do Something else..."); mre.WaitOne(); Console.WriteLine("任務已經完成了。。。");
執行結果:
不要阻塞線程池裡面的線程:
ThreadPool.SetMaxThreads(8, 8); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 10; i++) { int k = i; ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString("00")} show {k}"); if (k == 9) { mre.Set(); } else { mre.WaitOne(); } }); } if (mre.WaitOne()) { Console.WriteLine("任務全部執行成功!"); }
程式卡在這裡了,因為,線程池裡面就只有八個線程,現在有8個線程都在等,這就形成了死鎖,程式就卡在這。所以不要阻塞線程池裡面的線程。
篇幅有點多,下麵一篇筆記介紹.NET Framework3.0出來的Task,以及async和await