More Effective C# 41.鎖定 Handles 使用最小可能的範圍 More Effective C# 41.鎖定 Handles 使用最小可能的範圍(Use the Smallest Possible Scope for Lock Handles)

這個做法建議使用 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

這個做法在討論不要去鎖定那些會對外公開的物件,應該是要在物件內建立私有欄位當成鎖是比較好的,鎖定的程式碼也要盡量減少, 這樣可以加快程式運行的效率,也可以降低死鎖的風險。