反模式和陷阱

鎖定堆疊分配/本地變數

使用 lock 時的一個謬誤是在函式中使用本地物件作為鎖定器。由於這些本地物件例項在每次呼叫函式時都會有所不同,因此 lock 將無法按預期執行。

List<string> stringList = new List<string>();

public void AddToListNotThreadSafe(string something)
{
    // DO NOT do this, as each call to this method 
    // will lock on a different instance of an Object.
    // This provides no thread safety, it only degrades performance.
    var localLock = new Object();
    lock(localLock)
    {
        stringList.Add(something);
    }
}

// Define object that can be used for thread safety in the AddToList method
readonly object classLock = new object();

public void AddToList(List<string> stringList, string something)
{
    // USE THE classLock instance field to achieve a 
    // thread-safe lock before adding to stringList
    lock(classLock)
    {
        stringList.Add(something);
    }
}

假設鎖定限制了對同步物件本身的訪問

如果一個執行緒呼叫:lock(obj) 和另一個執行緒呼叫 obj.ToString() 第二個執行緒不會被阻止。

object obj = new Object();
 
public void SomeMethod()
{
     lock(obj)
    {
       //do dangerous stuff 
    }
 }

 //Meanwhile on other tread 
 public void SomeOtherMethod()
 {
   var objInString = obj.ToString(); //this does not block
 }

期望子類知道何時鎖定

有時候基類的設計使得他們的子類在訪問某些受保護的欄位時需要使用鎖:

public abstract class Base
{
    protected readonly object padlock;
    protected readonly List<string> list;

    public Base()
    {
        this.padlock = new object();
        this.list = new List<string>();
    }

    public abstract void Do();
}

public class Derived1 : Base
{
    public override void Do()
    {
        lock (this.padlock)
        {
            this.list.Add("Derived1");
        }
    }
}

public class Derived2 : Base
{
    public override void Do()
    {
        this.list.Add("Derived2"); // OOPS! I forgot to lock!
    }
}

使用模板方法 封裝鎖定更安全 :

public abstract class Base
{
    private readonly object padlock; // This is now private
    protected readonly List<string> list;

    public Base()
    {
        this.padlock = new object();
        this.list = new List<string>();
    }

    public void Do()
    {
        lock (this.padlock) {
            this.DoInternal();
        }
    }

    protected abstract void DoInternal();
}

public class Derived1 : Base
{
    protected override void DoInternal()
    {
        this.list.Add("Derived1"); // Yay! No need to lock
    }
}

鎖定盒裝 ValueType 變數不會同步

在下面的示例中,私有變數被隱式裝箱,因為它作為函式的 object 引數提供,期望監視器資源鎖定。裝箱恰好在呼叫 IncInSync 函式之前發生,因此每次呼叫函式時,裝箱例項對應於不同的堆物件。

public int Count { get; private set; }

private readonly int counterLock = 1;

public void Inc()
{
    IncInSync(counterLock);
}

private void IncInSync(object monitorResource)
{
    lock (monitorResource)
    {
        Count++;
    }
}

拳擊發生在 Inc 函式中:

BulemicCounter.Inc:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery+BulemicCounter.counterLock
IL_0008:  box         System.Int32**
IL_000D:  call        UserQuery+BulemicCounter.IncInSync
IL_0012:  nop         
IL_0013:  ret         

這並不意味著盒裝的 ValueType 根本不能用於監視器鎖定:

private readonly object counterLock = 1;

現在裝箱發生在建構函式中,這對鎖定很好:

IL_0001:  ldc.i4.1    
IL_0002:  box         System.Int32
IL_0007:  stfld       UserQuery+BulemicCounter.counterLock

當存在更安全的替代方案時,不必要地使用鎖

一個非常常見的模式是線上程安全類中使用私有 ListDictionary 並在每次訪問時鎖定:

public class Cache
{
    private readonly object padlock;
    private readonly Dictionary<string, object> values;

    public WordStats()
    {
        this.padlock = new object();
        this.values = new Dictionary<string, object>();
    }
    
    public void Add(string key, object value)
    {
        lock (this.padlock)
        {
            this.values.Add(key, value);
        }
    }

    /* rest of class omitted */
}

如果有多個方法訪問 values 字典,程式碼可能會變得很長,更重要的是,鎖定一直會模糊其意圖。鎖定也很容易忘記,缺乏正確的鎖定可能導致很難發現錯誤。

通過使用 ConcurrentDictionary ,我們可以完全避免鎖定:

public class Cache
{
    private readonly ConcurrentDictionary<string, object> values;

    public WordStats()
    {
        this.values = new ConcurrentDictionary<string, object>();
    }
    
    public void Add(string key, object value)
    {
        this.values.Add(key, value);
    }

    /* rest of class omitted */
}

使用併發集合還可以提高效能,因為它們在某種程度上都採用了無鎖技術