使用锁的最佳实践

13

假设我在某个类中有以下属性,其目的是用作锁。

protected object SyncRoot { get; private set; }

无论如何,不管这是如何设置的,如果它确实被设置了,最佳做法是如何使用它?

由于锁不能与空对象一起使用,我应该像这样处理吗?

lock (SyncRoot ?? new object())
    SomeMethod();

或者我应该像这样检查 null 吗?

if (SyncRoot != null)
    lock (SyncRoot)
        SomeMethod();
else
    SomeMethod();
如果它确实被设置了,我想要使用它进行锁定。否则,我不关心它。第一个解决方案是否存在任何低效或冗余的问题?
编辑:所有这些答案都很好。然而,我只能选择一个。根据我与Luke讨论的情况,我的SyncRoot没有理由为空。在单线程环境中使用锁的开销不是什么大问题,但在多线程环境中则是必要的。(四位回答者升级了投票)感谢你们快速的回复。
6个回答

23

通常我使用私有成员变量而不是属性,例如

private static object MyLock = new object();

这种方式可以确保它始终被初始化。

您也可以使用非静态版本,例如

private readonly object MyLock = new object();

在我的父类中,根据某些条件设置了这个属性。如果条件为真,我打算在派生类中使用这个变量或属性。 你认为lock(SyncRoot ?? new object())会带来太多的开销吗? - Beljoda
@Beljoda,锁对象只是一个new object()--为什么你会有不初始化的情况呢?(为什么不在声明时就初始化它,从而默认消除这个问题呢?) - Kirk Woll
@Luke,这是为我的管理器类而设计的,它与我接口到一个脚本语言。如果我最初决定不自动编译脚本,则此锁对象为空。如果我决定设置为自动编译,我使用此锁定,以便在重新定义方法时不会调用中间方法。 - Beljoda
@Beljoda,但是为什么不总是加锁呢?你是担心开销吗?说实话,锁定的开销很少会提供对这种额外复杂性的防御,因此这是一种罕见的情况。 - Kirk Woll
@Luke,嗯...你是对的。当这个管理器类上有多个线程运行的机会时(因为自动编译代码包括由事件生成的多个线程),就会设置此锁对象。如果未设置自动编译,则在此管理器类内部,只有一个线程会运行此类中的代码。 而锁的开销并不是非常大的阻碍。 我可能也会每次都锁定。 - Beljoda
1
@Beljoda lock(SyncRoot ?? new object()) 不是一个很好的选择,当你在 'new object()' 上加锁时,它根本不会起到任何作用。锁只有在对象被多个线程共享时才能真正发挥作用,在这种情况下,该对象根本不会被共享。 - undefined

14

同步

SyncRoot ?? new object()

如果SyncRootnull,那么每个线程每次都会得到一个新的对象,因此毫无意义。在单独的对象上同步没有效果:线程将立即继续执行,因为没有其他人可能在同一个new对象上同步。

您应该在构造函数中初始化SyncRoot,在第一个线程尝试获取锁之前进行初始化。


将同步到新对象是否会带来任何后果,如速度受阻?进行翻译。 - Beljoda
2
@Beljoda,对于在new object()上同步的性能影响已经无关紧要了,因为它是一个完全无用的习惯用法。 - Kirk Woll
2
速度并不重要;如果通过您的代码的每个线程都创建一个新的锁对象,则您的代码无法保护受保护资源免受多个线程访问。为了使线程排队并逐一访问受保护的资源,它们必须全部锁定在同一个对象实例上。 - dthorpe
1
@Beljoda 在新对象上同步等同于根本没有同步:一个线程获取锁并且另一个线程等待的唯一原因是它们查看相同的对象。如果每个线程获取一个新对象进行同步,则不会发生这种情况。 - Sergey Kalinichenko
@dasblinkenlight,好的。所以,无论我的类在单线程还是多线程应用程序中使用,我都应该使用您的解决方案。 谢谢。 - Beljoda

4

第一个问题是,它不会导致任何良好的同步:

lock (SyncRoot ?? new object())
    SomeMethod();

原因是如果您创建一个新对象并将其分配给SyncRoot,它将被放置在堆上,但没有对它的引用。因此,当另一个线程到来时,它将找不到它... 它变得完全无用,并且它不会阻止对关键部分的任何访问。
第二种方法可以工作,尽管我真的不明白为什么只有在可用时才使用锁定。

3

来自文档: https://msdn.microsoft.com/zh-cn/library/c5kehkcz.aspx

通常情况下,避免锁定公共类型或您代码控制之外的实例。常见的构造 lock (this)、lock (typeof(MyType)) 和 lock ("myLock") 违反了此指南:

  • 如果可以公开访问该实例,则 lock (this) 会出现问题。
  • 如果 MyType 是公开可访问的,则 lock (typeof(MyType)) 会出现问题。
  • 因为进程中使用相同字符串的任何其他代码都将共享相同的锁,所以 lock("myLock") 会出现问题。

最佳实践是定义一个私有对象进行锁定,或者定义一个私有静态对象变量以保护所有实例的公共数据。

示例:

 class Account
{
    decimal balance;
    private Object thisLock = new Object();

    public void Withdraw(decimal amount)
    {
        lock (thisLock)
        {
            if (amount > balance)
            {
                throw new Exception("Insufficient funds");
            }
            balance -= amount;
        }
    }
}

2
您最好的选择是在任何锁对象的使用者有机会使用它之前,始终初始化锁对象。始终分配锁对象的成本很小,在没有线程争用时获取锁的成本也很小。
因此,在您的代码中添加锁定/不锁定检查将使您的代码复杂度加倍,并可能引入微妙的线程错误,但可能不会产生任何实际的性能收益。
简化您的代码:始终获取锁。

1
如果需要线程安全地访问队列、集合和字典之类的对象,我会锁定该对象本身,而不需要单独使用锁定对象。最好将队列、集合或字典设置为私有或只读,以确保始终使用相同的对象。
示例:
class Processor
{
    readonly Queue<int> processQueue = new Queue<int>();

    public void AddToQueue(int index)
    {
        lock (processQueue)
        {
            processQueue.Enqueue(index);
        }
    }
}

或者你可以使用 ConcurrentQueue<T> - Baccata

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接