使用線程時最頭痛的就是共享資源的同步問題,處理不好會得到錯誤的結果,C#處理共享資源有以下幾種:
1、lock鎖
需要注意的地方:
1).lock不能鎖定空值某一對象可以指向Null,但Null是不需要被釋放的。(請參考:認識全面的null)
2).lock不能鎖定string類型,雖然它也是引用類型的。因為字符串類型被CLR“暫留”
這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中 的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。因此,最好鎖定不會被暫留的私有或受保護成員。
3).lock鎖定的對象是一個程序塊的內存邊界
4).值類型不能被lock,因為前文標紅字的“對象被釋放”
,值類型不是引用類型的
5).lock就避免鎖定public 類型或不受程序控制的對象。
應用場景:經常會應用於防止多線程操作導致公用變量值出現不確定的異常,用於確保操作的安全性
2、
互斥鎖(Mutex)
互斥鎖是一個互斥的同步對象,意味着同一時間有且僅有一個線程可以獲取它。
互斥鎖可適用於一個共享資源每次只能被一個線程訪問的情況
我們可以把Mutex看作一個出租車,乘客看作線程。乘客首先等車,然后上車,最后下車。當一個乘客在車上時,其他乘客就只有等他下車以后才可以上車。而 線程與C# Mutex對象的關系也正是如此,線程使用Mutex.WaitOne()方法等待C# Mutex對象被釋放,如果它等待的C# Mutex對象被釋放了,它就自動擁有這個對象,直到它調用Mutex.ReleaseMutex()方法釋放這個對象,而在此期間,其他想要獲取這個 C# Mutex對象的線程都只有等待。
如果要獲取一個互斥鎖。應調用互斥鎖上的WaitOne()方法,該方法繼承於Thread.WaitHandle類
它處於等到狀態直至所調用互斥鎖可以被獲取,因此該方法將組織住主調線程直到指定的互斥鎖可用,如果不需要擁有互斥鎖,用ReleaseMutex方法釋放,從而使互斥鎖可以被另外一個線程所獲取.
3、semaphore
其中lock 和mutex 差不多,都是鎖定同一個資源,不同之處mutex在整個進程中都可以訪問到。
而semaphore是鎖定多個資源,比如同一時期只能有兩個線程訪問,其它線程只能等待其中之一釋放鎖才能使用,Semaphore就是一個可以多次進入的“Mutex”。Mutex永遠只允許一個線程擁有它,而Semaphore可以允許多個線程請求,因此Semaphore被用於管理一次可以允許多個線程進入並發訪問資源的情況。
下面是一個簡單的例子,:
class Program { static Semaphore sp = new Semaphore(2,2); static void Main(string[] args) { DoWork(); Console.Read(); } private static void DoWork() { for (int i = 0; i < 10; i++) { Task.Run(() => { sp.WaitOne(); Console.WriteLine("線程:"+Thread.CurrentThread.ManagedThreadId+",開始運行"); Thread.Sleep(new Random().Next(1000)); Console.WriteLine("線程:" + Thread.CurrentThread.ManagedThreadId + ",結束此運行"); sp.Release(); }); } } }
另舉一個復雜一些的例子:學生都去圖書館查資料,圖書館共有3台電腦,如果來的人超過3人則需要排隊等待,此例子中還要注意一點,那個學生選擇那台電腦,學生找空閑電腦用Mutex鎖定電腦對象,否則定位的電腦可能是錯誤的(可能會出現多名同學使用同一台電腦的情況,使用mutex鎖定資源,這樣才能確保一台空閑電腦只能是一名學生選擇)
class Program { //圖書館擁有的公用計算機 private const int ComputerNum = 3; private static Computer[] LibraryComputers; //同步信號量 public static Semaphore sp = new Semaphore(ComputerNum, ComputerNum); static void Main(string[] args) { //圖書館擁有ComputerNum台電腦 LibraryComputers = new Computer[ComputerNum]; for (int i = 0; i < ComputerNum; i++) LibraryComputers[i] = new Computer("Computer" + (i + 1).ToString()); int peopleNum = 0; Random ran = new Random(); Thread user; System.Console.WriteLine("敲任意鍵模擬一批批的人排隊使用{0}台計算機,ESC鍵結束模擬……", ComputerNum); //每次創建若干個線程,模擬人排隊使用計算機 while (System.Console.ReadKey().Key != ConsoleKey.Escape) { peopleNum = ran.Next(0, 10); System.Console.WriteLine("\n有{0}人在等待使用計算機。", peopleNum); Task[] ts = new Task[peopleNum]; for (int i = 0; i <peopleNum; i++) { int n = i+1; ts[i]=Task.Run(() => { UseComputer("User" + n.ToString()); }); } Task.WaitAll(ts); Console.WriteLine("All threads finished!"); } } static Mutex m = new Mutex(); //線程函數 static void UseComputer(Object UserName) { sp.WaitOne();//等待計算機可用 //查找可用的計算機 Computer cp = null; m.WaitOne(); for (int i = 0; i < ComputerNum; i++) { if (LibraryComputers[i].IsOccupied == false) { LibraryComputers[i].IsOccupied = true; cp = LibraryComputers[i]; break; } } m.ReleaseMutex(); //使用計算機工作 cp.Use(UserName.ToString()); //不再使用計算機,讓出來給其他人使用 sp.Release(); } } class Computer { public readonly string ComputerName = ""; public Computer(string Name) { ComputerName = Name; } //是否被占用 public bool IsOccupied = false; //人在使用計算機 public void Use(String userName) { System.Console.WriteLine("{0}開始使用計算機{1}", userName, ComputerName); Thread.Sleep(new Random().Next(2000, 6000)); //隨機休眠,以模擬人使用計算機 System.Console.WriteLine("{0}結束使用計算機{1}", userName, ComputerName); IsOccupied = false; } }
4.
AutoResetEvent 允許線程通過發信號互相通信。通常,此通信涉及線程需要獨占訪問的資源。
線程通過調用 AutoResetEvent 上的 WaitOne 來等待信號。如果 AutoResetEvent 處於非終止狀態,則該線程阻塞,並等待當前控制資源的線程
通過調用 Set 發出資源可用的信號。
調用 Set 向 AutoResetEvent 發信號以釋放等待線程。AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,然后自動返回非終止狀態。如果沒有任何線程在等待,則狀態將無限期地保持為終止狀態。
可以通過將一個布爾值傳遞給構造函數來控制 AutoResetEvent 的初始狀態,如果初始狀態為終止狀態,則為 true;否則為 false。
舉例:面試時,每次叫一個,只能一個人進去。
class Program { static AutoResetEvent are = new AutoResetEvent(false); static void Main(string[] args) { //10個人排隊,叫一聲,進一個 for (int i = 0; i < 10; i++) { Task.Run(() => { are.WaitOne(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId+"進門"); Thread.Sleep(new Random().Next(1000)); Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "出門"); }); } System.Console.WriteLine("G:放行,ESC:退出\n"); Action action = new Action(() => { are.Set(); }) ; while (true) { ConsoleKey key = Console.ReadKey(true).Key; if (key == ConsoleKey.G) action(); if (key == ConsoleKey.Escape) { break; } } } }
5、ManualResetEvent
ManualResetEvent就像一個信號燈,可以利用它的信號,控制當前線程是掛起狀態還是運行狀態。
它有幾個常用的方法:Reset(),Set(),WaitOne();
初始化該對象時,可以指定其默認的狀態(有信號/無信號);
在初始化以后,該對象將保持原來的狀態不變,直到它的Reset()或者Set()方法被調用;
Reset()方法將其設置為無信號狀態,Set()方法將其設置為有信號狀態;
WaitOne()方法在無信號狀態下,可以使當前線程掛起;注意這里說的是當前線程;
直到調用了Set()方法,該線程才被激活。 在多線程的代碼里,可以使用一個ManualResetEvent對象來控制線程所有線程;
只要在調用WaitOne()方法前,調用Reset()方法,因為WaitOne()控制的是當前線程;
但是這樣做,ManualResetEvent對象的管理邏輯會變得復雜;
所以這里建議一條線程一個ManualResetEvent對象。
舉例://模擬3輛汽車過紅綠燈
class Program { static ManualResetEvent mre = new ManualResetEvent(false); static void Main(string[] args) { //模擬3輛汽車過紅綠燈 for (int i = 0; i < 3; i++) { Task.Run(() => { int count = 0; while (true) { mre.WaitOne(); Thread.Sleep(new Random().Next(5000)); count++; Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "第{0}次開始運行", count); } }); } Action stop = delegate() { mre.Reset(); Console.WriteLine("紅燈"); }; Action go = delegate() { mre.Set(); Console.WriteLine("綠燈"); }; System.Console.WriteLine("G:綠燈,R:紅燈\n"); while (true) { var k = Console.ReadKey(true).Key; if (k == ConsoleKey.G) { go(); } else if (k == ConsoleKey.R) { stop(); } else { System.Console.WriteLine("G:綠燈,R:紅燈\n"); } } } }