上篇 net 同步非同步 中篇 多線程的使用(Thread) 下篇 net 任務工廠實現非同步多線程 Thread多線程概述 Thread多線程概述 上一篇我們介紹了net 的同步與非同步,我們非同步演示的時候使用的是委托多線程來實現的。今天我們來細細的剖析下 多線程。 多線程的優點:可以同時完成多個任務; ...
上篇 net 同步非同步
中篇 多線程的使用(Thread)
下篇 net 任務工廠實現非同步多線程
Thread多線程概述
上一篇我們介紹了net 的同步與非同步,我們非同步演示的時候使用的是委托多線程來實現的。今天我們來細細的剖析下 多線程。
多線程的優點:可以同時完成多個任務;可以使程式的響應速度更快;可以讓占用大量處理時間的任務或當前沒有進行處理的任務定期將處理時間讓給別的任務;可以隨時停止任務;可以設置每個任務的優先順序以優化程式性能。
然而,多線程雖然有很多優點,但是也必須認識到多線程可能存在影響系統性能的不利方面,才能正確使用線程。弊端主要有如下幾點:
(1)線程也是程式,所以線程需要占用記憶體,線程越多,占用記憶體也越多。
(2)多線程需要協調和管理,所以需要占用CPU時間以便跟蹤線程[時間空間轉換,簡稱時空轉換]。
(3)線程之間對共用資源的訪問會相互影響,必須解決爭用共用資源的問題。
(4)線程太多會導致控制太複雜,最終可能造成很多程式缺陷。
當啟動一個可執行程式時,將創建一個主線程。在預設的情況下,C#程式具有一個線程,此線程執行程式中以Main方法開始和結束的代碼,Main()方法直接或間接執行的每一個命令都有預設線程(主線程)執行,當Main()方法返回時此線程也將終止。
一個進程可以創建一個或多個線程以執行與該進程關聯的部分程式代碼。在C#中,線程是使用Thread類處理的,該類在System.Threading命名空間中。使用Thread類創建線程時,只需要提供線程入口,線程入口告訴程式讓這個線程做什麼。通過實例化一個Thread類的對象就可以創建一個線程。
多線程Thread
上一節我們通過了線程的非同步瞭解了線程的等待和線程回調的區別、學習了不卡主線程的原理。這裡我們簡單學習下 Thread 類的使用.我們分別讓主線程和子線程迴圈輸出 1-100,我們來看下結果。
Thread類接收一個ThreadStart委托或ParameterizedThreadStart委托的構造函數,該委托包裝了調用Start方法時由新線程調用的方法,示例代碼如下:
Thread thread=new Thread(new ThreadStart(method));//創建線程
thread.Start(); //啟動線程
1.線程的無序性質
Thread t = new Thread(()=> { for (int i = 0; i < 100; i++) { Console.WriteLine($"我是子線程({i})~~~~"); } });// 參數 ThreadStart 是一個委托類型的。凡是委托,我們都可以使用lambda 表達式代替 t.Start();//Start 通過Start開啟一個線程 for (int i = 0; i < 100; i++) { Console.WriteLine($"我是主線程【{i}】"); }
輸出結果:
通過執行結果我們會看到,主線程和子線程不是一味的執行,是兼續的。也就是說主線程和子線程在執行過程中是互相搶CPU資源進行計算的。
這裡我們可以總結下, 非同步多線程三大特點:
(1) 同步卡界面,UI線程被占用;非同步多線程不卡界面,UI線程空閑,計算任務交給子線程。
(2) 同步方法慢,因為只有一個線程幹活;非同步多線程方法快,因為多個線程併發計算(空間換時間), 這裡也會消耗更多的資源,不是線程的線性關係(倍數關係),不是線程越多越好(1資源有限 2線程調度耗資源 3不穩定)
(3) ※非同步多線程是無序的,不可預測的:啟動順序不確定、消耗時間不確定、結束順序不確定( 這裡特別提醒下,我們不要試圖控制執行的順序)
非同步與多線程的區別: 非同步是主線程立即啟動,啟動完成以後,在執行子線程。但是多線程不一定是誰先啟動。如果看不懂,請看本人上一篇文章。
關於線程的無續,這裡就不過多的解釋了繼續往下看。
2.前臺線程與後臺線程
在上一篇文章我們簡單的說到了,線程本身並不是任何高級語言的概念,本身是電腦的概念,只是高級語言給予封裝了一層。
前臺線程:窗體Ui主線程退出(銷毀)以後,子線程必須計算完成才能退出。請下載demo 自行查看,還是上面的案列。
後臺線程:窗體Ui主線程退出(銷毀)以後,子線程就會退出。關閉窗體以後,控制台就退出了。我們通過Thread 類的 IsBackground屬性進行設置;設置為true 的時候為後臺線程,預設為false前臺線程。看下邊案列
private void button1_Click(object sender, EventArgs e) { Thread t = new Thread(()=> { for (int i = 0; i < 100; i++) { Thread.Sleep(100);//演示前臺線程和後臺線程,演示線程無序性質,請註釋掉 Console.WriteLine($"我是子線程({i})~~~~"); } });// 參數 ThreadStart 是一個委托類型的。凡是委托,我們都可以使用lambda 表達式代替 t.IsBackground=true;//設置為true 的時候為後臺線程,預設為false前臺線程。關閉窗體會立即停止計算。 t.Start();//Start 通過Start開啟一個線程 for (int i = 0; i < 100; i++) { Console.WriteLine($"我是主線程【{i}】"); } }後臺線程
後臺線程一般用於處理不重要的事情,應用程式結束時,後臺線程是否執行完成對整個應用程式沒有影響。如果要執行的事情很重要,需要將線程設置為前臺線程。
3.線程的狀態及屬性方法的使用
這裡簡單列下線程的常用屬性
屬性名稱 | 說明 |
---|---|
CurrentContext | 獲取線程正在其中執行的當前上下文。 |
CurrentThread | 獲取當前正在運行的線程。 |
ExecutionContext | 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各種上下文的信息。 |
IsAlive | 獲取一個值,該值指示當前線程的執行狀態。 |
IsBackground | 獲取或設置一個值,該值指示某個線程是否為後臺線程。 |
IsThreadPoolThread | 獲取一個值,該值指示線程是否屬於托管線程池。 |
ManagedThreadId | 獲取當前托管線程的唯一標識符。 |
Name | 獲取或設置線程的名稱。 |
Priority | 獲取或設置一個值,該值指示線程的調度優先順序。 |
ThreadState | 獲取一個值,該值包含當前線程的狀態。 |
通過ThreadState可以檢測線程是處於Unstarted、Sleeping、Running 等等狀態,它比 IsAlive 屬性能提供更多的特定信息。
前面說過,一個應用程式域中可能包括多個上下文,而通過CurrentContext可以獲取線程當前的上下文。
CurrentThread是最常用的一個屬性,它是用於獲取當前運行的線程。
Thread.CurrentThread.ManagedThreadId 獲取線程的ID ,我們儘量不要使用 Thread.CurrentThread.Name,因為name 並不是唯一的
線程狀態值Thread 中包括了多個方法來控制線程的創建、掛起、停止、銷毀,以後來的例子中會經常使用。
方法名稱 | 說明 |
---|---|
Abort() | 終止本線程。 |
GetDomain() | 返回當前線程正在其中運行的當前域。 |
GetDomainId() | 返回當前線程正在其中運行的當前域Id。 |
Interrupt() | 中斷處於 WaitSleepJoin 線程狀態的線程。 |
Join() | 已重載。 阻塞調用線程,直到某個線程終止時為止。 |
Resume() | 繼續運行已掛起的線程。 |
Start() | 執行本線程。 |
Suspend() | 掛起當前線程,如果當前線程已屬於掛起狀態則此不起作用 |
Sleep() | 把正在運行的線程掛起一段時間。 |
關於線程狀態操作的方法,我們儘量不要使用,這裡以銷毀線程的方法為列子去演示。本圖表除了第一個以外,都可以使用。
註意:
在我們net 中,語言分為托管代碼和非托管代碼。托管代碼是可以控制的,非托管代碼是不可控制的.我們線程銷毀實際上是拋出了一個異常,當我們程式捕獲到這個異常以後,線程因為異常而退出。所以銷毀線程在某些(值非托管調用)時候會出現問題的,我們後續在任務工廠 FacTask 的時候,再去詳細講解銷毀線程。如果非要"停止線程 靠的不是外部力量而是線程自身,外部修改信號量,線程檢測信號量,當線程檢測到信號量以後,我們拋出異常,在主線程中捕獲異常即可".
4.線程等待
我們在學習非同步的時候,知道UI主線程在等待子線程的時候,窗體是不可以移動的。並且我們可以有實時返回(有損耗)的等待(阻塞)和一直阻塞主線程(無損耗)等待子線程完事,再去執行主線程。下麵我們來看看一個案列,案列以5名學生寫作業為例子。
public void WriteJob(string name) { Console.WriteLine("********************** "+ name + " Start【" + Thread.CurrentThread.ManagedThreadId + "】等待............... ***"); Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 10; i++) { Thread.Sleep(32); Console.WriteLine($"學生:{name}在做第{i+1}題作業"); } watch.Stop(); Console.WriteLine("********************** " + name + " End【" + Thread.CurrentThread.ManagedThreadId + "】 用時"+ watch.ElapsedMilliseconds + "毫秒............... ***"); }
/// <summary> /// 線程等待 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, EventArgs e) { Console.WriteLine("**********************button2_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); List<Thread> threadList = new List<Thread>(); for (int i = 0; i < 5; i++) { string studentName = "甲" + i + "同學"; Thread thread = new Thread(() => WriteJob(studentName)); Console.WriteLine($"{studentName}開始寫作業"); thread.Start(); threadList.Add(thread); } //無損耗阻塞主線程 //foreach (var thread in threadList) //{ // thread.Join();//表示把thread線程任務join到當前線程,也就是當前線程等著thread任務完成 //} //帶返回,有損耗阻塞主線程 while (threadList.Count(t => t.ThreadState != System.Threading.ThreadState.Stopped) > 0) { Thread.Sleep(100); Console.WriteLine("請等待...."); } Console.WriteLine("**********************button2_Click end【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); }
看了上面的代碼,我們都發現無損耗阻塞使用的是 thread.Join(); ,有損耗阻塞,我們是需要自己去寫計算邏輯的。
5.線程回調
在多線程中是不存在回調的,我們只能通過自己去包裝,實現線程的回調。在包裝之前,我們先回顧下非同步回調的特點:
#region 非同步回調回顧 { //回調 AsyncCallback callBack = param => { Console.WriteLine("當前狀態:"+param.AsyncState); Console.WriteLine("你睡吧,我去吃好吃的去了"); }; Func<string, int> func = t => { Console.WriteLine(t); Thread.Sleep(5000); Console.WriteLine("等等吧,我在睡一會"); return DateTime.Now.Millisecond;//返回當前毫秒數 }; ///第一個參數是我們自定義的參數,第二個參數是我們回調的參數,第三個參數是狀態參數 IAsyncResult iAsyncResult= func.BeginInvoke("張四火,起床吧", callBack, "runState"); int second= func.EndInvoke(iAsyncResult); Console.WriteLine("當前毫秒數:"+second); } { //回調 AsyncCallback callBack = param => { Console.WriteLine("當前狀態:" + param.AsyncState); Console.WriteLine("媽媽說:孩子太小,睡會吧"); }; // 非同步 Action<string> act = t => { Console.WriteLine(t); Thread.Sleep(5000); Console.WriteLine("等等吧,我在睡一會"); }; IAsyncResult iAsyncResult = act.BeginInvoke("梓燁,起床吧", callBack, "runState"); act.EndInvoke(iAsyncResult); } #endregion
非同步回調:是分為有返回和無返回兩種,返回值類型取決於我們委托的返回值類型,回調就是在子線程執行完成以後,執行一個代碼塊。多線程下如何使用回調呢?在Thread下是沒有回調的。我們可以根據非同步回調的特點自己封裝一個線程回調或非同步返回值。
#region 回調封裝 /// <summary> /// 回調封裝 無返回值 /// </summary> /// <param name="start"></param> /// <param name="callback">回調</param> private void ThreadWithCallback(ThreadStart start, Action callback) { Thread thread = new Thread(() => { start.Invoke(); callback.Invoke(); }); thread.Start(); } /// <summary> /// 有返回值封裝(請根據本案例自行封裝回調) /// </summary> /// <typeparam name="T">返回值類型</typeparam> /// <param name="func">需要子線程執行的方法</param> /// <returns></returns> private Func<T> ThreadWithReturn<T>(Func<T> func) { T t = default(T);//初始化一個泛型 ThreadStart newStart = () => { t = func.Invoke(); }; Thread thread = new Thread(newStart); thread.Start(); return new Func<T>(() => { thread.Join(); return t; }); } #endregion
代碼調用
#region 多線程回調封裝調用 { //無返回 ThreadWithCallback(()=>{ Console.WriteLine("梓燁,起床吧"); Thread.Sleep(5000); Console.WriteLine("等等吧,我在睡一會"); },()=> { Console.WriteLine("媽媽說:梓燁太小,睡會吧"); }); } { //有返回 int secound= ThreadWithReturn<int>(()=>{ Console.WriteLine("張四火,起床吧"); Thread.Sleep(5000); Console.WriteLine("等等吧,我在睡一會"); return DateTime.Now.Millisecond;//返回當前毫秒數 }).Invoke(); Console.WriteLine(secound); } #endregion
以上都是 C#1.0時代的多線程,這些現在基本已經沒有人在使用了。註意:本文並沒有介紹信號量,在沒有介紹信號量退出線程的時候,我們還是使用net 自帶的終止線程。下邊擴展點技術:
6.線程同步
所謂同步:是指在某一時刻只有一個線程可以訪問變數。
如果不能確保對變數的訪問是同步的,就會產生錯誤。
c#為同步訪問變數提供了一個非常簡單的方式,即使用c#語言的關鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個時刻內只允許一個線程進入執行,而其他線程必須等待。在c#中,關鍵字Lock定義如下:
Lock(expression)
{
statement_block
}
expression代表你希望跟蹤的對象:
如果你想保護一個類的實例,一般地,你可以使用this;
如果你想保護一個靜態變數(如互斥代碼段在一個靜態方法內部),一般使用類名就可以了
而statement_block就算互斥段的代碼,這段代碼在一個時刻內只可能被一個線程執行。
以書店賣書為例
static void Main(string[] args) { BookShop book = new BookShop(); //創建兩個線程同時訪問Sale方法 Thread t1 = new Thread(new ThreadStart(book.Sale));//因為使用的同一個引用,所以書店庫存量始終是一個地址的引用 Thread t2 = new Thread(new ThreadStart(book.Sale)); //啟動線程 t1.Start(); t2.Start(); Console.ReadKey(); } class BookShop { public int num = 1;//庫存量 public void Sale() { int tmp = num; if (tmp > 0)//判斷是否有書,如果有就可以賣 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本圖書,還剩餘{0}本", num); } else { Console.WriteLine("沒有了"); } } }
從運行結果可以看出,兩個線程同步訪問共用資源,沒有考慮同步的問題,結果不正確(結果出現“-1”)。
如何做到線程的同步呢?我們需要使用線程鎖 lock
class BookShop { public int num = 1;//庫存量 public void Sale() { // 使用lock關鍵字解決線程同步問題。鎖住當前對象 lock (this) { int tmp = num; if (tmp > 0)//判斷是否有書,如果有就可以賣 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本圖書,還剩餘{0}本", num); } else { Console.WriteLine("沒有了"); } } } }
7.跨線程訪問
在很多實際應用中,子線程的計算百分比要時刻返回給主線程,列入進度條。我們以winform 的 textbox 輸出1-100為列。
產生錯誤的原因:textBox1是由主線程創建的,thread線程是另外創建的一個線程,在.NET上執行的是托管代碼,C#強制要求這些代碼必須是線程安全的,即不允許跨線程訪問Windows窗體的控制項。
解決方案:
1、在窗體的載入事件中,將C#內置控制項(Control)類的CheckForIllegalCrossThreadCalls屬性設置為false,屏蔽掉C#編譯器對跨線程調用的檢查。如下:
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
使用上述的方法雖然可以保證程式正常運行並實現應用的功能,但是在實際的軟體開發中,做如此設置是不安全的(不符合.NET的安全規範),在產品軟體的開發中,此類情況是不允許的。如果要在遵守.NET安全標準的前提下,實現從一個線程成功地訪問另一個線程創建的空間,要使用C#的方法回調機制。
2、使用回調函數
回調前邊有講解,這裡直接上代碼,註意,這裡的回調,使用的是控制項自帶的回調哦。
private void button1_Click(object sender, EventArgs e) { Action<int> act = t=> { this.textBox1.Text = t.ToString(); }; //創建一個線程去執行這個方法:創建的線程預設是前臺線程 Thread thread = new Thread(()=> { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.textBox1.Invoke(act, i); // this.textBox1.Invoke(t => { this.textBox1.Text = t.ToString(); }, i);這裡不允許使用 lambda 表達式,因為裡面傳遞的是一個委托類型,不是委托 } }); //Start方法標記這個線程就緒了,可以隨時被執行,具體什麼時候執行這個線程,由CPU決定 //將線程設置為後臺線程 thread.IsBackground = true; thread.Start(); }
經過這兩個擴展技術點,我們是不是就更牢固了我們的知識點。
線程池ThreadPool
1.線程池概述
線程池是C# 2.0以後才有的。確切的說是Net2.0以後,什麼是線程池呢??先看圖,然後在慢慢的聊。
.NET Framework的ThreadPool類提供一個線程池,該線程池可用於執行任務、發送工作項、處理非同步 I/O、代表其他線程等待以及處理計時器。那麼什麼是線程池?線程池其實就是一個存放線程對象的“池子(pool)”,他提供了一些基本方法,如:設置pool中最小/最大線程數量、把要執行的方法排入隊列等等。ThreadPool是一個靜態類,因此可以直接使用,不用創建對象。
微軟官網說法如下:許多應用程式創建大量處於睡眠狀態,等待事件發生的線程。還有許多線程可能會進入休眠狀態,這些線程只是為了定期喚醒以輪詢更改或更新的狀態信息。 線程池,使您可以通過由系統管理的工作線程池來更有效地使用線程。
說得簡單一點,每新建一個線程都需要占用記憶體空間和其他資源,而新建了那麼多線程,有很多在休眠,或者在等待資源釋放;又有許多線程只是周期性的做一些小工作,如刷新數據等等,太浪費了,划不來,實際編程中大量線程突發,然後在短時間內結束的情況很少見。於是,就提出了線程池的概念。線程池中的線程執行完指定的方法後並不會自動消除,而是以掛起狀態返回線程池,如果應用程式再次向線程池發出請求,那麼處以掛起狀態的線程就會被激活並執行任務,而不會創建新線程,這就節約了很多開銷。只有當線程數達到最大線程數量,系統才會自動銷毀線程。因此,使用線程池可以避免大量的創建和銷毀的開支,具有更好的性能和穩定性,其次,開發人員把線程交給系統管理,可以集中精力處理其他任務。
2.線程池初始化
線程池位於 ThreadPool中,該類也是在 System.Threading 命名空間下,線程預設初始化大小是有CPU和操作系統決定的。 其中線程數最小不能小於CPU的核數。在我們使用ThreadPool類的時候。
int workerThreads = 0; int ioThreads = 0; ThreadPool.GetMaxThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("可創建最大線程數: {0}; 最大 I/O 線程: {1}", workerThreads, ioThreads)); ThreadPool.GetMinThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("最小線程數: {0}; 最小 I/O 線程: {1}", workerThreads, ioThreads)); ThreadPool.GetAvailableThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("可以使用的工作線程: {0}; 可用 I/O 線程: {1}", workerThreads, ioThreads));
執行上面的代碼可以得帶結果如下:
可創建最大線程數: 2047; 最大 I/O 線程: 1000
最小線程數: 4; 最小 I/O 線程: 4
可以使用的工作線程: 2047; 可用 I/O 線程: 1000
3.設置線程池初始化大小
我們為什麼要設置線程池的大小呢?其實我們電腦並不是只運行你一個軟體,你的給其他軟體預留線程池,一般我們開發的時候使用的線程數是使用 8條、16條、32條和64條。不建議大於64條,畢竟老式的電腦支持有限,如果你電腦線程有2000多,你完全可以分配256以下的線程數。下麵我們來看下如何設置。
#region 設置初始化線程 ThreadPool.SetMaxThreads(8, 8);//最小也是CPU核數 ThreadPool.SetMinThreads(4, 4); #endregion #region 獲取線程池當前設置 ,預設設置取決於操作系統和CPU int workerThreads = 0; int ioThreads = 0; ThreadPool.GetMaxThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("可創建最大線程數: {0}; 最大 I/O 線程: {1}", workerThreads, ioThreads)); ThreadPool.GetMinThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("最小線程數: {0}; 最小 I/O 線程: {1}", workerThreads, ioThreads)); ThreadPool.GetAvailableThreads(out workerThreads, out ioThreads); Console.WriteLine(String.Format("可以使用的工作線程: {0}; 可用 I/O 線程: {1}", workerThreads, ioThreads)); #endregion
執行結果如下:
可創建最大線程數: 8; 最大 I/O 線程: 8
最小線程數: 4; 最小 I/O 線程: 4
可以使用的工作線程: 8; 可用 I/O 線程: 8
4.線程池的使用
線程池可以理解為,我們預先創建好指定數量的線程,給程式使用,當前正在使用的子線程一旦使用完成以後,要立即歸還,下一個任務使用的時候,我在分配給這個任務。我們使用ManualResetEvent類來控制線程的使用與歸還。具體看代碼。
#region 多線程的使用 Console.WriteLine("當前主線程id:{0}", Thread.CurrentThread.ManagedThreadId); //首先創建5個任務線程 ManualResetEvent[] mre = new ManualResetEvent[] { new ManualResetEvent(false), new ManualResetEvent(false), new ManualResetEvent(false), new ManualResetEvent(false), new ManualResetEvent(false) }; for (int i = 0; i < 5; i++) { ///false 預設是關閉的,TRUE 預設為打開的 Thread.Sleep(300); ThreadPool.QueueUserWorkItem(t => { //lambda任務 Console.WriteLine("參數的內容是"+t); Console.WriteLine("獲取參數值((dynamic)t).num:" + ((dynamic)t).num); int num = ((dynamic)t).num; for (int j = 0; j < num; j++) { Thread.Sleep(2);//一件很耗時的事情 } Console.WriteLine("當前子線程id:{0} 的狀態:{1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.ThreadState); //這裡是釋放共用鎖,讓其他線程進入 mre[i].Set();//可以理解為打開一個線程 //這裡是打開共用鎖,讓其他線程進入 // mre[i].Reset();//可以理解為關閉一個線程 Console.WriteLine(); }, new { num = (i + 1) * 10 }); mre[i].WaitOne(3000); //單獨阻塞線程,因為我們使用的是池,索引這裡不使用這個,用法同委托非同步 } //註意這裡,設定WaitAll是為了阻塞調用線程(主線程),讓其餘線程先執行完畢, //其中每個任務完成後調用其set()方法(收到信號),當所有 //的任務都收到信號後,執行完畢,將控制權再次交回調用線程(這裡的主線程) // ManualResetEvent.WaitAll(mre);不建議這麼使用 Console.ReadKey(); #endregion
執行結果:
當前主線程id:1
參數的內容是{ num = 10 }
獲取參數值((dynamic)t).num:10
當前子線程id:3 的狀態:Background
參數的內容是{ num = 20 }
獲取參數值((dynamic)t).num:20
當前子線程id:3 的狀態:Background
參數的內容是{ num = 30 }
獲取參數值((dynamic)t).num:30
當前子線程id:4 的狀態:Background
參數的內容是{ num = 40 }
獲取參數值((dynamic)t).num:40
當前子線程id:3 的狀態:Background
參數的內容是{ num = 50 }
獲取參數值((dynamic)t).num:50
當前子線程id:3 的狀態:Background
通過執行結果可以看出,第一次任務執行完成以後,把線程歸還了,第二次任務分配的還是當前線程,但是第三次任務執行不過來了,我們從小分配了 一次線程。請根據結果分析。關於回調,線程等待使用方法同非同步,救贖前一篇有介紹。
總結
1.本文主要演示了多線程和線程池的使用。誇線程訪問(主要用於做進度條)。
2.本文介紹了使用多線程的利與弊及線程池的利與弊。