反模式和陷阱

锁定堆栈分配/本地变量

使用 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 */
}

使用并发集合还可以提高性能,因为它们在某种程度上都采用了无锁技术