C# 線程、線程池、Task概念+代碼實(shí)踐
轉(zhuǎn)自:JerryMouseLi cnblogs.com/JerryMouseLi/p/14135600.html
前言
線程中的概念很多,如果沒有代碼示例來理解,會(huì)比較晦澀,而且有些概念落不到實(shí)處,因此,本文以一些運(yùn)行示例代碼,結(jié)果來闡述線程中的一些基礎(chǔ)概念。讓自己跟讀者一起把線程中的概念理解地更深刻。
一、線程安全
1.1、未出現(xiàn)線程搶占
class ThreadTest2
{
bool done;
static void Main()
{
ThreadTest2 tt = new ThreadTest2(); // Create a common instance
new Thread(tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void Go()
{
if (!done)
{
done = true;
Console.WriteLine("Done");
}
}
}
運(yùn)行結(jié)果如下:
Done
1.2、線程搶占
class ThreadTest2
{
bool done;
static void Main()
{
ThreadTest2 tt = new ThreadTest2(); // Create a common instance
new Thread(tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void Go()
{
if (!done)
{
Console.WriteLine("Done");
done = true;
}
}
}
運(yùn)行結(jié)果如下:
Done
Done
線程搶占例子2:
for (int i = 0; i < 10; i++)
new Thread (() => Console.Write (i)).Start();
運(yùn)行結(jié)果
0223557799
1.3、避免線程搶占
class ThreadTest2
{
static readonly object locker = new object();
bool done;
static void Main()
{
ThreadTest2 tt = new ThreadTest2(); // Create a common instance
new Thread(tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void Go()
{
lock (locker)
{
if (!done)
{
Console.WriteLine("Done");
done = true;
}
}
}
}
運(yùn)行結(jié)果如下:
Done
二、線程阻塞
class Program
{
static void Main()
{
Thread t = new Thread(Go);
t.Start();
t.Join();
Console.WriteLine("Thread t has ended!");
}
static void Go()
{
for (int i = 0; i < 1000; i++) Console.Write("y");
}
}
運(yùn)行結(jié)果:

1000個(gè)y打印完畢才輸出"Thread t has ended!"。
Thread.Sleep (500);
也會(huì)阻塞線程,讓渡CPU的執(zhí)行權(quán)給其他線程。
三、Thread.yield()和Thread.sleep(0)
sleep(0)效果相當(dāng)于yield(),會(huì)讓當(dāng)前線程放棄剩余時(shí)間片,進(jìn)入相同優(yōu)先級(jí)線程隊(duì)列的隊(duì)尾,只有排在前面的所有同優(yōu)先級(jí)線程完成調(diào)度后,它才能再次獲執(zhí)行的機(jī)會(huì)。
四、線程如何工作
多線痛通過內(nèi)部的線程調(diào)度器(thread scheduler)管理,通過clr委托操作系統(tǒng)。線程調(diào)度器會(huì)分配適當(dāng)?shù)膱?zhí)行時(shí)間給活動(dòng)線程,線程等待(鎖)或者線程阻塞(用戶輸入)不會(huì)消耗cpu執(zhí)行時(shí)間。
單核處理器電腦上,在Windows,時(shí)間片通常會(huì)被分配幾十毫秒,遠(yuǎn)大于線程上下文切換還時(shí)間幾毫秒。
在多處理器計(jì)算機(jī)上,多線程是通過時(shí)間片和真正的并發(fā)實(shí)現(xiàn)的,其中不同的線程在不同的CPU上同時(shí)運(yùn)行代碼。幾乎可以肯定,由于操作系統(tǒng)需要服務(wù)自己的線程以及其他應(yīng)用程序的線程,因此還會(huì)有一定的時(shí)間片。
當(dāng)線程的執(zhí)行由于諸如時(shí)間片之類的外部因素而被中斷時(shí),該線程被認(rèn)為是被搶占的。在大多數(shù)情況下,線程無法控制其被搶占的時(shí)間和地點(diǎn)。
五、線程與進(jìn)程
線程與進(jìn)程有相似之處。就像進(jìn)程在計(jì)算機(jī)上并行運(yùn)行一樣,多個(gè)線程在單個(gè)進(jìn)程中并行運(yùn)行。進(jìn)程彼此完全隔離;線程的隔離度有限。特別是,線程與在同一應(yīng)用程序中運(yùn)行的其他線程共享(堆)內(nèi)存。這就是為什么線程有用的原因:例如,一個(gè)線程可以在后臺(tái)獲取數(shù)據(jù),而另一個(gè)線程可以在數(shù)據(jù)到達(dá)時(shí)顯示數(shù)據(jù)。
六、線程的使用和濫用
利于響應(yīng)式用戶界面
在同時(shí)并行運(yùn)行的"worker"線程上運(yùn)行耗時(shí)的任務(wù),主UI線程可以自由繼續(xù)處理鍵盤和鼠標(biāo)事件。
有效利用原本被阻塞的CPU
當(dāng)線程正在等待來自另一臺(tái)計(jì)算機(jī)或硬件的響應(yīng)時(shí),多線程很有用。當(dāng)一個(gè)線程在執(zhí)行任務(wù)時(shí)被阻塞時(shí),其他線程可以利用本來沒有負(fù)擔(dān)的計(jì)算機(jī)的其他線程來響應(yīng)任務(wù)。
并行編程
如果以"分而治之"策略在多個(gè)線程之間共享工作負(fù)載,則執(zhí)行密集計(jì)算的代碼可以在多核或多處理器計(jì)算機(jī)上更快地執(zhí)行(請(qǐng)參閱第5部分)。
隨機(jī)執(zhí)行
在多核計(jì)算機(jī)上,有時(shí)可以通過預(yù)測(cè)可能需要完成的事情然后提前進(jìn)行來提高性能。LINQPad使用此技術(shù)來加速新查詢的創(chuàng)建。一種變化是并行運(yùn)行許多不同的算法,這些算法都可以解決同一任務(wù)。誰先獲得“勝利”,當(dāng)您不知道哪種算法執(zhí)行速度最快時(shí),此方法將非常有效。
允許服務(wù)同時(shí)處理請(qǐng)求
在服務(wù)器上,客戶端請(qǐng)求可以同時(shí)到達(dá),因此需要并行處理(如果使用ASP.NET,WCF,Web服務(wù)或遠(yuǎn)程處理,.NET Framework會(huì)為此自動(dòng)創(chuàng)建線程)。這在客戶端上也很有用(例如,處理對(duì)等網(wǎng)絡(luò)-甚至來自用戶的多個(gè)請(qǐng)求)。
使用ASP.NET和WCF之類的技術(shù),您如果不知道多線程正在發(fā)生-除非您在沒有適當(dāng)鎖定的情況下訪問共享數(shù)據(jù)(可能通過靜態(tài)字段),會(huì)破壞線程安全性。
線程之間的交互(通常是通過共享數(shù)據(jù)),會(huì)帶來很多復(fù)雜性,但卻不可避免,因此,有必要將交互保持在最低限度,并盡可能地堅(jiān)持簡(jiǎn)單可靠的設(shè)計(jì)。
好的策略是將多線程邏輯封裝到可重用的類中,這些類可以獨(dú)立檢查和測(cè)試。框架本身提供了許多更高級(jí)別的線程結(jié)構(gòu),我們將在后面介紹。
線程化還會(huì)在調(diào)度和切換線程時(shí)(如果活動(dòng)線程多于CPU內(nèi)核)會(huì)導(dǎo)致資源和CPU的浪費(fèi),并且還會(huì)產(chǎn)生創(chuàng)建/釋放成本。多線程并不總是可以加快您的應(yīng)用程序的速度-如果使用過多或使用不當(dāng),它甚至可能減慢其速度。例如,當(dāng)涉及大量磁盤I / O時(shí),讓幾個(gè)工作線程按順序運(yùn)行任務(wù)比一次執(zhí)行10個(gè)線程快得多。
七、線程傳參
7.1、lambda表達(dá)式傳參
最方便的方法就是通過lambda表達(dá)式調(diào)用匿名方法,傳參數(shù)。
static void Main()
{
Thread t = new Thread(() =>Print("Hello from t!"));
t.Start();
}
static void Print(string message)
{
Console.WriteLine(message);
}
7.2、線程start方法傳參
static void Main()
{
Thread t = new Thread(Print);
t.Start("Hello from t!");
}
static void Print(object messageObj)
{
string message = (string)messageObj;
// We need to cast here
Console.WriteLine(message);
}
7.3、線程創(chuàng)建需要時(shí)間
string text = "t1";
Thread t1 = new Thread ( () => Console.WriteLine (text) );
text = "t2";
Thread t2 = new Thread ( () => Console.WriteLine (text) );
t1.Start();
t2.Start();
運(yùn)行結(jié)果:
t2
t2
以上運(yùn)行結(jié)果說明,在t1線程創(chuàng)建之前text被修改成了t2。
八、線程命名
每個(gè)線程都有名稱屬性,目的是為了更方便調(diào)試。
static void Main()
{
Thread.CurrentThread.Name = "main";
Thread worker = new Thread(Go);
worker.Name = "worker";
worker.Start();
Go();
}
static void Go()
{
Console.WriteLine("Hello from " + Thread.CurrentThread.Name);
}
運(yùn)行結(jié)果:
Hello from main
Hello from worker
九、前臺(tái)線程與后臺(tái)線程
Thread worker = new Thread(() => Console.ReadLine());
if (args.Length > 0) worker.IsBackground = true;
worker.Name = "backThread";
worker.Start();
Console.WriteLine("finish!");
前臺(tái)線程會(huì)隨著主線程窗口關(guān)閉而停止,后臺(tái)線程及時(shí)主線程窗口關(guān)閉自己獨(dú)立運(yùn)行。
十、線程優(yōu)先級(jí)
線程優(yōu)先級(jí)決定了操作系統(tǒng)執(zhí)行活動(dòng)線程時(shí)間的長(zhǎng)短。
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
有時(shí)候提高了線程的優(yōu)先級(jí),但卻仍然無法滿足一些實(shí)時(shí)的應(yīng)用需求,這時(shí)候就需要提高進(jìn)程的優(yōu)先級(jí),System.Diagnostics命名空間中的process進(jìn)程類.
using (Process p = Process.GetCurrentProcess())
p.PriorityClass = ProcessPriorityClass.High;
實(shí)際上,ProcessPriorityClass.High比最高優(yōu)先級(jí)低1個(gè)級(jí)別:Realtime。將進(jìn)程優(yōu)先級(jí)設(shè)置為Realtime,可指示OS,您永遠(yuǎn)不希望該進(jìn)程將CPU時(shí)間浪費(fèi)在另一個(gè)進(jìn)程上。如果您的程序進(jìn)入意外的無限循環(huán),您甚至可能會(huì)發(fā)現(xiàn)操作系統(tǒng)已鎖定,只剩下電源按鈕可以拯救您!因此,高通常是實(shí)時(shí)應(yīng)用程序的最佳選擇。
如果您的實(shí)時(shí)應(yīng)用程序具有用戶界面,則提高處理優(yōu)先級(jí)將給屏幕更新帶來過多的CPU時(shí)間,從而減慢整個(gè)計(jì)算機(jī)的速度(尤其是在UI復(fù)雜的情況下)。降低主線程的優(yōu)先級(jí)并提高進(jìn)程的優(yōu)先級(jí)可確保實(shí)時(shí)線程不會(huì)因屏幕重繪而被搶占,但不會(huì)解決使其他應(yīng)用程序耗盡CPU時(shí)間的問題,因?yàn)椴僮飨到y(tǒng)仍會(huì)分配 整個(gè)過程的資源不成比例。理想的解決方案是使實(shí)時(shí)工作程序和用戶界面作為具有不同進(jìn)程優(yōu)先級(jí)的單獨(dú)應(yīng)用程序運(yùn)行,并通過遠(yuǎn)程處理或內(nèi)存映射文件進(jìn)行通信。內(nèi)存映射文件非常適合此任務(wù)。我們將在C#4.0的第14和25章中簡(jiǎn)要介紹它們的工作原理。
十一、異常處理
Go無法補(bǔ)捉異常,GoCatch能捕獲當(dāng)前線程的異常,輸出Console.WriteLine("exception.");由此可見,線程創(chuàng)建之后,異常只能由本線程捕獲,如果其調(diào)用方需要捕獲,則得用共享內(nèi)存方式往上傳,Task幫我們做了這件事,調(diào)用方可在task.result里捕獲到其他線程的異常。
public static void Main()
{
try
{
new Thread(Go).Start();
Console.ReadKey();
}
catch (Exception ex)
{
// We'll never get here!
Console.WriteLine("Exception!");
}
}
static void Go() { throw null; } // Throws a NullReferenceException
static void GoCatch()
{
try
{
// ...
throw null; // The NullReferenceException will get caught below
// ...
}
catch (Exception ex)
{
// Typically log the exception, and/or signal another thread
// that we've come unstuck
// ...
Console.WriteLine("exception.");
}
}
十二、線程池
當(dāng)你創(chuàng)建一個(gè)線程,幾百毫秒會(huì)被花費(fèi)在例如創(chuàng)建本地私有變量堆棧。每個(gè)線程都會(huì)默認(rèn)消耗1MB內(nèi)存,從而允許在非常精細(xì)的級(jí)別上應(yīng)用多線程而不會(huì)影響性能。當(dāng)利用多核處理器以“分而治之”的方式并行執(zhí)行計(jì)算密集型代碼時(shí),這很有用。
線程池還限制了將同時(shí)運(yùn)行的工作線程總數(shù)。活動(dòng)線程過多會(huì)限制操作系統(tǒng)的管理負(fù)擔(dān),并使CPU緩存無效。一旦達(dá)到限制,作業(yè)將排隊(duì)并僅在另一個(gè)作業(yè)完成時(shí)才開始。這使任意并發(fā)的應(yīng)用程序成為可能,例如Web服務(wù)器。(異步方法模式是一種先進(jìn)的技術(shù),通過高效利用池線程來進(jìn)一步實(shí)現(xiàn)這一點(diǎn);我們?cè)贑#4.0的第23章中簡(jiǎn)要介紹了這一點(diǎn))。
有多種進(jìn)入線程池的方法:
通過Task Parallel Library(來自Framework 4.0)
通過調(diào)用ThreadPool.QueueUserWorkItem
通過異步委托(await)
通過BackgroundWorker
以下方法間接使用線程池:
WCF,遠(yuǎn)程,ASP.NET和ASMX Web服務(wù)應(yīng)用程序服務(wù)器
System.Timers.Timer和System.Threading.Timer
以Async結(jié)尾的框架方法,例如WebClient(基于事件的異步模式)上的框架方法和大多數(shù)BeginXXX方法(異步編程模型模式)
PLINQ
使用池線程時(shí),需要注意以下幾點(diǎn):
無法設(shè)置池線程的名稱,這會(huì)使調(diào)試更加困難(盡管您可以在Visual Studio的“線程”窗口中進(jìn)行調(diào)試時(shí)附加說明)。
池線程始終是后臺(tái)線程(這通常不是問題)。
除非您調(diào)用ThreadPool.SetMinThreads(請(qǐng)參閱優(yōu)化線程池),否則阻塞線程池可能會(huì)在應(yīng)用程序的早期階段觸發(fā)額外的延遲。
可以自由更改池線程的優(yōu)先級(jí)-將其釋放回池后將恢復(fù)為正常狀態(tài)。
可以通過Thread.CurrentThread.IsThreadPoolThread屬性查詢當(dāng)前是否在線程池上執(zhí)行。
12.1、通過TPL進(jìn)入線程池
通過Task Parallel Library庫中的Task類可輕松使用線程池,Task類由Framework 4.0引入,如果你熟悉老的結(jié)構(gòu),考慮用不帶泛型Task類來替代ThreadPool.QueueUserWorkItem,而泛型Task 代表的是一個(gè)異步委托。新的結(jié)構(gòu)更快,更方便,比老的更靈活。
使用不帶泛型例子的Task類,調(diào)用Task.Factory.StartNew,傳遞一個(gè)目標(biāo)方法的委托;
static void Main() // The Task class is in System.Threading.Tasks
{
var task=Task.Factory.StartNew(Go);
Console.WriteLine("main");
task.Wait() ;
Console.WriteLine(task.Result);
Console.ReadLine();
}
static string Go()
{
if (Thread.CurrentThread.IsThreadPoolThread)
{ Console.WriteLine("Hello from the thread pool!"); }
else { Console.WriteLine("Hello just from the thread!"); }
return "task complete!";
}
輸出結(jié)果:
main
Hello from the thread pool!
task complete!
12.1.1、Task異常捕獲
static void Main() // The Task class is in System.Threading.Tasks
{
var task=Task.Factory.StartNew(Go);
Console.WriteLine("main");
try
{ task.Wait(); }
catch (Exception e)
{
Console.WriteLine("exception!");
}
Console.WriteLine(task.Result);
Console.ReadLine();
}
static string Go()
{
if (Thread.CurrentThread.IsThreadPoolThread)
{ Console.WriteLine("Hello from the thread pool!"); }
else { Console.WriteLine("Hello just from the thread!"); }
throw null;
return "task complete!";
}
運(yùn)行結(jié)果,在主線程中捕獲到了其他線程的異常:

static void Main()
{
// Start the task executing:
Task<string> task = Task.Factory.StartNew<string>
( () => DownloadString ("http://www.linqpad.net") );
// We can do other work here and it will execute in parallel:
RunSomeOtherMethod();
// When we need the task's return value, we query its Result property:
// If it's still executing, the current thread will now block (wait)
// until the task finishes:
string result = task.Result;
}
static string DownloadString (string uri)
{
using (var wc = new System.Net.WebClient())
return wc.DownloadString (uri);
}
Task<string> 就是一個(gè)返回值為string的異步委托。
12.2、不同過TPL進(jìn)入線程池
如果你的框架是.Net 4.0之前的,你可以不通過Task Parallel Library 進(jìn)入線程池。
12.2.1、QueueUserWorkItem
static void Main()
{
ThreadPool.QueueUserWorkItem(Go);
ThreadPool.QueueUserWorkItem(Go, 123);
Console.ReadLine();
}
static void Go(object data) // data will be null with the first call.
{
Console.WriteLine("Hello from the thread pool! " + data);
}
運(yùn)行結(jié)果:
Hello from the thread pool!
Hello from the thread pool! 123
與Task不同:
后續(xù)執(zhí)行中無法返回執(zhí)行結(jié)果;
無法返回異常給調(diào)用者;
12.2.2 異步委托
委托的EndInvoke 做了3件事:
阻塞等待;
返回結(jié)果;
向調(diào)用者跑出異常;
12.3、線程池優(yōu)化
線程池從其池中的一個(gè)線程開始。分配任務(wù)后,池管理器會(huì)“注入”新線程以應(yīng)對(duì)額外的并發(fā)工作負(fù)載(最大限制)。在足夠長(zhǎng)時(shí)間的不活動(dòng)之后,如果池管理器懷疑這樣做會(huì)導(dǎo)致更好的吞吐量,則可以“退出”線程。
可以通過調(diào)用ThreadPool.SetMaxThreads;來設(shè)置池將創(chuàng)建的線程的上限; 默認(rèn)值為:
32位環(huán)境中的Framework 4.0中的1023
64位環(huán)境中的Framework 4.0中的32768
Framework 3.5中的每個(gè)內(nèi)核250個(gè)
Framework 2.0中每個(gè)內(nèi)核25個(gè)
還可以通過調(diào)用ThreadPool.SetMinThreads設(shè)置下限。下限的作用是微妙的:這是一種高級(jí)優(yōu)化技術(shù),它指示池管理器在達(dá)到下限之前不要延遲線程的分配。當(dāng)存在阻塞的線程時(shí),提高最小線程數(shù)可提高并發(fā)性。
默認(rèn)的下限是每個(gè)處理器內(nèi)核一個(gè)線程-允許全部CPU利用率的最小值。但是,在服務(wù)器環(huán)境(例如IIS下的ASP .NET)上,下限通常要高得多-多達(dá)50個(gè)或更多。
設(shè)置線程池最小線程數(shù)量。
ThreadPool.SetMinThreads (50, 50);
本文代碼Git:https://github.com/JerryMouseLi/Thread.git
- EOF -
臥槽:微信可以這樣換個(gè)字體了!
GET 和 POST請(qǐng)求的本質(zhì)區(qū)別是什么?原來我一直理解錯(cuò)了
點(diǎn)贊和在看就是最大的支持??
