線程(一)——概念代碼實踐
線程(一)——概念代碼實踐
摘要
線程中的概念很多,如果沒有代碼示例來理解,會比較晦澀,而且有些概念落不到實處,因此,本文以一些運行示例代碼,結果來闡述線程中的一些基礎概念。讓自己跟讀者一起把線程中的概念理解地更深刻。
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 ("//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件事:
- 阻塞等待;
- 返回結果;
- 向調用者跑出異常;
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 代碼
14 參考文章
版權聲明:本文為博主翻譯文章+自己理解,部分代碼自己寫,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。 本文鏈接://www.cnblogs.com/JerryMouseLi/p/14135600.html