【问题标题】:How to wait till completing first operation before starting second in DbContext EF multithreading如何在 DbContext EF 多线程中开始第二个操作之前等到完成第一个操作
【发布时间】:2021-05-29 00:08:31
【问题描述】:

我的项目是多线程的。我正在使用 EF Core。有时我会收到错误:

System.InvalidOperationException: '在前一个操作完成之前,在此上下文上启动了第二个操作。这通常是由不同的线程同时使用同一个 DbContext 实例引起的。有关如何避免 DbContext 线程问题的更多信息

我研究了这个问题,发现了一些关于它的注释:

  • EF Core 不是安全线程
  • 最好的解决方案是在每个线程中创建新的上下文

但有时我需要在多个线程中使用相同的上下文。我重现了这个问题。 我的模型:

public class School
{
    public Guid Guid { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public virtual ICollection<Student> Students { get; set; }
}

public class Student
{
    public Guid Guid { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DOB { get; set; }
    public Guid SchoolGuid { get; set; }
    public virtual School School { get; set; }
    public virtual ICollection<Backpack> Backpacks { get; set; }
}    
public class Backpack
{
    public Guid Guid { get; set; }
    public string Name { get; set; }
    public decimal Cost { get; set; }
    public Guid StudentGuid { get; set; }
    public virtual Student Student { get; set; }
}

重现步骤:

static void Main(string[] args)
{
    MainContext mainContext = new MainContext();
    CreateData(mainContext);
    var task1 = Task.Run(new Action(() => GetStudents(mainContext)));
    var task2 = Task.Run(new Action(() => GetBackpacks(mainContext)));
    Task.WaitAll(task1, task2);
}

static void GetStudents(MainContext mainContext)
{
    var students = mainContext.Set<Student>().ToList();
}

static void GetBackpacks(MainContext mainContext)
{
    var backpacks = mainContext.Set<Backpack>().ToList();
}

static void CreateData(MainContext mainContext)
{
    if (!mainContext.Set<School>().Any())
    {
       for (int i = 0; i < 10; i++)
       {
          var school = new School() { Guid = Guid.NewGuid(), Address = $"Scho olAddress{i}", Name = $"SchoolName{i}" };
          mainContext.Add(school);
          for (int j = 0; j < 1000; j++)
          {
             var student = new Student() { Guid = Guid.NewGuid(), LastName = $"LastName{j}", FirstName = $"FirstName{j}", DOB = DateTime.Today, School = school };
             mainContext.Add(student);
             for (int l = 0; l < 100; l++)
             {
                var backpack = new Backpack() { Guid = Guid.NewGuid(), Name = $"Name{i}", Student = student, Cost = l };
                mainContext.Add(backpack);
             }
          }
       }
       mainContext.SaveChanges();
    }
}

我希望第二次手术等到完成第一次手术。我该如何配置?我可能有很多线程可以使用相同的上下文。如何自动建立使用上下文的队列?

使用锁对某些用例有帮助,但并非对所有用例都有帮助:

static void GetStudents(MainContext mainContext)
        {
            lock (mainContext)
            {
                Console.WriteLine("Loading Students are started");
                var students = mainContext.GetEntities<Student>().ToList();
                Console.WriteLine("Students are loaded");
            }
        }

        static void GetBackpacks(MainContext mainContext)
        {
            lock (mainContext)
            {
                Console.WriteLine("Loading Backpakcs are started");
                var backpacks = mainContext.GetEntities<Backpack>().ToList();
                Console.WriteLine("Backpakcs are loaded");
            }
        }

【问题讨论】:

  • 使其异步并等待结果。检查是否成功。如果是,请继续下一步
  • DbContext 是一个工作单元,而不是数据库连接。它并不意味着长期存在或重复使用。它不能从多个线程中使用,这样做也没有意义。该错误在您的 Main 方法中。 GetStudentsGetBackpacks 方法也没什么意义 - 要从异步执行中获得任何好处,您需要使用 ToListAsync,而不是启动另一个线程并使用 ToList() 阻止它
  • 最后,代码在左右创建新的 DbSet。没有教程这样做是有原因的。 DbContext 不是连接,它是特定实体的工作单元,通过 DbSet 存储库属性公开。充其量,这会使代码更难阅读。由于每个Backpack 都有一个Student,因此您可以使用myContext.Backpacks.Include(x=&gt;x.Student) 一次加载所有相关对象。
  • @PanagiotisKanavos 这些方法只是为了重现我的问题。主要问题是可能有两个线程请求一个上下文。我无法跟进所有线程。我想在全球范围内修复它,例如: GetEntities() { if(context.IsUsing) wait().ContinueWith()....; }
  • “有时我需要在多个线程中使用相同的上下文”不,你真的不需要。您的数据库可以处理多个并发请求,但它们只需要来自不同的 DbContext 实例。

标签: c# multithreading entity-framework-core


【解决方案1】:

首先你必须修复 CreateData。删除 mainContext.Add(scholl) 和 mainContext.Add(student)。仅使用 mainContext.Set Backpack ().Add(backpack);

static void CreateData(MainContext mainContext)
{
    if (!mainContext.Set<School>().Any())
    {
       for (int i = 0; i < 10; i++)
       {
          var school = new School() { Guid = Guid.NewGuid(), Address = $"Scho olAddress{i}", Name = $"SchoolName{i}" };
         
          for (int j = 0; j < 1000; j++)
          {
             var student = new Student() { Guid = Guid.NewGuid(), LastName = $"LastName{j}", FirstName = $"FirstName{j}", DOB = DateTime.Today, School = school };
           
             for (int l = 0; l < 100; l++)
             {
                var backpack = new Backpack() { Guid = Guid.NewGuid(), Name = $"Name{i}", Student = student, Cost = l };
                mainContext.Set<Backpack>().Add(backpack);
             }
          }
       }
       mainContext.SaveChanges();
    }
}

您必须为每个任务创建一个新的上下文对象。

MainContext mainContext1 = new MainContext();
CreateData(mainContext1);
MainContext mainContext2 = new MainContext();
CreateData(mainContext2);

    var task1 = Task.Run(new Action(() => GetStudents(mainContext1)));
    var task2 = Task.Run(new Action(() => GetBackpacks(mainContext2)));
    Task.WaitAll(task1, task2);

但如果你不能这样做,那么这是你可以从 EF 获得的最大值。

public class Program
{
    static async Task Main(string[] args)
   {
    MainContext mainContext = new MainContext();
    CreateData(mainContext);
     await GetStudents(mainContext);
     await GetBackpacks(mainContext);
        
        
    }
   static async Task GetStudents(MainContext mainContext)
{
  await mainContext.Set<Student>().ToListAsync();
}

static async Task GetBackpacks(MainContext mainContext)
{
   await mainContext.Set<Backpack>().ToListAsync();
}
}

但我在上面的代码中没有看到任何实际用途,所以可能你需要这个

public class Program
{
    static async Task Main(string[] args)
   {
    MainContext mainContext = new MainContext();
    CreateData(mainContext);
    var students=  await GetStudents(mainContext);
     var backPacks= await GetBackpacks(mainContext);
        
        
    }
   static async Task<List<Student>> GetStudents(MainContext mainContext)
{
    return await mainContext.Set<Student>().ToListAsync();
}

static async TaskList<Backpack>> GetBackpacks(MainContext mainContext)
{
   return await mainContext.Set<Backpack>().ToListAsync();
}
}

【讨论】:

  • 我想同时运行 GetStudents 和 GetBackpacks。我只是做了一个简单的项目来重现我的问题。您等待 GetStudents,然后运行 ​​GetBackpacks。在我的项目中,我有很多正在运行的线程,我无法跟进每个线程。这就是为什么我需要自动解决这个问题而不是通过配置每个线程
  • 感谢您的建议,但我需要 EF
  • 我向您展示了您可以从 EF 获得的最大值。或者您必须为每个任务创建新的 dbcontext。
  • 对于某些用例,我无法使用新上下文,我需要以某种方式完成这项工作,而无需创建新上下文并在单线程中运行所有操作
  • EF 负责在单个线程中运行异步任务。你不必担心这个。
【解决方案2】:

您不能将 DbContext 用于并发查询。 DbContext 不是线程安全的。 将 DbContext 与异步一起使用的正确方法是:

static async Task Main(string[] args)
{
    MainContext mainContext = new MainContext();
    CreateData(mainContext);
    await GetStudents(mainContext);
    await GetBackpacks(mainContext);
}

否则您必须为每个任务创建新的 DbContext。

【讨论】:

    最近更新 更多