LINQ to SQL 数据访问层,它是否是线程安全的?

3

我的代码是用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;
    }
}

Detached OCall类:

public class OCall
{
    public int ID { get; set; }
    public string Caller { get; set; }
    public bool Called { get; set; }
}
3个回答

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 并不提供此功能。

谢谢!看起来你使用了TransactionScope,就像使用锁语句一样。我原以为TransactionScope只能确保“全有或全无”的SQL查询执行,它是否也会阻止其他线程? - RuSh
@sharru - 当处于可串行化隔离级别时,数据库会执行此操作。如前所述,使用UPDLOCK可以避免更多的时间边缘情况。 - Marc Gravell

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调用基本上是无用的。您正确检查项目是否可用的唯一方法是弹出,询问Count只会对数据库产生无用的压力,以返回在并发环境下毫无意义的COUNT()值。


1

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

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


在这种情况下,SubmitChanges 中的一个将会抛出异常(存在自动事务和行检查),因此据我所知,您不应该同时获取相同的记录。虽然仍然有一些小问题,但并非完全与此处描述的问题相同... - Marc Gravell
谢谢!所以PopCall不是线程安全的,我真的不想获取两次记录,哪些更改可以使其线程安全? - RuSh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接