【问题标题】:Is it OK to use a string as a lock object?使用字符串作为锁对象可以吗?
【发布时间】:2012-10-09 17:01:31
【问题描述】:

我需要根据一组有限的字符串在一个区域中创建一个关键部分。我希望为同一个字符串实例共享锁(有点类似于String.Intern 方法)。

我正在考虑以下实现:

public class Foo
{
    private readonly string _s;
    private static readonly HashSet<string> _locks = new HashSet<string>();

    public Foo(string s)
    {
        _s = s;
        _locks.Add(s);
    }

    public void LockMethod()
    {
        lock(_locks.Single(l => l == _s))
        {
            ...
        }
    }
}

这种方法有什么问题吗?以这种方式锁定字符串对象是否可以,使用HashSet&lt;string&gt;时是否存在线程安全问题?

例如,创建一个Dictionary&lt;string, object&gt; 为每个字符串实例创建一个新的锁对象会更好吗?


最终实现

根据我采用以下实现的建议:

public class Foo
{
    private readonly string _s;
    private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>();

    public Foo(string s)
    {
        _s = s;
    }

    public void LockMethod()
    {
        lock(_locks.GetOrAdd(_s, _ => new object()))
        {
            ...
        }
    }
}

【问题讨论】:

  • 我在我的回答 here 中指出了使用 string 作为锁定目标的一些怪癖。
  • 我可以建议稍微改变一下吗? lock(_locks.GetOrAdd(_s, s => new object())) 这样一来,您只需为每个字符串创建一个对象,而不是每次调用 LockMethod() 时都创建一个。
  • 关于JonSkeetBrianGideon 都提到的不相等字符串实例的问题,我建议使用lock(_locks.GetOrAdd(String.Intern(_s), s =&gt; new object())),虽然实习可能仍然通过CompilationRelaxations.NoStringInterning 关闭,然后这赢了也不行。也许,使用字符串的hashcode 作为字典中的键会有所帮助?!
  • @Oliver 实习在您锁定字符串时会出现问题(其他人可以锁定相同的实习字符串实例)。这里我们锁定一个新的对象实例,而不是字符串实例。没有其他人可以访问这个新对象实例。
  • 如何安全删除keyed lock对象?

标签: c# .net multithreading .net-4.0 locking


【解决方案1】:

不鼓励锁定字符串,主要原因是(由于字符串实习)一些其他代码可能会在您不知道的情况下锁定同一个字符串实例。造成死锁情况的可能性。

现在,在大多数具体情况下,这可能是一个牵强附会的场景。这更像是图书馆的一般规则。

但另一方面,字符串的感知好处是什么?

所以,点对点:

这种方法有什么问题吗?

是的,但主要是理论上的。

这样锁定字符串对象可以吗,使用HashSet有线程安全问题吗?

只要线程只并发读取,HashSet&lt;&gt; 不涉及线程安全。

例如,创建一个为每个字符串实例创建一个新锁对象的 Dictionary 是否更好?

是的。只是为了安全起见。在大型系统中,避免死锁的主要目的是使锁定对象尽可能地保持本地和私有。只有有限数量的代码应该能够访问它们。

【讨论】:

  • 接受,因为我认为这个答案最全面地解决了具体问题。
  • 对于未来的读者,我发现锁定字符串的原因是可能存在我们不想锁定所有线程而是锁定由此类字符串标识的一组相关线程的情况。例如:从缓存中获取项目,如果未找到则从 API 请求它。我希望只向 API 发出一个请求来检索该项目,而所有其他相关线程都在等待它可用。同时,我不想阻止其他线程请求其他项目。所以,我锁定了项目键。我在刷新 OAuth 访问令牌时使用了相同的方法。
  • 我建议这个答案更深入 - stackoverflow.com/a/19375402/9350845
【解决方案2】:

我会说这是一个非常糟糕的主意,就个人而言。这不是字符串的用途。

(我个人不喜欢每个对象首先都有一个监视器这一事实,但这是一个稍微不同的问题。)

如果您想要一个表示可以在不同实例之间共享的锁的对象,为什么不为此创建一个特定的类型呢?您可以轻松地为锁命名以用于诊断目的,但锁定实际上不是字符串的目的。像这样的:

public sealed class Lock
{
    private readonly string name;

    public string Name { get { return name; } }

    public Lock(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException("name");
        }
        this.name = name;
    }
}

考虑到字符串是有时有时不是(通过简单的检查有时很难辨别的方式),你很容易得到不小心在你不想要的地方共享了锁。

【讨论】:

  • 谢谢...用静态的ConcurrentDictionary&lt;string, object&gt; 代替为每个字符串创建一个新的锁对象的HashSet&lt;string&gt; 是一个好方法吗?
  • @ZaidMasud:你仍然有一组静态(全局)锁,这对我来说听起来不是一个好的设计。我很少发现可变状态的静态最终成为一件好事。
  • @JonSkeet 我建议在答案中使用建议的Lock 类型的代码示例。
  • @ZaidMasud:这里有两个完全不同的问题:a)全局锁集合(如果可能,请避免); b)锁定什么(不是字符串;对象可以,锁定会更好)
  • @Alex:不,我真的不这么认为——你基本上有一个锁的全局命名空间......你怎么知道其他代码会使用什么?是的,实习可以确保您获得该名称的锁定,但是确保只有适当的代码使用它的严格性在哪里?
【解决方案3】:

锁定字符串可能会有问题,因为实习字符串本质上是全局的。

Interned 字符串是每个进程的,因此它们甚至在不同的 AppDomain 之间共享。类型对象也是如此(所以不要锁定 typeof(x))。

【讨论】:

  • 有时每个进程的锁变量就是你要找的;p
  • @leppie 当然可以,但字符串/类型对象可能并不明显具有此“功能”。
  • 顺便说一句,字符串实习似乎是only an issue for literal strings。因此,如果字符串没有被创建为文字,那么在我看来,整个问题有点夸大了。
  • 默认情况下会保留文字字符串,但您可以根据需要保留任何字符串。这里的要点是,您无法通过简单地查看字符串引用的用法来判断字符串是否被实习。 IMO 锁定字符串没有任何好处,并且考虑到在某些情况下它可能会导致难以发现问题,最好避免它。
  • 兴趣,是在某处描述还是有人测试过?
【解决方案4】:

不久前我遇到了一个类似的问题,我正在寻找一种基于字符串值锁定一段代码的好方法。这是我们目前所拥有的,它解决了字符串的问题,并具有我们想要的粒度。

主要思想是使用字符串键维护同步对象的静态ConcurrentDictionary。当一个线程进入该方法时,它会立即建立一个锁并尝试将同步对象添加到并发字典中。如果我们可以添加到并发字典中,这意味着没有其他线程拥有基于我们的字符串键的锁,我们可以继续我们的工作。否则,我们将使用并发字典中的同步对象来建立第二个锁,它将等待正在运行的线程完成处理。当第二个锁被释放时,我们可以尝试再次将当前线程的同步对象添加到字典中。

请注意:线程没有排队 - 因此,如果具有相同字符串键的多个线程同时竞争锁,则无法保证它们的处理顺序。

如果您认为我忽略了某些内容,请随时批评。

public class Foo
{
    private static ConcurrentDictionary<string, object> _lockDictionary = new ConcurrentDictionary<string, object>();

    public void DoSomethingThreadCriticalByString(string lockString)
    {
        object thisThreadSyncObject = new object();

        lock (thisThreadSyncObject)
        {
            try
            {
                for (; ; )
                {
                   object runningThreadSyncObject = _lockDictionary.GetOrAdd(lockString, thisThreadSyncObject);
                   if (runningThreadSyncObject == thisThreadSyncObject)
                       break;

                    lock (runningThreadSyncObject)
                    {
                        // Wait for the currently processing thread to finish and try inserting into the dictionary again.
                    }
                }

                // Do your work here.

            }
            finally
            {
                // Remove the key from the lock dictionary
                object dummy;
                _lockDictionary.TryRemove(lockString, out dummy);
            }
        }
    }
}

【讨论】:

  • 删除锁对象看起来不对,如果第二个线程正在持有并等待这个特定的锁对象,然后第三个步骤进入,字典中没有对象,那么第三个线程将为同一个键添加一个新对象,并持有与第二个线程不同的锁对象,那么互斥就会被打破。
  • 删除锁对象是必要的,否则其他线程将无法执行。锁对象被移除后,外层锁将被释放。然后任何等待内部锁的线程将继续执行。将其锁对象添加到字典的第一个线程将继续,其余的将再次等待内部锁。
猜你喜欢
  • 2018-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-27
  • 2013-11-10
  • 2016-02-08
  • 1970-01-01
相关资源
最近更新 更多