進程與線程
概念
1.CPU的線程概念和程序的進程線程概念不同。這里我們只將程序的概念。程序中一次只能執行一個進程,一個進程至少包含一個線程(windows系統中是這樣)。具體可以查看簡書:https://www.jianshu.com/p/af6dcc255dbe中大佬的講解
2.如果有一塊內存空間很特殊,要求每次只能有一個線程進行讀寫操作,那么可以使用“互斥鎖”(Mutual exclusion,縮寫Mutex),防止多個線程同時讀寫某一塊內存區域。
3.如果有一塊內存空間允許多線程操作,但要求每次最多只能有N個線程進行讀寫操作,那么可以使用“信息量”(Semaphore)的方法進行限制。實現方式類似於假如這塊內存空間被線程操作時有個count進行計數,每當有個線程讀寫時count就-1,當count=0時就不允許有線程對這塊內存空間進行操作了,直到有一個線程退出,才能允許下一個線程進入。
不難看出,Mutex是Semaphore的一種特殊情況。
4.操作系統的設計,可以歸結為三點:
(1)以多進程形式,允許多個任務同時進行。
(2)以多線程形式,允許單個任務分為不同的部分進行。
(3)提供協調機制,一方面防止進程之間和線程之間產生沖突,另一方面允許進程方面和線程方面共享資源。
線程-委托方式發起線程
在.NET Framework中我們可以使用委托調用BeginInvoke方法開啟一個線程。
Ps:.NET core 3.0不支持該方法,編譯可通過,但運行會報錯
直接貼代碼:
BeginInvoke開啟線程
需要引入命名空間using System.Threading;
class Program { static int Test(int i, string str) { Console.WriteLine("線程開啟。ID:" + i + " 線程名:" + str); Console.WriteLine("運算中"); Thread.Sleep(100);//使當前線程休眠,單位為ms Console.WriteLine("\n線程結束,返回結果"); return i+1; } static void Main(string[] args) { Func<int, string, int> f = Test;//委托指向所需開啟的線程 //IAsyncResult類的對象用於取得當前線程的狀態 IAsyncResult ar = f.BeginInvoke(100, "線程測試", null, null); //通過BeginInvoke開啟線程。ps:.NET core 3.0不支持BeginInvoke Console.WriteLine("Main"); Console.ReadKey(); } }
其中:Console.ReadKey();//如果不等線程結束就執行這步,進程會強制關閉線程,因為它開啟的是一個后台線程。關於后台和前台線程下面會講
IAsyncResult接口
首先解釋下里面的IAsyncResult接口,這個接口是用來表示異步操作的狀態的。按F12可查看到:
然后是IAsyncResult ar = f.BeginInvoke(100, "線程測試", null, null);這句代碼。
光標放在BeginInvoke上我們可以看到,對應着我們定義的Func<int,string,int>,前兩個參數即是我們傳遞到Func中的兩個參數(100, "線程測試"),都很好理解。Func指向的方法需要幾個參數,BeginInvoke前面部分就需要幾個參數;如果Func不需要參數,那么BeginInvoke就只有了兩個參數;
然后是倒數兩個參數。第一個是回調函數,可在線程結束后調用一個方法,這個方法或委托只能傳遞一個IAsyncResult類型的參數,該參數值由第二個參數提供,一般可以傳遞被調用方法的委托;
檢測線程結束並輸出返回值
檢測線程結束並輸出返回值的方法有一下幾個:
方法1:使用IAsynResult.IsCompleted判斷線程狀態並用Func<>.EndInvoke(IAsynResult result)取得返回值
while (ar.IsCompleted == false) { Console.Write("."); Thread.Sleep(10); } int result = f.EndInvoke(ar);//通過EndInvoke取得返回值 //EndInvoke將會阻塞線程,直到異步調用完成后才返回 Console.WriteLine("線程的返回結果為:" + result); Console.ReadKey();
阻塞線程在這里的意思就是result會等待委托f開啟的線程結束,才會被EndInvoke的返回值賦值。加入f指向的方法中最后一步Thread.Sleep(10000);這個操作的話,result會直到10s后方法結束了才得到賦值,繼續執行下面的代碼。
運行結果:
由於是異步,所以’.’可能會在”線程開啟.....”前輸出,如上圖所示
方法2:使用IAsyncResult.AsyncWaitHandle.WaitOne(int millisecondsTimeout)判斷線程狀態
bool isEnd = ar.AsyncWaitHandle.WaitOne(2000); if(isEnd) { int res = f.EndInvoke(ar); Console.WriteLine("線程的返回結果為:" + res); }
WaitOne()有多個重載,這里我們只需要使用第一個參數為int類型的即可。為了簡便我們直接將millisecondsTimeout簡略成t。
WaitOne(int t)表示在指定時間t毫秒內等待,直到超時或者完成。在時間內完成返回true;當超時線程仍沒有完成則返回false。t為0表示不等待,t為-1表示永遠等待直到異步調用完成
輸出結果如下:
方法3:使用BeginInvoke中的回調函數進行返回
既然是使用回調函數,我們就需要將上面的BeginInvoke改一下:
IAsyncResult ar = f.BeginInvoke(100,”線程測試”,OnCallBack,f);
//ps:.NET core 3.0不支持BeginInvoke
編寫一個回調函數:
static void OnCallBack(IAsyncResult iaRes) { Func<int,string,int> f = iaRes.AsyncState as Func<int,string,int>; int i = f.EndInvoke(iaRes);//取得返回值 Console.WriteLine(“線程測試結束,返回值為:”+i); }
看下輸出結果:
沒問題。
這里有兩個注意點:
(1)IAsyncResult ar = f.BeginInvoke(100,”線程測試”,OnCallBack,f);中f的執行順序和BeginInvoke中最后面的兩個參數。
順序:首先會先執行委托f(BeginInvoke前),然后執行參數f,而后再執行OnCallBack方法。
最后兩個參數:第一個參數好理解,即我們要使用的回調函數名。它一定指向的是一個方法,這樣才能執行異步同調。
第二個參數可以通過IAsyncResult.AsyncState獲得。輸入的值分三種情況:
當參數值為null時,由於沒有傳入什么參數給回調參數,所以回調參數中只能做一些與線程返回值無關的操作,因為此時AsyncState沒有值,我們用Console.WriteLine(iaRes.AsyncState)輸出一下可得如下結果:
當參數值為委托或者方法時,會執行這個方法並將返回值作為OnCallBack的參數傳遞給OnCallBack。以上面f為例,按照順序會先執行f.BeginInvoke,然后執行f的方法(Test),將f的返回值作為回調函數的參數。
當參數值為object類型的變量時,直接作為回調函數的參數。具體如何使用暫時還不懂,看到有些大佬說當遇到多線程的情況就很有用,可以用來區別線程。以后遇到了再更新吧。
(2)Func<int,string,int> f = iaRes.AsyncState as Func<int,string,int>;這樣做的用意:
AsyncState用於獲取IAsyncResult的一個對象,把光標放到上面我們可以看到提示:
前面我們知道EndInvoke可以獲取線程的返回值,所以我們可以將AsyncState對象轉換為委托類型的對象,然后調用該對象中EndInvoke得到我們想要的返回值。
擴展:可以使用Lambda表達式簡化回調函數
IAsyncResult ar = f.BeginInvoke(100,”線程測試”,a=> { int res = f.EndInvoke(a); Console.WriteLine(“用Lambda表達式簡化回調函數,返回值為:”+res); } ,null);
這樣就顯得代碼更加簡潔了。
以上參考大佬博客:https://www.cnblogs.com/oracleblogs/p/3275193.html
https://www.cnblogs.com/renhaojie/archive/2009/09/10/1564052.html
Thread開啟線程
除了BeginInvoke,我們還能使用Thread中Start開啟線程
需要引入System.Threading;命名空間
編寫一個Fileload方法作為線程
static void DownloadFile(object FileName)//必須為object類的參數才能創建Thread對象 { Console.WriteLine("線程id為:"+ Thread.CurrentThread.ManagedThreadId+" 文件名為: "+FileName);//Thread.CurrentThread.ManagedThreadId為線程id Console.WriteLine("下載開始"); Thread.Sleep(2000); Console.WriteLine("下載完成"); }
注意:參數必須為object類型;方法不能為Conditional特性,下面會報錯
然后在Main中進行調用:
Thread t = new Thread(DownloadFile);//如果上面的參數不為object這里會報錯 t.start("xxx.avi"); Console.ReadKey();
輸出:
可以用Lambda表達式簡化一下:
Thread t = new Thread((object FileName )=> { Console.WriteLine("線程id為:"+ Thread.CurrentThread.ManagedThreadId+" 文件名為: "+FileName);//Thread.CurrentThread.ManagedThreadId為線程id Console.WriteLine("下載開始"); Thread.Sleep(2000); Console.WriteLine("下載完成"); });
我們也可以自己編寫一個類,使用上面的方式調用類實例中的方法開啟線程。就不贅述了。
后台線程和前台線程
概念:線程是寄托在進程上的,進程都結束了,線程會關閉。而前台線程只要未退出,進程就不會終止。也就是程序不會關閉(即在資源管理器中可以看到進程未結束。)
上面Thread開啟的線程就是一個前台線程(默認創建后就是前台),main中會等待他執行完畢后才開始繼續下面的代碼。我們可以通過Thread.IsBackground賦值,將其設置為后台線程。
代碼也是很簡單,直接賦值即可。
控制線程
終止線程
我們可以使用Thread.Abort();終止線程。被終止的線程會拋出一個ThreadAbortException類型的異常,我們可以通過try catch獲取這個異常,然后在異常結束前做一些清理工作。
線程睡眠
我們可以使用Thread.Join();使當前線程睡眠,直到t線程結束再繼續執行下面的代碼
線程池開啟線程
我們可以使用ThreadPool.QueueUserWorkItem(method m,object o1,object o2....)方法來開啟多個線程。
代碼如下:
ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試1"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試2"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試3"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試4"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試5"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試6"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試7"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試8"); ThreadPool.QueueUserWorkItem(DownloadFile,"線程池測試9");
輸出結果為:
線程池注意事項:
- 線程池中的線程都是后台線程,如果進程中的所有前台線程都結束了,所有后台線程都會停止。且不能講入池的線程改為前台線程。
- 不能給入池的線程設置優先級或名稱。
- 入池的線程只能用於時間較短的任務。如果線程需要一直運行(如word的拼寫檢查器線程),就應該使用Thread創建一個線程。
Task開啟線程
使用Task.Start();開啟線程:
依然是上面的DownloadFile方法作為線程:
static void DownloadFile(object FileName)//必須為object類的參數才能創建Thread對象 { Console.WriteLine("線程id為:" + Thread.CurrentThread.ManagedThreadId + " 輸入參數為:" + FileName); Console.WriteLine("下載開始"); Thread.Sleep(2000); Console.WriteLine("下載完成"); }
然后創建一個Task對象用於獲取方法:
Main中:
Task t = new Task(DownloadFile,"任務1"); t.Start(); Console.WriteLine("Main"); Console.ReadKey();
使用TaskFactory.StartNew開啟線程:
TaskFactory tf = new TaskFactory(); Task t1 = tf.StartNew(DownloadFile, "任務1");//該方法能返回一個Task的對象 Task t2 = tf.StartNew(DownloadFile, "任務2"); Task t3 = tf.StartNew(DownloadFile, "任務3"); Task t4 = tf.StartNew(DownloadFile, "任務4"); Task t5 = tf.StartNew(DownloadFile, "任務5"); Console.WriteLine("Main"); Console.ReadKey();
輸出結果如下:
簡單控制任務順序
如果一個任務t1的執行時依賴於另一個任務t2的,那么就需要在這個任務t2執行完畢后才開始執行t1。這個時候我們就可以使用連續任務。
比如,定義兩個任務:
static void DoFirst() { Console.WriteLine("do in Task:"+Task.CurrentId); Thread.Sleep(3000); } static void DoSecond(Task t) { Console.WriteLine("task+"+t.Id+" finish"); Console.WriteLine("this task id is "+Task.CurrentId); Thread.Sleep(3000); }
Main中創建任務:
Task t1 = new Task(DoFirst); Task t2 = t1.ContinueWith(DoSecond); Task t3 = t1.ContinueWith(DoSecond); Task t4 = t2.ContinueWith(DoSecond);
這里t2和t3會等t1執行結束后才開始執行,t4則會等待t2執行完后開始。
任務層次結構
線程中是沒有父子關系的,而任務中有。
當我們在一個任務t1中啟動另一個任務t2,t2就相當與是t1的子任務了,兩個任務異步執行,如果父任務t1執行完了但t2還沒執行完,那么父任務的狀態會被設置為WaitForChildrenToComplete而不是Complete;只有當子任務也執行完了,父任務的狀態才會設置為RunToCompletion。
舉個栗子:
//父任務 static void parentTask() { Console.WriteLine("parent task id "+Task.CurrentId); var child = new Task(childTask); child.Start(); Thread.Sleep(1000); Console.WriteLine("parent started child,parent end"); } //子任務 static void childTask() { Console.WriteLine("child start"); Thread.Sleep(5000); Console.WriteLine("child finish"); }
Main中:
var parent = new Task(parentTask); parent.Start(); Thread.Sleep(1000); Console.WriteLine("parent.Status:"+parent.Status); Thread.Sleep(4000); Console.WriteLine("parent.Status:"+parent.Status); Console.WriteLine("Main"); Console.ReadKey();
結果如下:
多線程間的爭用條件
當同時有多個相同線程時,它們可能會爭用同一個資源發生一些我們不想要看到的情況,這時候我們可以給被爭用的對象加上一個所lock(對象)。
直接上代碼:
自定義MyThreadObject類用於創建對象:
class MyThreadObject { private int state = 5; public void ChangeState() { state++; if (state==5) { Console.WriteLine("state = 5"); } state = 5; } }
編寫一個循環調用的方法:
//循環調用MyThreadObject中的ChangeState方法 static void circleChange(object o) { MyThreadObject m = o as MyThreadObject; while (true) { //鎖定對象,讓其同一時刻只允許被一個線程使用 lock (m) { m.ChangeState(); } } }
主函數中創建兩個線程模擬爭用
static void Main(string[] args) { //發起第一個線程循環調用 MyThreadObject m = new MyThreadObject(); Thread t = new Thread(circleChange); t.Start(m); //發起第二個線程與第一個線程異步調用,此時兩個線程會不停的將state更改為5,這樣就會滿足類中的ChangeState一直輸出"state = 5" //解決方法就是在循環調用的方法中加上一個lock將對象鎖定 new Thread(circleChange).Start(m); Console.ReadKey(); }
死鎖問題
當兩個線程中有兩個鎖並且造成了互相鎖定導致程序無法繼續執行的情況,如以下代碼:
public void Deadlock1() { int i = 0; while (true) { lock (s1) { lock (s2) { s1.ChangeState(); s2.ChangeState(); i++; Console.WriteLine("Running i:"+i); } } } } public void Deadlock2() { int i = 0; while (true) { lock (s2) { lock (s1) { s1.ChangeState(); s2.ChangeState(); i++; Console.WriteLine("Running i:" + i); } } } }
當線程1運行到Deadlock1中的lock(s1)時s1將被鎖定,此時如果線程2運行到Deadlock2中的lock(s2)並將s2鎖定,這樣的話線程1將無法獲取s2,線程2無法獲取s1,他們就造成了我們常說的死鎖。死鎖歸根到底是lock的順序問題,所以我們只要安排好順序,就能解決死鎖的問題。就以上面的例子來說,我們只要統一好順序,將Deadlock2中的lock(s2)和lock(s1)調換即可。
MyThreadObject m1 = new MyThreadObject(); MyThreadObject m2 = new MyThreadObject(); SampleThread s = new SampleThread(m1,m2); new Thread(s.Deadlock1).Start(); new Thread(s.Deadlock2).Start();
輸出結果為: