More Effective C# 42.避免在鎖定的區段呼叫不明的程式碼 More Effective C# 42.避免在鎖定的區段呼叫不明的程式碼(Avoid Calling Unknown Code in Locked Sections)

這個做法講解了死鎖是怎麼造成的並且該怎麼做才能避免死鎖。

當一條執行緒 A 在等待執行緒 B 完成工作,這時執行緒 B 也同時在等待執行緒 A 完成工作,這種情況就會造成死鎖, 不過除此之外還有其他可能會造成死鎖,那就是在 lock 範圍內加入了許多不相干的程式碼,假如它們牽涉到另一條執行緒,那也是有可能造成死鎖的。

假設下面這段程式碼是一個 WinForm 程式,我們在做法 39 有提到過要更新 UI 必須使用當初建立 UI 的執行緒才允許更新,所以才要透過 Control.Invoke 將委派交給 UI 執行緒運行,但是在運行 RaiseProgress?.Invoke 之前就已經把 syncHandle 鎖定起來了,代表目前 取得鎖的是背景執行緒,但是運行 RaiseProgress?.Invoke 之後取得 progressCounter 還需要再次取得 syncHandle,現在是 UI 執行緒 想要取得鎖,但是鎖已經被背景執行緒拿去了,最後就造成鎖死。

void Main()
{
	var worker = new WorkerClass();
	worker.RaiseProgress += engine_RaiseProgress;
	worker.DoWork();
}
static void engine_RaiseProgress(object sender, EventArgs e)
{
	WorkerClass engine = sender as WorkerClass;
	if (engine != null)
		Console.WriteLine(engine.Progress);
}

public class WorkerClass
{
	public event EventHandler<EventArgs> RaiseProgress;
	private object syncHandle = new object();
	public void DoWork()
	{
		for (int count = 0; count < 100; count++)
		{
			lock (syncHandle)
			{
				System.Threading.Thread.Sleep(100);
				progressCounter++;
				RaiseProgress?.Invoke(this, EventArgs.Empty);
			}
		}
	}
	private int progressCounter = 0;
	public int Progress
	{
		get
		{
			lock (syncHandle)
				return progressCounter;
		}
	}
}

要修改這個問題也很簡單,因為問題就是背景執行緒與 UI 執行緒都要用到鎖,所以把關鍵的 Control.Invoke 的程式移出鎖定範圍, 這樣就能讓背景執行緒提早離開 lock 語句並釋放掉鎖,等到 UI 執行緒要用到鎖的時候就不會衝突了。

從這個例子可以了解 lock 並不是越多越好,太多的 lock 只會導致死鎖的機率增加,並且 lock 的鎖定範圍也不能太大,不要將 不相干的程式碼都加入倒 lock 範圍內部,這樣只會造成查問題的麻煩。


Summary

這個做法的關鍵就是要縮小 lock 的鎖定範圍,並且在處理事件的時候要額外注意,因為很容易觸發意料之外的邏輯導致死鎖。