【问题标题】:LINQ to SQL DAL, is this thread safe?LINQ to SQL DAL,这个线程安全吗?
【发布时间】:2011-04-03 16:09:49
【问题描述】:

我的代码是用 C# 编写的,数据层使用 LINQ to SQL 填充/加载分离的对象类。

我最近更改了代码以使用多线程,我很确定我的 DAL 不是线程安全的。

你能告诉我 PopCall() 和 Count() 是否是线程安全的,如果不是,我该如何修复它们?

public class DAL
{
   //read one Call item from database and delete same item from database. 
    public static OCall PopCall()
    {
        using (var db = new MyDataContext())
        {
            var fc = (from c in db.Calls where c.Called == false select c).FirstOrDefault();
            OCall call = FillOCall(fc);
            if (fc != null)
            {
                db.Calls.DeleteOnSubmit(fc);
                db.SubmitChanges();
            }
            return call;
        }
    }

    public static int Count()
    {
        using (var db = new MyDataContext())
        {
            return (from c in db.Calls select c.ID).Count();
        }
    }

    private static OCall FillOCall(Model.Call c)
    {
        if (c != null)
            return new OCall { ID = c.ID, Caller = c.Caller, Called = c.Called };
        else return null;
    }
}

分离的 OCall 类:

public class OCall
{
    public int ID { get; set; }
    public string Caller { get; set; }
    public bool Called { get; set; }
}

【问题讨论】:

    标签: c# .net multithreading linq-to-sql


    【解决方案1】:

    不幸的是,再多的 Linq-To-Sql 技巧、SqlClient 隔离级别和 System.Transactions 都无法使 PopCall() 线程安全,其中“线程安全”真正意味着“并发安全”(即,当并发发生在数据库服务器,在客户端代码/进程的控制和范围之外)。任何类型的 C# 锁定和同步都不会帮助您。您只需要深入了解关系存储引擎的工作原理,就可以正确地做到这一点。使用表作为队列(就像你在这里所做的那样)是出了名的棘手,容易出现死锁,而且很难让它正确。

    更不幸的是,您的解决方案必须针对特定平台。我只会解释使用 SQL Server 的正确方法,那就是利用 OUTPUT 子句。如果您想了解更多详细信息,请阅读这篇文章Using tables as Queues。您的 Pop 操作必须在数据库中以原子方式发生,调用如下:

    WITH cte AS (
     SELECT TOP(1) ... 
     FROM Calls WITH (READPAST)
     WHERE Called = 0)
    DELETE
    FROM cte
    OUTPUT DELETED.*;
    

    不仅如此,Calls 表还必须使用Called 列上最左侧的聚集键来组织。为什么会这样,在我之前引用的文章中再次解释过。

    在这种情况下,Count 调用基本上是无用的。正确检查可用项目的唯一方法是向 Pop,请求 Count 只会对数据库施加无用的压力以返回 COUNT() 值,这在并发环境下没有任何意义。

    【讨论】:

      【解决方案2】:

      单独而言,它们是线程安全的,因为它们使用隔离的数据上下文等。但是,它们不是原子单元。因此,安全地检查计数是否 > 0,然后假设仍有一些东西要弹出。任何其他线程都可能正在改变数据库。

      如果你需要这样的东西,你可以用TransactionScope 包裹起来,它会给你(默认情况下)可序列化的隔离级别:

      using(var tran = new TransactionScope()) {
          int count = OCall.Count();
          if(count > 0) {
              var call = Count.PopCall();
              // TODO: something will call, assuming it is non-null
          }
      }
      

      当然,这会引入阻塞。最好直接检查FirstOrDefault()

      请注意,PopCall 仍然可能引发异常 - 如果另一个线程/进程删除了您获取它和调用 SubmitChanges 之间的数据。扔在这里的好处是你不应该发现你返回相同的记录两次。

      SubmitChanges 是事务性的,但读取不是,除非由事务范围或类似范围跨越。使PopCall 原子而不抛出:

      public static OCall PopCall()
      {
          using(var tran = new TrasactionScope())
              using (var db = new MyDataContext())
              {
                  var fc = (from c in db.Calls where c.Called == false select c).FirstOrDefault();
      
                  OCall call = FillOCall(fc);
      
                  if (fc != null)
                  {
                      db.Calls.DeleteOnSubmit(fc);
                      db.SubmitChanges();
                  }
      
                  return call;
              }
              tran.Complete();
          }
      }
      

      现在FirstOrDefault 被可序列化的隔离级别所覆盖,因此执行读取操作会锁定数据。如果我们可以在此处显式发出UPDLOCK,那会甚至更好,但 LINQ-to-SQL 不提供此功能。

      【讨论】:

      • 谢谢! ,似乎您使用 TrasactionScope 因为它是一个锁定语句,我认为 TrasactionScope 只能确保“全有或全无”的 sql 查询执行。它还会阻塞其他线程吗?
      • @sharru - 数据库在可序列化隔离时执行此操作。如前所述,UPDLOCK 会更好地避免更多的时间边缘情况。
      【解决方案3】:

      Count() 是线程安全的。从两个不同的线程同时调用它两次不会造成任何伤害。现在,另一个线程可能会在调用期间更改项目数,但那又如何呢?另一个线程可能会在它返回后一微秒改变项目的数量,而你对此无能为力。

      另一方面,PopCall 确实存在线程问题的可能性。一个线程可以读取fc,然后在它到达SubmitChanges()之前,另一个线程可能会介入并执行读取和删除,然后返回到第一个线程,它将尝试删除已经删除的记录。然后两个调用将返回相同的对象,即使您的意图是一行只返回一次。

      【讨论】:

      • SubmitChanges 之一在这种情况下会抛出异常(有一个自动事务和行检查) - 所以 AFAIK 你不应该得到并行返回的相同记录。仍然是一个小故障,但不完全是这里描述的小故障......
      • 谢谢! ,所以 PopCall 不是线程安全的,我真的不想两次获取记录,哪些更改会使其线程安全?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多