【问题标题】:Named Lock Collection in C#?C# 中的命名锁集合?
【发布时间】:2014-12-30 17:05:54
【问题描述】:

我有多个线程将数据写入一个公共源,我希望两个线程在当且仅当它们接触同一条数据时才相互阻塞。

如果有办法专门锁定任意键就好了:

string id = GetNextId();
AquireLock(id);
try
{
    DoDangerousThing();
}
finally
{
    ReleaseLock(id);
}

如果没有其他人试图锁定同一个密钥,我希望他们能够同时运行。

我可以通过一个简单的互斥体字典来实现这一点,但我需要担心驱逐旧的、未使用的锁,如果集合变得太大,这可能会成为一个问题。

是否有这种类型的锁定模式的现有实现。

【问题讨论】:

标签: c# synchronization mutex


【解决方案1】:

您可以尝试使用ConcurrentDictionary<string, object> 来创建命名对象实例。当您需要一个新的锁实例(以前没有使用过)时,您可以将它添加到字典中(添加是通过GetOrAdd 进行的原子操作),然后所有线程可以共享同一个命名对象,一旦您从中提取它字典,基于您的数据。

例如:

// Create a global lock map for your lock instances.
public static ConcurrentDictionary<string, object> GlobalLockMap =
    new ConcurrentDictionary<string, object> ();

// ...

var oLockInstance = GlobalLockMap.GetOrAdd ( "lock name", x => new object () );

if (oLockInstance == null)
{
    // handle error
}

lock (oLockInstance)
{
    // do work
}

【讨论】:

  • 这基本上是我要写的答案,但我认为没有必要验证 oLockInstance 不为空。 GetOrAdd 永远不应返回 null,因为您始终从您提供的 lambda 返回一个有效对象。
  • @NSFW 我同意你的观点,它不应该——我只是添加了那一点作为最后一道防线。这些年来,我看到了奇怪的事情。 :)
  • 漂亮而简单。你能想出一个好方法来确保 GlobalMapLock 的大小得到控制吗?我可以有很多钥匙。
  • @captncraig 这可能是可能的,但我认为这会非常困难,因为一个锁可能一次由一个或多个线程使用 - 你可以从字典中删除它(线程保留在对象实例),但是如果一个新线程需要相同的实例,它现在会创建一个新线程,并且您将有多个并发线程锁定在独立的锁实例上。
  • @captncraig “很多钥匙”有多少?拥有很多很多的对象并不一定会占用大量内存 - 处理内存可能更容易。
【解决方案2】:

lock 关键字 (MSDN) 已经这样做了。

当你锁定时,你传递对象来锁定on

lock (myLockObject)
{
}

这使用带有特定对象的Monitor 类来同步同一对象上使用lock 的任何线程。

由于字符串字面量是“内部”的——也就是说,它们被缓存以供重用,因此每个具有相同值的字面量实际上都是同一个对象——你也可以对字符串这样做:

lock ("TestString")
{
}

由于您没有处理字符串文字,因此您可以按照以下说明对您读取的字符串进行实习:C#: Strings with same contents

如果使用的引用是从实习字符串(文字或显式实习)复制(直接或间接),它甚至可以工作。但我不会推荐它。这是非常脆弱的,并且可能导致难以调试的问题,因为可以很容易地创建与实习字符串具有相同值的字符串的新实例。

只有当其他东西进入同一对象上的锁定部分时,锁定才会阻塞。因此,无需保留字典,只需保留适用的锁定对象即可。

但实际上,您需要维护 ConcurrentDictionary 或类似名称以允许您的对象访问适当的锁定对象。

【讨论】:

  • 也许我的问题具有误导性。锁定键不是字符串文字,而是来自数据本身。
  • @captncraig 然后您需要创建代表该数据的对象并跟踪它们。鉴于字符串的实现,您可能只需锁定字符串本身就可以逃脱,但我不会指望它。
  • @captncraig 检查一下,除非你实习字符串,否则它肯定行不通。请参阅我链接的帖子。
  • @BradleyDotNET:仅当您使用一个内部字符串(通常只是文字)或从一个内部字符串分配的引用(直接或间接)时,锁定字符串才有效。监视器位于特定对象上,因此如果您将文字复制到其他字符串实例,将原件锁定在一个线程中,而将副本锁定在另一个线程中不会同步线程。因此,我强烈反对任何人将字符串文字用于lock 语句。它太容易损坏或被滥用。在您的第一个提案中坚持使用专用对象是最好的恕我直言。
  • @PeterDuniho 我试图这么说,但也许不清楚。我会尝试编辑,如果你有更好的方式来强调这一点,我很乐意接受你的编辑。
【解决方案3】:

您可以使用ConcurrentDictionary&lt;string, object&gt; 创建和重用不同的锁。如果您想从字典中删除锁,并在将来重新打开同名资源,您必须始终检查关键区域内部是否先前获取的锁已被其他线程删除或更改。并注意在离开临界区之前的最后步骤从字典中移除锁。

    static ConcurrentDictionary<string, object> _lockDict =
        new ConcurrentDictionary<string, object>();

    // VERSION 1: single-shot method

    public void UseAndCloseSpecificResource(string resourceId)
    {
        bool isSameLock;
        object lockObj, lockObjCheck;
        do
        {
            lock (lockObj = _lockDict.GetOrAdd(resourceId, new object()))
            {
                if (isSameLock = (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                                  object.ReferenceEquals(lockObj, lockObjCheck)))
                {
                    // ... open, use, and close resource identified by resourceId ...

                    // This must be the LAST statement
                    _lockDict.TryRemove(resourceId, out lockObjCheck);
                }
            }
        }
        while (!isSameLock);
    }

    // VERSION 2: separated "use" and "close" methods
    //            (can coexist with version 1)

    public void UseSpecificResource(string resourceId)
    {
        bool isSameLock;
        object lockObj, lockObjCheck;
        do
        {
            lock (lockObj = _lockDict.GetOrAdd(resourceId, new object()))
            {
                if (isSameLock = (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                                  object.ReferenceEquals(lockObj, lockObjCheck)))
                {
                    // ... open and use (or reuse) resource identified by resourceId ...
                }
            }
        }
        while (!isSameLock);
    }

    public bool TryCloseSpecificResource(string resourceId)
    {
        bool result = false;
        object lockObj, lockObjCheck;
        if (_lockDict.TryGetValue(resourceId, out lockObj))
        {
            lock (lockObj)
            {
                if (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                    object.ReferenceEquals(lockObj, lockObjCheck))
                {
                    result = true;
                    // ... close resource identified by resourceId ...

                    // This must be the LAST statement
                    _lockDict.TryRemove(resourceId, out lockObjCheck);
                }
            }
        }
        return result;
    }

【讨论】:

  • 这太复杂了。 ConcurrentDictionary 的唯一问题是它不能保证传递给 GetOrAdd 的值工厂只会被调用一次。使用惰性作为字典项可以解决问题(一些轻量级惰性实例将被丢弃而不生成值)。
  • 这是对字符串键进行锁定的唯一实现,似乎可以安全地清除 ConcurrentDictionary 中的锁定对象和字符串。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-12-20
  • 2014-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多