1. 程式人生 > 程式設計 >深入分析C# 執行緒同步

深入分析C# 執行緒同步

上一篇介紹瞭如何開啟執行緒,執行緒間相互傳遞引數,及執行緒中本地變數和全域性共享變數區別。

本篇主要說明執行緒同步。

如果有多個執行緒同時訪問共享資料的時候,就必須要用執行緒同步,防止共享資料被破壞。如果多個執行緒不會同時訪問共享資料,可以不用執行緒同步。

執行緒同步也會有一些問題存在:

  1. 效能損耗。獲取,釋放鎖,執行緒上下文建切換都是耗效能的。
  2. 同步會使執行緒排隊等待執行。

執行緒同步的幾種方法:

阻塞

當執行緒呼叫Sleep,Join,EndInvoke,執行緒就處於阻塞狀態(Sleep使呼叫執行緒阻塞,Join、EndInvoke使另外一個執行緒阻塞),會立即從cpu退出。(阻塞狀態的執行緒不消耗cpu)

當執行緒在阻塞和非阻塞狀態間切換時會消耗幾毫秒時間。

//Join
static void Main()
{
 Thread t = new Thread (Go);
 Console.WriteLine ("Main方法已經執行...."); 
 t.Start();
 t.Join();//阻塞Main方法
 Console.WriteLine ("Main方法解除阻塞,繼續執行...");
}
 
static void Go()
{
 Console.WriteLine ("在t執行緒上執行Go方法..."); 
}

//Sleep
static void Main()
{
 Console.WriteLine ("Main方法已經執行...."); 
 Thread.CurrentThread.Sleep(3000);//阻塞當前執行緒
 Console.WriteLine ("Main方法解除阻塞,繼續執行...");
}
 
 //Task
 static void Main()
{
 Task Task1=Task.Run(() => { 
  Console.WriteLine("task方法執行..."); 
  Thread.Sleep(1000);
  }); 
 Console.WriteLine(Task1.IsCompleted);  
 Task1.Wait();//阻塞主執行緒 ,等該Task1完成
 Console.WriteLine(Task1.IsCompleted); 
}

加鎖(lock)

加鎖使多個執行緒同一時間只有一個執行緒可以呼叫該方法,其他執行緒被阻塞。

同步物件的選擇:

  • 使用引用型別,值型別加鎖時會裝箱,產生一個新的物件。
  • 使用private修飾,使用public時易產生死鎖。(使用lock(this),lock(typeof(例項))時,該類也應該是private)。
  • string不能作為鎖物件。
  • 不能在lock中使用await關鍵字

鎖是否必須是靜態型別?

如果被鎖定的方法是靜態的,那麼這個鎖必須是靜態型別。這樣就是在全域性鎖定了該方法,不管該類有多少個例項,都要排隊執行。

如果被鎖定的方法不是靜態的,那麼不能使用靜態型別的鎖,因為被鎖定的方法是屬於例項的,只要該例項呼叫鎖定方法不產生損壞就可以,不同例項間是不需要鎖的。這個鎖只鎖該例項的方法,而不是鎖所有例項的方法.*

class ThreadSafe
{
 private static object _locker = new object();
 
 void Go()
 {
 lock (_locker)
 {
 ......//共享資料的操作 (Static Method),使用靜態鎖確保所有例項排隊執行
 }
 }

private object _locker2=new object();
 void GoTo()
 {
 lock(_locker2)
 //共享資料的操作,非靜態方法,是用非靜態鎖,確保同一個例項的方法呼叫者排隊執行
 }
}

同步物件可以兼作它lock的物件

如:

class ThreadSafe
{
 private List <string> _list = new List <string>(); 
 void Test()
 {
 lock (_list)
 {
 _list.Add ("Item 1");
 }
 }
}

Monitors

lock其實是Monitors的簡潔寫法。

lock (x) 
{ 
 DoSomething(); 
} 

兩者其實是一樣的。

System.Object obj = (System.Object)x; 
System.Threading.Monitor.Enter(obj); 
try 
{ 
 DoSomething(); 
} 
finally 
{ 
 System.Threading.Monitor.Exit(obj); 
} 

互斥鎖(Mutex)

互斥鎖是一個互斥的同步物件,同一時間有且僅有一個執行緒可以獲取它。可以實現程序級別上執行緒的同步。

class Program
 {
 //例項化一個互斥鎖
 public static Mutex mutex = new Mutex();

 static void Main(string[] args)
 {
  for (int i = 0; i < 3; i++)
  {
  //在不同的執行緒中呼叫受互斥鎖保護的方法
  Thread test = new Thread(MutexMethod);
  test.Start();
  }
  Console.Read();
 }

 public static void MutexMethod()
 {
  Console.WriteLine("{0} 請求獲取互斥鎖",Thread.CurrentThread.Name);
  mut.WaitOne();
  Console.WriteLine("{0} 已獲取到互斥鎖",Thread.CurrentThread.Name); 
  Thread.Sleep(1000);
  Console.WriteLine("{0} 準備釋放互斥鎖",Thread.CurrentThread.Name);
  // 釋放互斥鎖
  mut.ReleaseMutex();
  Console.WriteLine("{0} 已經釋放互斥鎖",Thread.CurrentThread.Name);
 }
 }

互斥鎖可以在不同的程序間實現執行緒同步

使用互斥鎖實現一個一次只能啟動一個應用程式的功能。

 public static class SingleInstance
 {
 private static Mutex m;

 public static bool IsSingleInstance()
 {
  //是否需要建立一個應用
  Boolean isCreateNew = false;
  try
  {
  m = new Mutex(initiallyOwned: true,name: "SingleInstanceMutex",createdNew: out isCreateNew);
  }
  catch (Exception ex)
  {
  
  }
  return isCreateNew;
 }
 }

互斥鎖的帶有三個引數的建構函式

  1. initiallyOwned: 如果initiallyOwned為true,互斥鎖的初始狀態就是被所例項化的執行緒所獲取,否則例項化的執行緒處於未獲取狀態。
  2. name:該互斥鎖的名字,在作業系統中只有一個命名為name的互斥鎖mutex,如果一個執行緒得到這個name的互斥鎖,其他執行緒就無法得到這個互斥鎖了,必須等待那個執行緒對這個執行緒釋放。
  3. createNew:如果指定名稱的互斥體已經存在就返回false,否則返回true。

訊號和控制代碼

lockmutex可以實現執行緒同步,確保一次只有一個執行緒執行。但是執行緒間的通訊就不能實現。如果執行緒需要相互通訊的話就要使用AutoResetEvent,ManualResetEvent,通過訊號來相互通訊。它們都有兩個狀態,終止狀態和非終止狀態。只有處於非終止狀態時,執行緒才可以阻塞。

AutoResetEvent:

AutoResetEvent 建構函式可以傳入一個bool型別的引數,false表示將AutoResetEvent物件的初始狀態設定為非終止。如果為true標識終止狀態,那麼WaitOne方法就不會再阻塞執行緒了。但是因為該類會自動的將終止狀態修改為非終止,所以,之後再呼叫WaitOne方法就會被阻塞。

WaitOne 方法如果AutoResetEvent物件狀態非終止,則阻塞呼叫該方法的執行緒。可以指定時間,若沒有獲取到訊號,返回false

set 方法釋放被阻塞的執行緒。但是一次只可以釋放一個被阻塞的執行緒。

class ThreadSafe 
{ 
 static AutoResetEvent autoEvent; 

 static void Main() 
 { 
 //使AutoResetEvent處於非終止狀態
 autoEvent = new AutoResetEvent(false); 

 Console.WriteLine("主執行緒執行..."); 
 Thread t = new Thread(DoWork); 
 t.Start(); 

 Console.WriteLine("主執行緒sleep 1秒..."); 
 Thread.Sleep(1000); 

 Console.WriteLine("主執行緒釋放訊號..."); 
 autoEvent.Set(); 
 } 

 static void DoWork() 
 { 
 Console.WriteLine(" t執行緒執行DoWork方法,阻塞自己等待main執行緒訊號..."); 
 autoEvent.WaitOne(); 
 Console.WriteLine(" t執行緒DoWork方法獲取到main執行緒訊號,繼續執行..."); 
 } 

} 

輸出

主執行緒執行...
主執行緒sleep 1秒...
t執行緒執行DoWork方法,阻塞自己等待main執行緒訊號...
主執行緒釋放訊號...
t執行緒DoWork方法獲取到main執行緒訊號,繼續執行...

ManualResetEvent

ManualResetEventAutoResetEvent用法類似。

AutoResetEvent在呼叫了Set方法後,會自動的將訊號由釋放(終止)改為阻塞(非終止),一次只有一個執行緒會得到釋放訊號。而ManualResetEvent在呼叫Set方法後不會自動的將訊號由釋放(終止)改為阻塞(非終止),而是一直保持釋放訊號,使得一次有多個被阻塞執行緒執行,只能手動的呼叫Reset方法,將訊號由釋放(終止)改為阻塞(非終止),之後的再呼叫Wait.One方法的執行緒才會被再次阻塞。

public class ThreadSafe
{
 //建立一個處於非終止狀態的ManualResetEvent
 private static ManualResetEvent mre = new ManualResetEvent(false);

 static void Main()
 {
 for(int i = 0; i <= 2; i++)
 {
  Thread t = new Thread(ThreadProc);
  t.Name = "Thread_" + i;
  t.Start();
 }

 Thread.Sleep(500);
 Console.WriteLine("\n新執行緒的方法已經啟動,且被阻塞,呼叫Set釋放阻塞執行緒");

 mre.Set();

 Thread.Sleep(500);
 Console.WriteLine("\n當ManualResetEvent處於終止狀態時,呼叫由Wait.One方法的多執行緒,不會被阻塞。");

 for(int i = 3; i <= 4; i++)
 {
  Thread t = new Thread(ThreadProc);
  t.Name = "Thread_" + i;
  t.Start();
 }

 Thread.Sleep(500);
 Console.WriteLine("\n呼叫Reset方法,ManualResetEvent處於非阻塞狀態,此時呼叫Wait.One方法的執行緒再次被阻塞");
 

 mre.Reset();

 Thread t5 = new Thread(ThreadProc);
 t5.Name = "Thread_5";
 t5.Start();

 Thread.Sleep(500);
 Console.WriteLine("\n呼叫Set方法,釋放阻塞執行緒");

 mre.Set();
 }


 private static void ThreadProc()
 {
 string name = Thread.CurrentThread.Name;

 Console.WriteLine(name + " 執行並呼叫WaitOne()");

 mre.WaitOne();

 Console.WriteLine(name + " 結束");
 }
}


//Thread_2 執行並呼叫WaitOne()
//Thread_1 執行並呼叫WaitOne()
//Thread_0 執行並呼叫WaitOne()

//新執行緒的方法已經啟動,且被阻塞,呼叫Set釋放阻塞執行緒

//Thread_2 結束
//Thread_1 結束
//Thread_0 結束

//當ManualResetEvent處於終止狀態時,呼叫由Wait.One方法的多執行緒,不會被阻塞。

//Thread_3 執行並呼叫WaitOne()
//Thread_4 執行並呼叫WaitOne()

//Thread_4 結束
//Thread_3 結束

///呼叫Reset方法,ManualResetEvent處於非阻塞狀態,此時呼叫Wait.One方法的執行緒再次被阻塞

//Thread_5 執行並呼叫WaitOne()
//呼叫Set方法,釋放阻塞執行緒
//Thread_5 結束

Interlocked

如果一個變數被多個執行緒修改,讀取。可以用Interlocked

計算機上不能保證對一個數據的增刪是原子性的,因為對資料的操作也是分步驟的:

  1. 將例項變數中的值載入到暫存器中。
  2. 增加或減少該值。
  3. 在例項變數中儲存該值。

Interlocked為多執行緒共享的變數提供原子操作。
Interlocked提供了需要原子操作的方法:

  • public static int Add (ref int location1,int value); 兩個引數相加,且把結果和賦值該第一個引數。
  • public static int Increment (ref int location); 自增。
  • public static int CompareExchange (ref int location1,int value,int comparand);

location1 和comparand比較,被value替換.

value 如果第一個引數和第三個引數相等,那麼就把value賦值給第一個引數。

comparand 和第一個引數對比。

ReaderWriterLock

如果要確保一個資源或資料在被訪問之前是最新的。那麼就可以使用ReaderWriterLock.該鎖確保在對資源獲取賦值或更新時,只有它自己可以訪問這些資源,其他執行緒都不可以訪問。即排它鎖。但用改鎖讀取這些資料時,不能實現排它鎖。

lock允許同一時間只有一個執行緒執行。而ReaderWriterLock允許同一時間有多個執行緒可以執行讀操作,或者只有一個有排它鎖的執行緒執行寫操作。

 class Program
 {
 // 建立一個物件
 public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
 static void Main(string[] args)
 {
  //建立一個執行緒讀取資料
  Thread t1 = new Thread(Write);
  // t1.Start(1);
  Thread t2 = new Thread(Write);
  //t2.Start(2);
  // 建立10個執行緒讀取資料
  for (int i = 3; i < 6; i++)
  {
  Thread t = new Thread(Read);
  // t.Start(i);
  }

  Console.Read();

 }

 // 寫入方法
 public static void Write(object i)
 {
  // 獲取寫入鎖,20毫秒超時。
  Console.WriteLine("執行緒:" + i + "準備寫...");
  readerwritelock.AcquireWriterLock(Timeout.Infinite);
  Console.WriteLine("執行緒:" + i + " 寫操作" + DateTime.Now);
  // 釋放寫入鎖
  Console.WriteLine("執行緒:" + i + "寫結束...");
  Thread.Sleep(1000);
  readerwritelock.ReleaseWriterLock();

 }

 // 讀取方法
 public static void Read(object i)
 {
  Console.WriteLine("執行緒:" + i + "準備讀...");

  // 獲取讀取鎖,20毫秒超時
  readerwritelock.AcquireReaderLock(Timeout.Infinite);
  Console.WriteLine("執行緒:" + i + " 讀操作" + DateTime.Now);
  // 釋放讀取鎖
  Console.WriteLine("執行緒:" + i + "讀結束...");
  Thread.Sleep(1000);

  readerwritelock.ReleaseReaderLock();

 }
 }
//分別遮蔽writer和reader方法。可以更清晰的看到 writer被阻塞了。而reader沒有被阻塞。

//遮蔽reader方法
//執行緒:1準備寫...
//執行緒:1 寫操作2017/7/5 17:50:01
//執行緒:1寫結束...
//執行緒:2準備寫...
//執行緒:2 寫操作2017/7/5 17:50:02
//執行緒:2寫結束...

//遮蔽writer方法
//執行緒:3準備讀...
//執行緒:5準備讀...
//執行緒:4準備讀...
//執行緒:5 讀操作2017/7/5 17:50:54
//執行緒:5讀結束...
//執行緒:3 讀操作2017/7/5 17:50:54
//執行緒:3讀結束...
//執行緒:4 讀操作2017/7/5 17:50:54
//執行緒:4讀結束...

參考:

  • MSDN
  • 《CLR via C#》

以上就是深入分析C# 執行緒同步的詳細內容,更多關於c# 執行緒同步的資料請關注我們其它相關文章!