執行緒(一)——執行緒,執行緒池,Task概念+代碼實踐
目錄- 執行緒(一)——執行緒,執行緒池,Task概念+代碼實踐
- 摘要
- 1 執行緒安全
- 1.1 未出現執行緒搶占
- 1.2 執行緒搶占
- 1.3 避免執行緒搶占
- 2 執行緒阻塞
- 3 Thread.yield()和Thread.sleep(0)
- 4 執行緒如何作業
- 5 執行緒與行程
- 6 執行緒的使用和濫用
- 7 執行緒傳參
- 7.1 lambda運算式傳參
- 7.2 執行緒start方法傳參
- 7.3 執行緒創建需要時間
- 8 執行緒命名
- 9 前臺執行緒與后臺執行緒
- 10 執行緒優先級
- 11 例外處理
- 12 執行緒池
- 12.1 通過TPL進入執行緒池
- 12.1.1 Task例外捕獲
- 12.2 不同過TPL進入執行緒池
- 12.2.1 QueueUserWorkItem
- 12.2.2 異步委托
- 12.3 執行緒池優化
- 12.1 通過TPL進入執行緒池
- 13 代碼
- 14 參考文章
摘要
執行緒中的概念很多,如果沒有代碼示例來理解,會比較晦澀,而且有些概念落不到實處,因此,本文以一些運行示例代碼,結果來闡述執行緒中的一些基礎概念,讓自己跟讀者一起把執行緒中的概念理解地更深刻,
1 執行緒安全
1.1 未出現執行緒搶占
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");
}
}
}
運行結果如下:
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;
}
}
}
運行結果如下:
Done
Done
執行緒搶占例子2:
for (int i = 0; i < 10; i++)
new Thread (() => Console.Write (i)).Start();
運行結果
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;
}
}
}
}
運行結果如下:
Done
2 執行緒阻塞
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");
}
}
運行結果:

1000個y列印完畢才輸出"Thread t has ended!",
Thread.Sleep (500);
也會阻塞執行緒,讓渡CPU的執行權給其他執行緒,
3 Thread.yield()和Thread.sleep(0)
sleep(0)效果相當于yield(),會讓當前執行緒放棄剩余時間片,進入相同優先級執行緒佇列的隊尾,只有排在前面的所有同優先級執行緒完成調度后,它才能再次獲執行的機會,
4 執行緒如何作業
多線痛通過內部的執行緒調度器(thread scheduler)管理,通過clr委托作業系統,執行緒調度器會分配適當的執行時間給活動執行緒,執行緒等待(鎖)或者執行緒阻塞(用戶輸入)不會消耗cpu執行時間,
單核處理器電腦上,在Windows,時間片通常會被分配幾十毫秒,遠大于執行緒背景關系切換還時間幾毫秒,
在多處理器計算機上,多執行緒是通過時間片和真正的并發實作的,其中不同的執行緒在不同的CPU上同時運行代碼, 幾乎可以肯定,由于作業系統需要服務自己的執行緒以及其他應用程式的執行緒,因此還會有一定的時間片,
當執行緒的執行由于諸如時間片之類的外部因素而被中斷時,該執行緒被認為是被搶占的, 在大多數情況下,執行緒無法控制其被搶占的時間和地點,
5 執行緒與行程
執行緒與行程有相似之處, 就像行程在計算機上并行運行一樣,多個執行緒在單個行程中并行運行, 行程彼此完全隔離; 執行緒的隔離度有限, 特別是,執行緒與在同一應用程式中運行的其他執行緒共享(堆)記憶體, 這就是為什么執行緒有用的原因:例如,一個執行緒可以在后臺獲取資料,而另一個執行緒可以在資料到達時顯示資料,
6 執行緒的使用和濫用
-
利于回應式用戶界面
在同時并行運行的“worker”執行緒上運行耗時的任務,主UI執行緒可以自由繼續處理鍵盤和滑鼠事件, -
有效利用原本被阻塞的CPU
當執行緒正在等待來自另一臺計算機或硬體的回應時,多執行緒很有用, 當一個執行緒在執行任務時被阻塞時,其他執行緒可以利用本來沒有負擔的計算機的其他執行緒來回應任務, -
并行編程
如果以``分而治之''策略在多個執行緒之間共享作業負載,則執行密集計算的代碼可以在多核或多處理器計算機上更快地執行(請參閱第5部分), -
隨機執行
在多核計算機上,有時可以通過預測可能需要完成的事情然后提前進行來提高性能, LINQPad使用此技術來加速新查詢的創建, 一種變化是并行運行許多不同的演算法,這些演算法都可以解決同一任務, 誰先獲得“勝利”,當您不知道哪種演算法執行速度最快時,此方法將非常有效, -
允許服務同時處理請求
在服務器上,客戶端請求可以同時到達,因此需要并行處理(如果使用ASP.NET,WCF,Web服務或遠程處理,.NET Framework會為此自動創建執行緒), 這在客戶端上也很有用(例如,處理對等網路-甚至來自用戶的多個請求),
使用ASP.NET和WCF之類的技術,您如果不知道多執行緒正在發生-除非您在沒有適當鎖定的情況下訪問共享資料(可能通過靜態欄位),會破壞執行緒安全性,
執行緒之間的互動(通常是通過共享資料),會帶來很多復雜性,但卻不可避免,因此,有必要將互動保持在最低限度,并盡可能地堅持簡單可靠的設計,
好的策略是將多執行緒邏輯封裝到可重用的類中,這些類可以獨立檢查和測驗, 框架本身提供了許多更高級別的執行緒結構,我們將在后面介紹,
執行緒化還會在調度和切換執行緒時(如果活動執行緒多于CPU內核)會導致資源和CPU的浪費,并且還會產生創建/釋放成本, 多執行緒并不總是可以加快您的應用程式的速度-如果使用過多或使用不當,它甚至可能減慢其速度, 例如,當涉及大量磁盤I / O時,讓幾個作業執行緒按順序運行任務比一次執行10個執行緒快得多,
7 執行緒傳參
7.1 lambda運算式傳參
最方便的方法就是通過lambda運算式呼叫匿名方法,傳引數,
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 執行緒創建需要時間
string text = "t1";
Thread t1 = new Thread ( () => Console.WriteLine (text) );
text = "t2";
Thread t2 = new Thread ( () => Console.WriteLine (text) );
t1.Start();
t2.Start();
運行結果:
t2
t2
以上運行結果說明,在t1執行緒創建之前text被修改成了t2,
8 執行緒命名
每個執行緒都有名稱屬性,目的是為了更方便除錯,
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);
}
運行結果:
Hello from main
Hello from worker
9 前臺執行緒與后臺執行緒
Thread worker = new Thread(() => Console.ReadLine());
if (args.Length > 0) worker.IsBackground = true;
worker.Name = "backThread";
worker.Start();
Console.WriteLine("finish!");
前臺執行緒會隨著主執行緒視窗關閉而停止,后臺執行緒及時主執行緒視窗關閉自己獨立運行,
10 執行緒優先級
執行緒優先級決定了作業系統執行活動執行緒時間的長短,
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
有時候提高了執行緒的優先級,但卻仍然無法滿足一些實時的應用需求,這時候就需要提高行程的優先級,System.Diagnostics命名空間中的process行程類.
using (Process p = Process.GetCurrentProcess())
p.PriorityClass = ProcessPriorityClass.High;
實際上,ProcessPriorityClass.High比最高優先級低1個級別:Realtime, 將行程優先級設定為Realtime,可指示OS,您永遠不希望該行程將CPU時間浪費在另一個行程上, 如果您的程式進入意外的無限回圈,您甚至可能會發現作業系統已鎖定,只剩下電源按鈕可以拯救您! 因此,高通常是實時應用程式的最佳選擇,
如果您的實時應用程式具有用戶界面,則提高處理優先級將給螢屏更新帶來過多的CPU時間,從而減慢整個計算機的速度(尤其是在UI復雜的情況下), 降低主執行緒的優先級并提高行程的優先級可確保實時執行緒不會因螢屏重繪而被搶占,但不會解決使其他應用程式耗盡CPU時間的問題,因為作業系統仍會分配 整個程序的資源不成比例, 理想的解決方案是使實時作業程式和用戶界面作為具有不同行程優先級的單獨應用程式運行,并通過遠程處理或記憶體映射檔案進行通信, 記憶體映射檔案非常適合此任務, 我們將在C#4.0的第14和25章中簡要介紹它們的作業原理,
11 例外處理
Go無法補捉例外,GoCatch能捕獲當前執行緒的例外,輸出Console.WriteLine("exception.");由此可見,執行緒創建之后,例外只能由本執行緒捕獲,如果其呼叫方需要捕獲,則得用共享記憶體方式往上傳,Task幫我們做了這件事,呼叫方可在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.");
}
}
12 執行緒池
當你創建一個執行緒,幾百毫秒會被花費在例如創建本地私有變數堆疊,每個執行緒都會默認消耗1MB記憶體,從而允許在非常精細的級別上應用多執行緒而不會影響性能,當利用多核處理器以“分而治之”的方式并行執行計算密集型代碼時,這很有用,
執行緒池還限制了將同時運行的作業執行緒總數,活動執行緒過多會限制作業系統的管理負擔,并使CPU快取無效,一旦達到限制,作業將排隊并僅在另一個作業完成時才開始,這使任意并發的應用程式成為可能,例如Web服務器, (異步方法模式是一種先進的技術,通過高效利用池執行緒來進一步實作這一點;我們在C#4.0的第23章中簡要介紹了這一點),
有多種進入執行緒池的方法:
?通過Task Parallel Library(來自Framework 4.0)
?通過呼叫ThreadPool.QueueUserWorkItem
?通過異步委托(await)
?通過BackgroundWorker
以下方法間接使用執行緒池:
?WCF,遠程,ASP.NET和ASMX Web服務應用程式服務器
?System.Timers.Timer和System.Threading.Timer
?以Async結尾的框架方法,例如WebClient(基于事件的異步模式)上的框架方法和大多數BeginXXX方法(異步編程模型模式)
?PLINQ
使用池執行緒時,需要注意以下幾點:
?您無法設定池執行緒的名稱,這會使除錯更加困難(盡管您可以在Visual Studio的“執行緒”視窗中進行除錯時附加說明),
?池執行緒始終是后臺執行緒(這通常不是問題),
?除非您呼叫ThreadPool.SetMinThreads(請參閱優化執行緒池),否則阻塞執行緒池可能會在應用程式的早期階段觸發額外的延遲,
您可以自由更改池執行緒的優先級-將其釋放回池后將恢復為正常狀態,
您可以通過Thread.CurrentThread.IsThreadPoolThread屬性查詢當前是否在執行緒池上執行,
12.1 通過TPL進入執行緒池
通過Task Parallel Library庫中的Task類可輕松使用執行緒池,Task類由Framework 4.0引入,如果你熟悉老的結構,考慮用不帶泛型Task類來替代ThreadPool.QueueUserWorkItem,而泛型Task
使用不帶泛型例子的Task類,呼叫Task.Factory.StartNew,傳遞一個目標方法的委托;
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!";
}
輸出結果:
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!";
}
運行結果,在主執行緒中捕獲到了其他執行緒的例外:

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> 就是一個回傳值為string的異步委托,
12.2 不同過TPL進入執行緒池
如果你的框架是.Net 4.0之前的,你可以不通過Task Parallel Library 進入執行緒池,
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);
}
運行結果:
Hello from the thread pool!
Hello from the thread pool! 123
與Task不同:
- 后續執行中無法回傳執行結果;
- 無法回傳例外給呼叫者;
12.2.2 異步委托
即鄙人寫的這篇文章深入理解C#中的異步(一)——APM模式EAP模式里的2.1APM異步編程模式,
需要補充說明的是:
委托的EndInvoke 做了3件事:
- 阻塞等待;
- 回傳結果;
- 向呼叫者跑出例外;
2.1.3 為異步委托的回呼例子
12.3 執行緒池優化
執行緒池從其池中的一個執行緒開始, 分配任務后,池管理器會“注入”新執行緒以應對額外的并發作業負載(最大限制), 在足夠長時間的不活動之后,如果池管理器懷疑這樣做會導致更好的吞吐量,則可以“退出”執行緒,
您可以通過呼叫ThreadPool.SetMaxThreads;來設定池將創建的執行緒的上限; 默認值為:
?32位環境中的Framework 4.0中的1023
?64位環境中的Framework 4.0中的32768
?Framework 3.5中的每個內核250個
?Framework 2.0中每個內核25個
您還可以通過呼叫ThreadPool.SetMinThreads設定下限, 下限的作用是微妙的:這是一種高級優化技術,它指示池管理器在達到下限之前不要延遲執行緒的分配, 當存在阻塞的執行緒時,提高最小執行緒數可提高并發性,
默認的下限是每個處理器內核一個執行緒-允許全部CPU利用率的最小值, 但是,在服務器環境(例如IIS下的ASP .NET)上,下限通常要高得多-多達50個或更多,
設定執行緒池最小執行緒數量,
ThreadPool.SetMinThreads (50, 50);
13 代碼
本文代碼git下載
14 參考文章
Threading in C#
著作權宣告:本文為博主翻譯文章+自己理解,部分代碼自己寫,遵循 CC 4.0 BY-SA 著作權協議,轉載請附上原文出處鏈接和本宣告, 本文鏈接:https://www.cnblogs.com/JerryMouseLi/p/14135600.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/234532.html
標籤:.NET Core
