并發(fā)編程 - 線程淺試
前面已經(jīng)對線程有了初步認(rèn)識,下面我們來嘗試使用線程。

01、線程創(chuàng)建
在C#中創(chuàng)建線程主要是通過Thread構(gòu)造函數(shù)實現(xiàn),下面講解3種常見的創(chuàng)建方式。
1、通過ThreadStart創(chuàng)建
Thread有一個帶有ThreadStart類型參數(shù)的構(gòu)造函數(shù),其中參數(shù)ThreadStart是一個無參無返回值委托,因此我們可以創(chuàng)建一個無參無返回值方法傳入Thread構(gòu)造函數(shù)中,代碼如下:
public class ThreadSample
{
public static void CreateThread()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(BusinessProcess);
thread.Start();
}
//線程1
public static void BusinessProcess()
{
Console.WriteLine($"BusinessProcess 線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("開始處理業(yè)務(wù)……");
//業(yè)務(wù)實現(xiàn)
Console.WriteLine("結(jié)束處理業(yè)務(wù)……");
}
}
代碼也相當(dāng)簡單,我們在主線程中通過Thread創(chuàng)建了一個新的線程用來運行BusinessProcess方法,同時通過Thread.CurrentThread.ManagedThreadId打印出當(dāng)前線程Id。
代碼執(zhí)行結(jié)果如下,主線程Id和業(yè)務(wù)線程Id并不相同。

2、通過ParameterizedThreadStart帶參創(chuàng)建
Thread還有一個帶有ParameterizedThreadStart類型參數(shù)的構(gòu)造函數(shù),其中參數(shù)ParameterizedThreadStart是一個有參無返回值委托,其中參數(shù)為object類型,因此我們可以創(chuàng)建一個有參無返回值方法傳入Thread構(gòu)造函數(shù)中,然后通過Thread.Start方法把參數(shù)傳遞給線程,代碼如下:
public static void CreateThreadParameterized()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(BusinessProcessParameterized);
//傳入?yún)?shù)
thread.Start("Hello World!");
}
//帶參業(yè)務(wù)線程
public static void BusinessProcessParameterized(object? param)
{
Console.WriteLine($"BusinessProcess 線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"參數(shù) param 為:{param}");
Console.WriteLine("開始處理業(yè)務(wù)……");
//業(yè)務(wù)實現(xiàn)
Console.WriteLine("結(jié)束處理業(yè)務(wù)……");
}
我們看看代碼執(zhí)行結(jié)果:

該方式有個限制,因為ParameterizedThreadStart委托參數(shù)為object類型,因此我們的業(yè)務(wù)方法也必須要用object類型接收參數(shù),然后再根據(jù)實際類型進行轉(zhuǎn)換。
3、通過Lambda表達式創(chuàng)建
通過上面可以知道無論ThreadStart還是ParameterizedThreadStart本質(zhì)上都是一個委托,因此我們可以直接使用Lambda表達式直接構(gòu)建一個委托??梢钥纯匆韵麓a:
public static void CreateThreadLambda()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(() =>
{
Console.WriteLine($"業(yè)務(wù)線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("開始處理業(yè)務(wù)……");
//業(yè)務(wù)實現(xiàn)
Console.WriteLine("結(jié)束處理業(yè)務(wù)……");
});
//傳入?yún)?shù)
thread.Start();
}
代碼執(zhí)行結(jié)果如下:

因為Lambda表達式可以直接訪問外部作用域中的變量,因此線程傳參還可以使用Lambda表達式來實現(xiàn)。
但是這也導(dǎo)致了一些問題,比如下面代碼執(zhí)行結(jié)果應(yīng)該是什么?先自己想想看。
public static void CreateThreadLambdaParameterized()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
var param = "Hello";
var thread1 = new Thread(() => BusinessProcessParameterized(param));
thread1.Start();
param = "World";
var thread2 = new Thread(() => BusinessProcessParameterized(param));
thread2.Start();
}
//帶參業(yè)務(wù)線程
public static void BusinessProcessParameterized(string param)
{
Console.WriteLine($"業(yè)務(wù)線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"參數(shù) param 為:{param}");
}
看看執(zhí)行結(jié)果:

和你想想的結(jié)果一樣嗎?
這是因為當(dāng)在Lambda 表達式中使用任何外部局部變量時,編譯器會自動生成一個類,并將該變量作為該類的一個屬性。因此這些外部變量并不是存儲在棧中,而是通過引用存儲在堆中,因此此時param參數(shù)實際上在內(nèi)存中是一個類是一個引用類型,所以兩個線程中使用的param都指向了堆中的同一個值。
并且使用Lambda表達式引用另一個C#對象的方式有個專有名詞叫閉包。感興趣的可以去了解下閉包概念。
02、線程休眠
可以通過Sleep方法暫停當(dāng)前線程,使其處于休眠狀態(tài),以盡可能少的占用CPU時間??慈缦率纠a,通過在Sleep方法前后打印出當(dāng)前時間對比,來觀察暫停線程效果。
public static void ThreadSleep()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(() =>
{
Console.WriteLine($"業(yè)務(wù)線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"暫停線程前:{DateTime.Now:HH:mm:ss}");
//暫停線程10秒
Thread.Sleep(10000);
Console.WriteLine($"暫停線程后:{DateTime.Now:HH:mm:ss}");
});
thread.Start();
thread.Join();
}
代碼執(zhí)行結(jié)果如下:

可以發(fā)現(xiàn)暫停線程前后正好差了10秒鐘。
03、線程等待
線程等待指讓程序等待另一個需要長時間計算的線程運行完成后,再繼續(xù)后面操作。而使用Thread.Sleep方法并不能滿足需求,因為當(dāng)前并不知道執(zhí)行計算到底需要多少時間,因此可以使用Thread.Join。如上一小節(jié)中代碼,當(dāng)代碼執(zhí)行到Thread.Join方法時,則線程會處于阻塞狀態(tài),只有線程執(zhí)行完成后才會繼續(xù)往下執(zhí)行。具體示例可以看上一小節(jié)。
04、線程其他方法
此外線程還有暫停、恢復(fù)、中斷、終止等線程方法,這里就不介紹了,因為一些方法已經(jīng)棄用沒有必要再花經(jīng)歷學(xué)習(xí)了。
05、異常處理
對于線程中的異常需要特別注意,對于一個Thread子線程所產(chǎn)生的異常,默認(rèn)情況下主線程并不能捕捉到,可以查看下面示例:
public static void ThreadException()
{
Console.WriteLine($"主線程Id:{Thread.CurrentThread.ManagedThreadId}");
try
{
var thread = new Thread(ThreadThrowException);
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine("子線程異常信息:" + ex.Message);
}
}
//業(yè)務(wù)線程不處理異常,直接拋出
public static void ThreadThrowException()
{
Console.WriteLine($"業(yè)務(wù)線程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("開始處理業(yè)務(wù)……");
//業(yè)務(wù)實現(xiàn)
Console.WriteLine("結(jié)束處理業(yè)務(wù)……");
throw new Exception("異常");
}
運行結(jié)果如下:

可以看到在主線程中并沒有捕捉到子線程拋出的異常,而導(dǎo)致程序直接中斷。因此我們在處理線程異常時需要特別注意,可以直接在線程中處理異常。
06、何時應(yīng)該使用線程
線程有很多優(yōu)點,但也并不是萬能的,因為每一個線程都會產(chǎn)生大量的資源消耗,包括:占用大量內(nèi)存空間,線程的創(chuàng)建、銷毀和管理,線程之間的上下文切換,以及垃圾回收的消耗。
舉個簡單例子,比如一個小餐館,有一個廚師,一個下單員,客戶下單給下單員,下單員把客戶下的菜單傳遞給廚師。假如現(xiàn)在客戶很多一個下單員忙不過來,老板決定再添加一個下單員,此時下單的效率可以提升一倍,但是廚師還是一個,那么就會導(dǎo)致當(dāng)廚師和A下單員交接的時候,B下單員只能等著,并且因為之前廚師和A下單員長時間合作形成了彼此默契,這是再和B下單員交接的時候效率可能并不高,因此最終整體效率并不一定提升多少。如果把廚師比作CPU處理器,下單員比作線程,如果要想餐館的整體效率提升那么在增加下單員的時候,必須要相應(yīng)的添加廚師,才能使得餐館最大效率的提升。
因此并不是說無腦的添加線程就可以使得程序效率提升,需要按需使用。
比如在以下使用場景可以考慮使用多線程:文件多寫、網(wǎng)絡(luò)請求、數(shù)據(jù)庫查詢、圖像處理、數(shù)據(jù)分析、定時任務(wù)等。
注:測試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網(wǎng)安備 33010602011771號