這個做法建議使用 synchronization primitives 之前應該要想清楚,不要整個應用程式都是同步化的語法,最後造成死鎖的機率增加。
我們寫程式可以透過 private 宣告一個成員,讓這個成員只能在某個特定區域才可以修改狀態,同時也能避免修改這個成員狀態的程式碼散落到 應用程式的各個角落,在處理並行程式碼也要有類似概念來限制提供同步處理的物件。
在上個做法有提到 lock(this)
這個寫法是很不好的,相同概念的寫法還有 lock(typeof(MyType))
,它們的問題都是公開的範圍太大。
導致任何外部程式碼都可以鎖定這些對象。
下面這個例子就有可能在未來跨執行緒操作時發生死鎖,並且很難找到問題。
void Main()
{
LockingExample x = new LockingExample();
lock (x)
{
x.MyMethod();
}
}
public class LockingExample
{
public void MyMethod()
{
lock (this)
{
}
}
}
public class LockingExample1
{
public void MyMethod()
{
lock (typeof(LockingExample1))
{
}
}
}
要解決這個問題最簡單的方式就是建立私有的物件來獲得鎖。
private object syncHandle = new object();
public void IncrementTotal()
{
lock (syncHandle)
{
// code elided
}
}
也可以透過 Interlocked.CompareExchange
來延遲建立物件提升效率,這個做法因為是原子性的所以能夠確保只會初始化一次。
private object syncHandle;
private object GetSyncHandle()
{
System.Threading.Interlocked.CompareExchange(ref syncHandle, new object(), null);
return syncHandle;
}
public void AnotherMethod()
{
lock (GetSyncHandle())
{
// ... code elided
}
}
對於靜態的方法使用相同的技巧也是有效的,只需要建立靜態的物件即可。
private static readonly object staticSyncHandle = new object();
public static void StaticMethod()
{
lock (staticSyncHandle)
{
// 靜態方法的同步邏輯
}
}
在使用 lock 語句的時候不一定要把整段方法都包在 lock 裡面,反而是只需要把必要的程式碼包起來就好,這樣執行效率會比較高也可以降低死鎖的風險。
public void YetAnotherMethod()
{
DoStuffThatIsNotSynchronized();
int val = RetrieveValue();
lock (GetSyncHandle())
{
// ... code elided
}
DoSomeFinalStuff();
}
最後如果發現一個類別需要使用多個同步物件來保護不同的資源,那可能是你的類別責任設計的過於複雜,這種情況應該要將類別拆分的更小一點。
Summary
這個做法在討論不要去鎖定那些會對外公開的物件,應該是要在物件內建立私有欄位當成鎖是比較好的,鎖定的程式碼也要盡量減少, 這樣可以加快程式運行的效率,也可以降低死鎖的風險。