2010-08-25 58 views
3

我的代码是用C#编写的,数据层使用LINQ to SQL来填充/加载分离的对象类。LINQ to SQL DAL,这是线程安全吗?

我最近更改了代码以使用多线程,并且我非常确定我的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; } 
} 

回答

3

单独它们是线程安全的,因为它们使用独立的数据上下文等。但是,它们不是原子单元。所以它是不是安全检查计数是> 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不提供这个,那将会更好

+0

谢谢! ,似乎你使用TrasactionScope,因为它是一个锁定语句,我认为TrasactionScope只能确保“全部或全部”SQL查询执行。它是否也阻止其他线程? – RuSh 2010-08-25 11:43:23

+0

@sharru - 数据库在可序列化隔离时执行该操作。如前所述,UPDLOCK会更好地避免更多的时间边缘情况。 – 2010-08-25 17:17:38

1

COUNT()是线程安全的。同时调用两次,来自两个不同的线程不会损害任何内容。现在,另一个线程可能会在通话期间更改项目的数量,但那又如何?另一个线程在返回后可能会在一微秒内更改它的数量,并且您无能为力。

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

+0

其中一个'SubmitChanges'将在这种情况下抛出一个异常(有一个自动事务和行检查) - 所以AFAIK你不应该得到并行返回的相同记录。仍然是一个小故障,但不是这里描述的小故障... – 2010-08-25 11:39:13

+0

谢谢! ,所以PopCall不会线程安全,我真的不想要两次获取记录,哪些更改会使线程安全? – RuSh 2010-08-25 11:46:33

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()值,这在并发环境下意味着什么。