从 Task.WhenAll 调用的异步方法使用 DbContext 并返回错误

Posted

技术标签:

【中文标题】从 Task.WhenAll 调用的异步方法使用 DbContext 并返回错误【英文标题】:Async method called from Task.WhenAll use DbContext and returned an error 【发布时间】:2021-03-06 21:18:44 【问题描述】:

您好,我需要一些关于 Task.WhenAll() 运行的异步方法的建议。 它的 dotnet core 3 项目和上下文和服务被添加为 AddScoped。

当我运行 DoWorksAysnc 方法时,我总是遇到一个问题“在前一个操作完成之前在此上下文上启动了第二个操作。这通常是由使用相同 DbContext 实例的不同线程引起的。有关如何避免线程问题的更多信息与 DbContext"

我了解 DbContext 不是线程安全的。但无论如何我可以同时运行 DowksAsync 方法吗?

谢谢,

   public async Task DoWorksAsync(List<int> ids)
        
            
            if (ids.Count > 0)
            
                var listOfTasks = new List<Task>();
                foreach(var id in ids)
                
                    var task = Task.Run(async () => await UpdateDataById(id));
                    listOfTasks.Add(task);
                
                await Task.WhenAll(listOfTasks);
            
        

UpdateDataById 方法是

 public async Task UpdateDataById (int id)
        
            try
            

                //Get products from webservice
                List<Product> products = await _searchService.GetItemsByIdAsync(id);
                await _dataService.CreateTableIfNotExist(id); //Method inside is await _dbContext.Database.ExecuteSqlRawAsync(string.Format(_sqlCreateTableIfNotExist, id));


                // UpdateProductsToDataTable method includes truncate table and bulkcopy.writeToServerAsync(tablename)
                //await _dbContext.Database.ExecuteSqlRawAsync(string.Format("Truncate Table 0", tableName));    
                //await bulkcopy.WriteToServerAsync();
              
                if (await _dataService.UpdateProductsToDataTable(id,products) == true) 
                
                   await _emailService.sendEmailNotificationAsync();
                


                _logService.AddLog("process is done"); 

                _unitOfWork.SaveAsync(); //_context.SaveChangesAsync()
            
            catch (Exception ex)
            
                await _logService.AddErrorLog(new ErrorLog
                
                    Id= id,
                    ErrorMessage = ex.Message
                );
                _unitOfWork.SaveAsync();
            
        

【问题讨论】:

对你为什么 Task.Run 一个等待已经返回一个任务的方法的异步 lambda 感到困惑。如果你想要一个任务,直接调用 UpdateDataById 会给你一个任务? 【参考方案1】:

虽然你知道这一点,DbContexts 不是线程安全的,故事结束..

此问题很常见,如果您遵循这些经验法则

,此修复很简单
    不要缓存DbContext,它们已经在内部缓存。如果您有一个存储库(我真的建议您不要),除非您绝对确定它是 线程安全 并且它被非常谨慎地用于小型工作负载。但是请参阅第 2 点。 总是如果可以的话,在 using 语句中添加 DbContext从不调用来自多个线程的DbContext 实例(它们不是线程安全的)。 分别参见第 1 点和第 2 点 如果您发现自己想在数据库中添加更多线程(以加快速度),那么您可能做错了。数据库并行处理自己的工作负载和查询计划,并且通常可以比您做得更好(在合理范围内)。编写更好的查询并分析它们。 IE。不要单独查询每个 id,而是一次查询所有 id(如果可以的话)

如果您确实需要这样做,请确保您的 DbContext 通过一种或另一种方式为每个线程提供一个单独的实例。

或按照 Jeremy Lakeman 的建议。确保每个方法调用的上下文都是唯一的。

【讨论】:

另外,您可以为每个方法调用创建一个服务范围和上下文。 @JeremyLakeman,更新和归因。如果您想随时更新我的​​答案,如果您想通过另一个答案在 DI 路径上走得更远,我会很高兴地投票 嗨,它是 dotnet core 3 项目,上下文和服务已添加为 AddScoped。我想进行 1 个方法调用,但要更新多个 id 的数据。有什么想法吗? 见第 1 点。这些最简单的方法来实现这一点,只是在方法中启动上下文(如果可以的话),将其放在 using 语句中。但是还有其他解决方案 扩展点 1:DbContext 本身充当存储库,因此不建议实现存储库模式并与 DbContext 一起使用。【参考方案2】:

“使用 Entity Framework 6 以正确的方式管理 DbContext:深入指南”是理解 DbContext 问题的好论文。 link

一些亮点:

环境 DbContext 范围按预期流经代码执行 以简单、干净的方式管理 DbContext 实例的生命周期 自动管理多个 DbContext 派生类型 支持嵌套范围和异步代码 DbContext 生命周期不再依赖 DI 根据需要设置 DbContext 范围:JoinExisting、CreateNew、Suppress JoinExisting:作用域生命周期;最常见的服务/API 方法 CreateNew:瞬态生命周期;最常见的异步代码 抑制:抑制环境上下文;最常见的并行代码 DbContextScope 基于 EF 中的 TransactionScope 类 .NET 5/6 存在分叉:https://github.com/yegor-mialyk/DbContextScope

【讨论】:

以上是关于从 Task.WhenAll 调用的异步方法使用 DbContext 并返回错误的主要内容,如果未能解决你的问题,请参考以下文章

异步/等待死锁 Task.WaitAll 与 Task.WhenAll [重复]

Task CancellationTokenSource和Task.WhenAll的应用

可以使用 Task.WhenAll(...) 或其他比每次等待更有意义的东西重写此异步/等待代码吗? [复制]

Parallel.ForEach 与 Task.Run 和 Task.WhenAll

在一定时间后取消 Task.WhenAll

Task.WhenAll 不等待任务完成[关闭]