C# 線程相關(guān)知識總結(jié)
初識線程
線程是一個獨立的運行單元,每個進(jìn)程內(nèi)部都有多個線程,每個線程都可以各自同時執(zhí)行指令。每個線程都有自己獨立的棧,但是與進(jìn)程內(nèi)的其他線程共享內(nèi)存。但是對于.NET的客戶端程序(Console,WPF,WinForms)是由CLR創(chuàng)建的單線程(主線程,且只創(chuàng)建一個線程)來啟動。在該線程上可以創(chuàng)建其他線程。
圖:
線程工作方式
多線程由內(nèi)部線程調(diào)度程序管理,線程調(diào)度器通常是CLR委派給操作系統(tǒng)的函數(shù)。線程調(diào)度程序確保所有活動線程都被分配到合適的執(zhí)行時間,線程在等待或阻止時 (例如,在一個獨占鎖或用戶輸入) 不會消耗 CPU 時間。
在單處理器計算機(jī)上,線程調(diào)度程序是執(zhí)行時間切片 — 迅速切換每個活動線程。在 Windows 中, 一個時間片是通常數(shù)十毫秒為單位的區(qū)域 — — 相比來說 線程間相互切換比CPU更消耗資源。在多處理器計算機(jī)上,多線程用一種混合的時間切片和真正的并發(fā)性來實現(xiàn),不同的線程會在不同的cpu運行代碼。
創(chuàng)建線程
如:
using System; using System.Threading; class ThreadTest { static void Main() { Thread t = new Thread (Write2); // 創(chuàng)建線程t t.Start(); // 執(zhí)行 Write2() // 同時執(zhí)行主線程上的該方法 for (int i = 0; i < 1000; i++) Console.Write ("1"); } static void Write2() { for (int i = 0; i < 1000; i++) Console.Write ("2"); } }
輸出
111122221122221212122221212......
在主線程上創(chuàng)建了一個新的線程,該新線程執(zhí)行WrWrite2方法,在調(diào)用t.Start()時,主線程并行,輸出“1”。
圖:
線程Start()之后,線程的IsAlive屬性就為true,直到該線程結(jié)束(當(dāng)線程傳入的方法結(jié)束時,該線程就結(jié)束)。
CLR使每個線程都有自己獨立的內(nèi)存棧,所以每個線程的本地變量都相互獨立。
如:
static void Main() { new Thread (Go).Start(); // 創(chuàng)建一個新線程,并調(diào)用Go方法 Go(); // 在主線程上調(diào)用Go方法 } static void Go() { // 聲明一個本地局部變量 cycles for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N'); }
輸出
NNNNNNNNNN (共輸出10個N)
在新線程和主線程上調(diào)用Go方法時分別創(chuàng)建了變量cycles,這時cycles在不同的線程棧上,所以相互獨立不受影響。
圖:
如果不同線程指向同一個實例的引用,那么不同的線程共享該實例。
如:
class ThreadTest { //全局變量 int i; static void Main() { ThreadTest tt = new ThreadTest(); // 創(chuàng)建一個ThreadTest類的實例 new Thread (tt.Go).Start(); tt.Go(); } // Go方法屬于ThreadTest的實例 void Go() { if (i==1) { ++i; Console.WriteLine (i); } } }
輸出
2
新線程和主線程上調(diào)用了同一個實例的Go方法,所以變量i共享。
靜態(tài)變量也可以被多線程共享
class ThreadTest { static int i; // 靜態(tài)變量可以被線程共享 static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (i==1) { ++i; Console.WriteLine (i); } } }
輸出
2
如果將Go方法的代碼位置互換
static void Go() { if (i==1) { Console.WriteLine (i);++i;} }
輸出
1 1(有時輸出一個,有時輸出兩個)
如果新線程在Write之后,done=true之前,主線程也執(zhí)行到了write那么就會有兩個done。
不同線程在讀寫共享字段時會出現(xiàn)不可控的輸出,這就是多線程的線程安全問題。
解決方法: 使用排它鎖來解決這個問題--lock
class ThreadSafe { static bool done; static readonly object locker = new object(); static void Main() { new Thread (Go).Start(); Go(); } static void Go() { //使用lock,確保一次只有一個線程執(zhí)行該代碼 lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }
當(dāng)多個線程都在爭取這個排它鎖時,一個線程獲取該鎖,其他線程會處于blocked狀態(tài)(該狀態(tài)時不消耗cpu),等待另一個線程釋放鎖時,捕獲該鎖。這就保證了一次
只有一個線程執(zhí)行該代碼。
Join和Sleep
Join可以實現(xiàn)暫停另一個線程,直到調(diào)用Join方法的線程結(jié)束。
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"); }
輸出
yyyyyy..... Thread t has ended!
線程t調(diào)用Join方法,阻塞主線程,直到t線程執(zhí)行結(jié)束,再執(zhí)行主線程。
Sleep:暫停該線程一段時間
Thread.Sleep (TimeSpan.FromHours (1)); // 暫停一個小時 Thread.Sleep (500); // 暫停500毫秒 Join是暫停別的線程,Sleep是暫停自己線程。
上面的例子是使用Thread類的構(gòu)造函數(shù),給構(gòu)造函數(shù)傳入一個ThreadStart委托。來實現(xiàn)的。
public delegate void ThreadStart();
然后調(diào)用Start方法,來執(zhí)行該線程。委托執(zhí)行完該線程也結(jié)束。
如:
class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); // 執(zhí)行Go方法 Go(); // 同時在主線程上執(zhí)行Go方法 } static void Go() { Console.WriteLine ("hello!"); } }
多數(shù)情況下,可以不用new ThreadStart委托。直接在構(gòu)造函數(shù)里傳入void類型的方法。
Thread t = new Thread (Go);
使用lambda表達(dá)式
static void Main() { Thread t = new Thread ( () => Console.WriteLine ("Hello!") ); t.Start(); }
Foreground線程和Background線程
默認(rèn)情況下創(chuàng)建的線程都是Foreground,只要有一個Foregournd線程在執(zhí)行,應(yīng)用程序就不會關(guān)閉。
Background線程則不是。一旦Foreground線程執(zhí)行完,應(yīng)用程序結(jié)束,background就會強(qiáng)制結(jié)束。
可以用IsBackground來查看該線程是什么類型的線程。
線程異常捕獲
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // 不能捕獲異常 Console.WriteLine ("Exception!"); } } static void Go() { throw null; } //拋出 Null異常
此時并不能在Main方法里捕獲線程Go方法的異常,如果是Thread自身的異??梢圆东@。
正確捕獲方式:
public static void Main() { new Thread (Go).Start(); } static void Go() { try { // ... throw null; // 這個異常會被下面捕獲 // ... } catch (Exception ex) { // ... } }
線程池
當(dāng)創(chuàng)建一個線程時,就會消耗幾百毫秒cpu,創(chuàng)建一些新的私有局部變量棧。每個線程還消耗(默認(rèn))約1 MB的內(nèi)存。線程池通過共享和回收線程,允許在不影響性能的情況下啟用多線程。
每個.NET程序都有一個線程池,線程池維護(hù)著一定數(shù)量的工作線程,這些線程等待著執(zhí)行分配下來的任務(wù)。
線程池線程注意點:
1 線程池的線程不能設(shè)置名字(導(dǎo)致線程調(diào)試?yán)щy)。
2 線程池的線程都是background線程
3 阻塞一個線程池的線程,會導(dǎo)致延遲。
4 可以隨意設(shè)置線程池的優(yōu)先級,在回到線程池時改線程就會被重置。
通過Thread.CurrentThread.IsThreadPoolThread.可以查看該線程是否是線程池的線程。
使用線程池創(chuàng)建線程的方法:
- Task
- ThreadPool.QueueUserWorkItem
- Asynchronous delegates
- BackgroundWorker
TPL
Framework4.0下可以使用Task來創(chuàng)建線程池線程。調(diào)用Task.Factory.StartNew(),傳遞一個委托
- Task.Factory.StartNew
static void Main() { Task.Factory.StartNew (Go); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
Task.Factory.StartNew 返回一個Task對象??梢哉{(diào)用該Task對象的Wait來等待該線程結(jié)束,調(diào)用Wait時會阻塞調(diào)用者的線程。
- Task構(gòu)造函數(shù) 給Task構(gòu)造函數(shù)傳遞Action委托,或?qū)?yīng)的方法,調(diào)用start方法,啟動任務(wù)
- Task.Run 直接調(diào)用Task.Run傳入方法,執(zhí)行。
static void Main() { Task t=new Task(Go); t.Start(); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
static void Main() { Task.Run(() => Go()); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
QueueUserWorkItem
QueueUserWorkItem沒有返回值。使用 QueueUserWorkItem,只需傳遞相應(yīng)委托的方法就行。
static void Main() { //Go方法的參數(shù)data此時為空 ThreadPool.QueueUserWorkItem (Go); //Go方法的參數(shù)data此時為123 ThreadPool.QueueUserWorkItem (Go, 123); Console.ReadLine(); } static void Go (object data) { Console.WriteLine ("Hello from the thread pool! " + data); }
委托異步
委托異步可以返回任意類型個數(shù)的值。
使用委托異步的方式:
- 聲明一個和方法匹配的委托
- 調(diào)用該委托的BeginInvoke方法,獲取返回類型為IAsyncResult的值
- 調(diào)用EndInvoke方法傳遞IAsyncResulte類型的值獲取最終結(jié)果
如:
static void Main() { Func<string, int> method = Work; IAsyncResult cookie = method.BeginInvoke ("test", null, null); // // ... 此時可以同步處理其他事情 // int result = method.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); } static int Work (string s) { return s.Length; }
使用回調(diào)函數(shù)來簡化委托的異步調(diào)用,回調(diào)函數(shù)參數(shù)為IAsyncResult類型
static void Main() { Func<string, int> method = Work; method.BeginInvoke ("test", Done, method); // ... //并行其他事情 } static int Work (string s) { return s.Length; } static void Done (IAsyncResult cookie) { var target = (Func<string, int>) cookie.AsyncState; int result = target.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
使用匿名方法
Func<string, int> f = s => { return s.Length; }; f.BeginInvoke("hello", arg => { var target = (Func<string, int>)arg.AsyncState; int result = target.EndInvoke(arg); Console.WriteLine("String length is: " + result); }, f);
線程傳參和線程返回值
Thread
Thread構(gòu)造函數(shù)傳遞方法有兩種方式:
public delegate void ThreadStart(); public delegate void ParameterizedThreadStart (object obj);
所以Thread可以傳遞零個或一個參數(shù),但是沒有返回值。
- 使用lambda表達(dá)式直接傳入?yún)?shù)。
- 調(diào)用Start方法時傳入?yún)?shù)
static void Main() { Thread t = new Thread ( () => Print ("Hello from t!") ); t.Start(); } static void Print (string message) { Console.WriteLine (message); }
static void Main() { Thread t = new Thread (Print); t.Start ("Hello from t!"); } static void Print (object messageObj) { string message = (string) messageObj; Console.WriteLine (message); }
Lambda簡潔高效,但是在捕獲變量的時候要注意,捕獲的變量是否共享。
如:
for (int i = 0; i < 10; i++) new Thread (() => Console.Write (i)).Start();
輸出
0223447899
因為每次循環(huán)中的i都是同一個i,是共享變量,在輸出的過程中,i的值會發(fā)生變化。
解決方法-局部域變量
for (int i = 0; i < 10; i++) { int temp = i; new Thread (() => Console.Write (temp)).Start(); }
這時每個線程都指向新的域變量temp(此時每個線程都有屬于自己的花括號的域變量)在該線程中temp不受其他線程影響。
委托
委托可以有任意個傳入和輸出參數(shù)。以Action,F(xiàn)unc來舉例。
- Action 有零個或多個傳入?yún)?shù),但是沒有返回值。
- Func 有零個或多個傳入?yún)?shù),和一個返回值。
Func<string, int> method = Work; IAsyncResult cookie = method.BeginInvoke("test", null, null); // // ... 此時可以同步處理其他事情 // int result = method.EndInvoke(cookie); Console.WriteLine("String length is: " + result); int Work(string s) { return s.Length; }
使用回調(diào)函數(shù)獲取返回值
static void Main() { Func<string, int> method = Work; method.BeginInvoke ("test", Done, null); // ... //并行其他事情 } static int Work (string s) { return s.Length; } static void Done (IAsyncResult cookie) { var target = (Func<string, int>) cookie.AsyncState; int result = target.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
EndInvoke做了三件事情:
- 等待委托異步的結(jié)束。
- 獲取返回值。
- 拋出未處理異常給調(diào)用線程。
Task
Task泛型允許有返回值。
如:
static void Main() { // 創(chuàng)建Task并執(zhí)行 Task<string> task = Task.Factory.StartNew<string> ( () => DownloadString ("http://www.baidu.com") ); // 同時執(zhí)行其他方法 Console.WriteLine("begin"); //等待獲取返回值,并且不會阻塞主線程 Console.WriteLine(task.Result); Console.WriteLine("end"); } static string DownloadString (string uri) { using (var wc = new System.Net.WebClient()) return wc.DownloadString (uri); }
參考:
http://www.albahari.com/threading/
以上就是C# 線程相關(guān)知識總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于C# 線程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
WinForm實現(xiàn)最小化到系統(tǒng)托盤方法實例詳解
這篇文章主要介紹了WinForm實現(xiàn)最小化到系統(tǒng)托盤方法,實例分析了C#中實現(xiàn)WinForm最小化到系統(tǒng)托盤所需的相關(guān)控件與使用技巧,需要的朋友可以參考下2015-05-05C# 使用HttpClient上傳文件并附帶其他參數(shù)的步驟
這篇文章主要介紹了C# 使用HttpClient上傳文件并附帶其他參數(shù)的步驟,幫助大家更好的理解和使用c#,感興趣的朋友可以了解下2020-12-12C#設(shè)計模式之適配器模式與裝飾器模式的實現(xiàn)
創(chuàng)建型設(shè)計模式主要是為了解決創(chuàng)建對象的問題,而結(jié)構(gòu)型設(shè)計模式則是為了解決已有對象的使用問題。本文將用C#語言實現(xiàn)結(jié)構(gòu)型設(shè)計模式中的適配器模式與裝飾器模式,感興趣的可以了解一下2022-04-04C#實現(xiàn)用戶自定義控件中嵌入自己的圖標(biāo)
這篇文章主要介紹了C#實現(xiàn)用戶自定義控件中嵌入自己的圖標(biāo),較為詳細(xì)的分析了C#實現(xiàn)自定義控件中嵌入圖標(biāo)的具體步驟與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2016-03-03Unity實現(xiàn)粒子光效導(dǎo)出成png序列幀
這篇文章主要為大家詳細(xì)介紹了Unity實現(xiàn)粒子光效導(dǎo)出成png序列幀,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-03-03