C# Lock关键字与字符串不按预期工作

C# Lock关键字与字符串不按预期工作,c#,.net,locking,C#,.net,Locking,我有一些代码,可以使用给定的标记在数据库中创建新行,或者如果现有行已经存在,则返回现有行: public static RunSet ProvisionRun(ApplicationDbContext context, IIdentity identity, string token, CrewType crewType) { var manager = new UserManager<ApplicationUser>(new UserStore<ApplicationU

我有一些代码,可以使用给定的
标记在数据库中创建新行,或者如果现有行已经存在,则返回现有行:

public static RunSet ProvisionRun(ApplicationDbContext context, IIdentity identity, string token, CrewType crewType)
{
   var manager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(context));
   var user = manager.FindById(identity.GetUserId());
   var runSet = user.RunSets.SingleOrDefault(r => r.RunName == token);

   if (runSet != null) // Run set already created, they must be uploading new files to this run; return existing run
      return runSet;

   // Create the run
   var newRun = new RunSet
   {
      Created = DateTime.Now,
      CrewType = crewType.ToString(),
      RunName = token,
      InputFiles = new List<RunFile>()
   };

   user.RunSets.Add(newRun);
   context.SaveChanges();

   return newRun;
}
这修复了这个bug。但是,没有两个用户会传入相同的
令牌
,因此我真正想做的是阻止同一个用户调用该方法,但允许两个不同的用户同时调用该方法。我应该能够锁定用户的身份:

lock (identity.Name) // This is a string
{
   // Same stuff here

   return newRun;
}
然而,这不起作用。我已经在调试器中使用了它,实际上它只是直接通过
,即使
标识.Name在两个线程中都是相同的:

我有点倾向于相信
lock
关键字在精确的引用等式上起作用,这是有意义的。然而,通过挖掘MSDN上的信息,它清楚地指出:

我假设框架有一个散列锁,并将检查对象是否相等。这也可能是因为文档有误导性,因为它使用的是一个字符串常量,可能会被插入


不管怎样,很明显我对锁的理解有一些差距。有人要填这些吗?谢谢

运行时字符串不保存在intern池中,因此即使它们具有相同的值,也会有不同的引用。但是,预编译的字符串保存在intern池中,并且在值相同时具有相同的引用。我倾向于相信这就是为什么你的线程不“共享同一个锁”,相同的
Name
s值有不同的引用

我构建了一个简单的类,可以在类似您的情况下使用:

public class NamedLock : IDisposable
{
    public NamedLock(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException();
        }
        mutex = new Mutex(false, name);
        mutex.WaitOne();
    }

    public void Dispose()
    {
        if (mutex != null)
        {
            mutex.ReleaseMutex();
            mutex = null;
        }
    }

    private Mutex mutex;
}
用法如下:

using(new NamedLock(identity.Name))
{
    // do some stuff
}
但要注意“命名互斥体”:

因为它们是系统范围的,所以可以使用命名互斥体跨进程边界协调资源使用


关于您的问题,我还猜测您的字符串具有不同的引用,并且lock语句处理的是引用相等而不是值相等,这对我来说很有意义。

您实际寻找的是原子数据库操作,而不是线程同步

即使您使
按您的需要工作,它也只能在一个进程内工作。如果将应用程序扩展到多个服务器,问题就会再次出现

这些代码也会让其他开发人员感到困惑。没有人希望使用
lock
来同步对数据库表的访问

不幸的是,简单地将当前代码包装到事务中也不起作用。按照现在的写法,它

  • 从表中选择一行
  • 如果找不到行,请插入新行
  • 如果使用低于serializable的任何事务隔离级别,则不会阻止2个事务同时插入新行

    如果您使用serializable,您将获得死锁,因为两个事务将获得
    选择
    的共享锁,并且它们都将尝试获得
    插入
    的独占锁

    要解决此问题,整个操作应按以下步骤执行:

  • 如果新行不存在,请插入新行
  • 选择行

  • 在EF中,最接近1的方法。是
    AddOrUpdate
    ,但“更新”部分不是您想要的。似乎唯一的选择是存储过程。

    创建一个字典并使用它来保持对象的同步

    static Dictionary<string, object> lockMe
    
    if(!lockMe.ContainsKey(identity.Name)) lockMe.Add(identity.Name,new Object());
    
    lock(lockMe[identity.Name])
    {}
    
    静态字典锁
    如果(!lockMe.ContainsKey(identity.Name))lockMe.Add(identity.Name,new Object());
    锁(lockMe[identity.Name])
    {}
    

    编辑:您需要同步字典填充,所以请将if Add放入一个lock语句。

    不是说锁定字符串,但为什么不自己锁定标识呢?我试过了,它的行为与锁定字符串相同。永远不要锁定字符串!你不仅会有引用身份问题,还会做锁反转噩梦。谢谢@EricLippert!您能否提供仅锁定该用户的建议?使用命名互斥锁是一种好方法,还是太过了?@shay\uuuux:假设您锁定“a”,然后在仍然锁定的情况下,等待“B”。同时,在库中的同一个过程中,您甚至没有编写代码,也不知道“B”上的锁,然后尝试获取“a”上的锁。现在你陷入了僵局。只锁定您完全控制的私有对象。这样,在你不知情的情况下,其他任何人都无法锁定该对象。请记住,正确使用共享锁需要整个过程中的每一行代码都由理解锁策略的人编写。这是并发很难实现的众多原因之一;然而,我在任何地方都没有发现
    lock
    关键字对引用相等有效的文档。从中可以看出,它对引用相等有效:lock关键字通过获得给定对象的互斥锁,将语句块标记为关键部分。因为两个不同的对象可以有相同的值。Eric Lippert发现了锁的核心细节。不用说,它最明确地基于对象的精确引用。我最初的直觉是正确的。命名互斥锁肯定是一个解决方案,但它相当沉重,我也有自己的问题。绝对锁使用引用标识。永远不要锁定字符串或装箱值。@MikeChristensen您可以锁定
    string.Intern(identity.Name)
    ,以确保始终使用相同的引用,但我不确定这可能会导致什么样的性能损失。或者,您可以在将名称分配给该属性时对其进行内部搜索。在您的示例中,您应该使用ConcurrentDictionary,但如果有数百万或数十亿用户(因此是键),这并不十分实用。根据我的经验,ConcurrentDictionary速度很慢。数以百万计的用户意味着大量的兆字节,数十亿是不现实的。1)
    ConcurrentDictionary
    static Dictionary<string, object> lockMe
    
    if(!lockMe.ContainsKey(identity.Name)) lockMe.Add(identity.Name,new Object());
    
    lock(lockMe[identity.Name])
    {}