Parallel.ForEach 中每个线程的 DbContext 安全吗?

Cas*_*ter 5 c# parallel-processing multithreading entity-framework entity-framework-6

我现在正在签订一份合同,以提高使用 EF 6 作为 ORM 的现代 SaaS SPA Web 应用程序的后端服务的性能。我建议的第一件事是向当前运行单线程的后端服务引入一些多线程。首席软件工程师表示我们不能这样做,因为 EF 6 不是线程安全的。

我不是实体框架方面的专家。我选择的 ORM 是 DevExpress 的 XPO,并且我已经完成了与下面建议的类似的操作,使用该 ORM 没有出现问题。使用 EF 6 这种模式本质上不安全吗?

int[] ids;
using(var db = new ApplicationDbContext())
{
    // query to surface id's of records representing work to be done
    ids = GetIdsOfRecordsRepresentingSomeTask(db);
}

Parallel.ForEach(ids, id => { 
    using(var db = new ApplicationDbContext())
    {
        var processor = new SomeTaskProcessor(db, id);
        processor.ExecuteLongRunningProcessThatReadsDbAndCreatesSomeNewRecords();
        db.SaveChanges();
    }
});
Run Code Online (Sandbox Code Playgroud)

我对此进行了研究,并且我同意 DbContext 不是线程安全的。我建议的模式确实使用多个线程,但单个 DbContext 仅由单个线程以单线程方式访问。领导告诉我,DbContext 本质上是一个单例,这段代码最终会弄乱数据库。我找不到任何东西来支持这个说法。这件事上的领导正确吗?

谢谢

Eri*_* J. 1

您的模式是线程安全的。但是,至少对于 SQL Server 而言,如果并发性太高,您会发现总吞吐量会随着数据库资源争用的增加而下降。

理论上,Parallel.ForEach 优化了线程数量,但在实践中,我发现它在我的应用程序中允许过多的并发性。

您可以使用ParallelOptions可选参数控制并发。测试您的用例并查看默认并发是否适合您。

您的评论:请记住,无论如何,现在我们正在讨论上面代码中的 100 个 id,其中大多数 id 代表的工作不会最终对数据库进行任何更改,并且寿命很短,而满手可能需要分钟即可完成并最终将 10 条新记录添加到数据库中。您会立即推荐什么 MaxDegreesOfParallelism 值?

根据您的一般描述,可能是 2-3,但这取决于数据库密集程度ExecuteLongRunningProcessThatReadsDbAndCreatesSomeNewRecords(与执行 CPU 密集型活动或等待来自文件、Web 服务调用等的 IO 相比)。除此之外,如果该方法主要执行数据库任务,您可能会出现锁定争用或压垮您的 IO 子系统(磁盘)。在您的环境中进行测试以确定。

也许值得探索一下为什么ExecuteLongRunningProcessThatReadsDbAndCreatesSomeNewRecords给定的 ID 需要这么长时间才能完成。

更新

下面是一些测试代码,用于演示线程不会相互阻塞并且确实可以并发运行。为了简单起见,我删除了 DbContext 部分,因为它不会影响线程问题。

class SomeTaskProcessor
{
    static Random rng = new Random();
    public int Id { get; private set; }
    public SomeTaskProcessor(int id) { Id = id; }
    public void ExecuteLongRunningProcessThatReadsDbAndCreatesSomeNewRecords()
    {
        Console.WriteLine($"Starting ID {Id}");
        System.Threading.Thread.Sleep(rng.Next(1000));
        Console.WriteLine($"Completing ID {Id}");
    }
}
class Program
{
    static void Main(string[] args)
    {
        int[] ids = Enumerable.Range(1, 100).ToArray();

        Parallel.ForEach(ids, id => {
                var processor = new SomeTaskProcessor(id);
                processor.ExecuteLongRunningProcessThatReadsDbAndCreatesSomeNewRecords();
        });
    }
}
Run Code Online (Sandbox Code Playgroud)