深入分析C# 線程同步
上一篇介紹了如何開啟線程,線程間相互傳遞參數,及線程中本地變量和全局共享變量區(qū)別。
本篇主要說明線程同步。
如果有多個線程同時訪問共享數據的時候,就必須要用線程同步,防止共享數據被破壞。如果多個線程不會同時訪問共享數據,可以不用線程同步。
線程同步也會有一些問題存在:
- 性能損耗。獲取,釋放鎖,線程上下文建切換都是耗性能的。
- 同步會使線程排隊等待執(zhí)行。
線程同步的幾種方法:
阻塞
當線程調用Sleep,Join,EndInvoke,線程就處于阻塞狀態(tài)(Sleep使調用線程阻塞,Join、EndInvoke使另外一個線程阻塞),會立即從cpu退出。(阻塞狀態(tài)的線程不消耗cpu)
當線程在阻塞和非阻塞狀態(tài)間切換時會消耗幾毫秒時間。
//Join
static void Main()
{
Thread t = new Thread (Go);
Console.WriteLine ("Main方法已經運行....");
t.Start();
t.Join();//阻塞Main方法
Console.WriteLine ("Main方法解除阻塞,繼續(xù)運行...");
}
static void Go()
{
Console.WriteLine ("在t線程上運行Go方法...");
}
//Sleep
static void Main()
{
Console.WriteLine ("Main方法已經運行....");
Thread.CurrentThread.Sleep(3000);//阻塞當前線程
Console.WriteLine ("Main方法解除阻塞,繼續(xù)運行...");
}
//Task
static void Main()
{
Task Task1=Task.Run(() => {
Console.WriteLine("task方法執(zhí)行...");
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關鍵字
鎖是否必須是靜態(tài)類型?
如果被鎖定的方法是靜態(tài)的,那么這個鎖必須是靜態(tài)類型。這樣就是在全局鎖定了該方法,不管該類有多少個實例,都要排隊執(zhí)行。
如果被鎖定的方法不是靜態(tài)的,那么不能使用靜態(tài)類型的鎖,因為被鎖定的方法是屬于實例的,只要該實例調用鎖定方法不產生損壞就可以,不同實例間是不需要鎖的。這個鎖只鎖該實例的方法,而不是鎖所有實例的方法.*
class ThreadSafe
{
private static object _locker = new object();
void Go()
{
lock (_locker)
{
......//共享數據的操作 (Static Method),使用靜態(tài)鎖確保所有實例排隊執(zhí)行
}
}
private object _locker2=new object();
void GoTo()
{
lock(_locker2)
//共享數據的操作,非靜態(tài)方法,是用非靜態(tài)鎖,確保同一個實例的方法調用者排隊執(zhí)行
}
}
同步對象可以兼作它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)
互斥鎖是一個互斥的同步對象,同一時間有且僅有一個線程可以獲取它??梢詫崿F進程級別上線程的同步。
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()
{
//是否需要創(chuàng)建一個應用
Boolean isCreateNew = false;
try
{
m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew);
}
catch (Exception ex)
{
}
return isCreateNew;
}
}
互斥鎖的帶有三個參數的構造函數
- initiallyOwned: 如果initiallyOwned為true,互斥鎖的初始狀態(tài)就是被所實例化的線程所獲取,否則實例化的線程處于未獲取狀態(tài)。
- name:該互斥鎖的名字,在操作系統中只有一個命名為name的互斥鎖mutex,如果一個線程得到這個name的互斥鎖,其他線程就無法得到這個互斥鎖了,必須等待那個線程對這個線程釋放。
- createNew:如果指定名稱的互斥體已經存在就返回false,否則返回true。
信號和句柄
lock和mutex可以實現線程同步,確保一次只有一個線程執(zhí)行。但是線程間的通信就不能實現。如果線程需要相互通信的話就要使用AutoResetEvent,ManualResetEvent,通過信號來相互通信。它們都有兩個狀態(tài),終止狀態(tài)和非終止狀態(tài)。只有處于非終止狀態(tài)時,線程才可以阻塞。
AutoResetEvent:
AutoResetEvent 構造函數可以傳入一個bool類型的參數,false表示將AutoResetEvent對象的初始狀態(tài)設置為非終止。如果為true標識終止狀態(tài),那么WaitOne方法就不會再阻塞線程了。但是因為該類會自動的將終止狀態(tài)修改為非終止,所以,之后再調用WaitOne方法就會被阻塞。
WaitOne 方法如果AutoResetEvent對象狀態(tài)非終止,則阻塞調用該方法的線程。可以指定時間,若沒有獲取到信號,返回false
set 方法釋放被阻塞的線程。但是一次只可以釋放一個被阻塞的線程。
class ThreadSafe
{
static AutoResetEvent autoEvent;
static void Main()
{
//使AutoResetEvent處于非終止狀態(tài)
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線程信號,繼續(xù)執(zhí)行...");
}
}
輸出
主線程運行...
主線程sleep 1秒...
t線程運行DoWork方法,阻塞自己等待main線程信號...
主線程釋放信號...
t線程DoWork方法獲取到main線程信號,繼續(xù)執(zhí)行...
ManualResetEvent
ManualResetEvent和AutoResetEvent用法類似。
AutoResetEvent在調用了Set方法后,會自動的將信號由釋放(終止)改為阻塞(非終止),一次只有一個線程會得到釋放信號。而ManualResetEvent在調用Set方法后不會自動的將信號由釋放(終止)改為阻塞(非終止),而是一直保持釋放信號,使得一次有多個被阻塞線程運行,只能手動的調用Reset方法,將信號由釋放(終止)改為阻塞(非終止),之后的再調用Wait.One方法的線程才會被再次阻塞。
public class ThreadSafe
{
//創(chuàng)建一個處于非終止狀態(tài)的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處于終止狀態(tài)時,調用由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處于非阻塞狀態(tài),此時調用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處于終止狀態(tài)時,調用由Wait.One方法的多線程,不會被阻塞。
//Thread_3 運行并調用WaitOne()
//Thread_4 運行并調用WaitOne()
//Thread_4 結束
//Thread_3 結束
///調用Reset方法,ManualResetEvent處于非阻塞狀態(tài),此時調用Wait.One方法的線程再次被阻塞
//Thread_5 運行并調用WaitOne()
//調用Set方法,釋放阻塞線程
//Thread_5 結束
Interlocked
如果一個變量被多個線程修改,讀取。可以用Interlocked。
計算機上不能保證對一個數據的增刪是原子性的,因為對數據的操作也是分步驟的:
- 將實例變量中的值加載到寄存器中。
- 增加或減少該值。
- 在實例變量中存儲該值。
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允許同一時間只有一個線程執(zhí)行。而ReaderWriterLock允許同一時間有多個線程可以執(zhí)行讀操作,或者只有一個有排它鎖的線程執(zhí)行寫操作。
class Program
{
// 創(chuàng)建一個對象
public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
static void Main(string[] args)
{
//創(chuàng)建一個線程讀取數據
Thread t1 = new Thread(Write);
// t1.Start(1);
Thread t2 = new Thread(Write);
//t2.Start(2);
// 創(chuàng)建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# 線程同步的資料請關注腳本之家其它相關文章!

