C# LINQ对SQL DAL来说,这是线程安全的吗?

C# LINQ对SQL DAL来说,这是线程安全的吗?,c#,.net,multithreading,linq-to-sql,C#,.net,Multithreading,Linq To Sql,我的代码是用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()

我的代码是用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; }
}
Count()是线程安全的。从两个不同的线程同时调用它两次不会造成任何伤害。现在,另一个线程可能会在调用过程中更改项目的数量,但那又怎样呢?另一个线程可能会在返回后的一微秒内更改项目的数量,对此您无能为力


另一方面,PopCall确实存在线程问题的可能性。一个线程可以读取
fc
,然后在它到达
SubmitChanges()
之前,另一个线程可以在返回到第一个线程之前进行调解并执行读取和删除操作,该线程将尝试删除已删除的记录。然后,两个调用都将返回同一个对象,即使您希望一行只返回一次。

它们各自是线程安全的,因为它们使用独立的数据上下文等。但是,它们不是原子单元。因此,检查计数是否大于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不提供这个功能。

不幸的是,没有太多的LINQ-to-SQL欺骗,也没有SqlClient隔离级别,也没有系统。事务可以使
PopCall()
线程安全,而“线程安全”实际上意味着“并发安全”(即,当并发发生在数据库服务器上时,超出了客户机代码/进程的控制和范围)。任何类型的C#锁定和同步都不会对您有所帮助。您只需深入了解关系存储引擎的工作方式,就可以正确地获取此数据。使用表作为队列(就像您在这里所做的那样)是出了名的棘手,容易死锁,而且很难纠正

更不幸的是,您的解决方案必须是特定于平台的。我只想解释使用SQL Server的正确方法,那就是利用该子句。如果您想了解更多有关这种情况的详细信息,请阅读本文。您的Pop操作必须在数据库中以原子方式进行,并使用如下调用:

WITH cte AS (
 SELECT TOP(1) ... 
 FROM Calls WITH (READPAST)
 WHERE Called = 0)
DELETE
FROM cte
OUTPUT DELETED.*;
不仅如此,
Calls
表还必须在
Called
列上使用最左边的聚集键进行组织。我在前面引用的文章中再次解释了为什么会出现这种情况


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

在这种情况下,
提交更改之一将引发异常(存在自动事务和行检查)-好吧,你不应该同时返回相同的记录。仍然是一个小故障,但不完全是这里描述的小故障…谢谢!所以PopCall-ins不是线程安全的,我真的不想获取两次记录,哪些更改会使它线程安全?谢谢!看起来你使用TrasactionScope,因为它是一个锁语句,我想TrasactionScope只确保sql查询执行“全有或全无”。它还会阻止其他线程吗?@sharru-db在可序列化隔离状态下会这样做。如前所述,UPDLOCK最好避免更多的边缘计时情况。