Task.Run导致“无法访问已释放的上下文实例”异常。

huangapple go评论81阅读模式
英文:

Task.Run causes "Cannot access a disposed context instance" exception

问题

使用Task.Run会导致我的应用程序在使用DbContext时出现“对象已处理”异常。

代码看起来像这样(查看整个链条):

UserController.cs

[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserService userService;

    public UsersController(UserService userService)
    {
        this.userService = userService;
    }

    public async Task<ActionResult> DoSomething()
    {
        await this.userService.MyMethod();
        return this.Ok();
    }
}

UserService.cs

public class UserService
{
    private readonly UserRepository userRepository;

    public UserService(UserRepository userRepository)
    {
        this.userRepository = userRepository;
    }

    public async Task MyMethod()
    {
        // 一些逻辑
        Task.Run(() => MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // 一些逻辑
        // 调用UserRepository,后者通过DI使用DbContext
    }
}

UserRepository.cs

public class UserRepository
{
    private MyDbContext dbContext;

    public UserRepository(MyDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public async Task DoSomethingToo(string username)
    {
        var user = await this.dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
        // 一些逻辑
    }
}

导致以下异常(消息):

无法访问已处理的上下文实例。此错误的常见原因是处理从依赖项注入解析的上下文实例,然后尝试在应用程序的其他地方使用相同的上下文实例。如果在上下文实例上调用“Dispose”或将其包装在使用语句中,则可能会发生这种情况。如果使用依赖项注入,则应让依赖项注入容器负责处理上下文实例的处理。

我如何配置我的数据库上下文和UserRepository:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // 一些逻辑
    if (MyDbContextFactory.GetConnectionString() != null)
    {
        services.AddDbContext<MyDbContext>(options => options.UseMySQL(MyDbContextFactory.GetConnectionString())
        .LogTo(s => System.Diagnostics.Debug.WriteLine(s)));
    }

    services.AddScoped(typeof(UserService));
    services.AddScoped(typeof(UserRepository));
    // 一些逻辑
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 一些逻辑
    using (var serviceScope = app.ApplicationServices.CreateScope())
    {
        var dbContext = serviceScope.ServiceProvider.GetService<MyDbContext>();

        if (dbContext != null)
        {
            dbContext.Database.Migrate();
        }
    }
    // 一些逻辑
}

public class MysqlEntityFrameworkDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
    {
        serviceCollection.AddEntityFrameworkMySQL();
        new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
            .TryAddCoreServices();
    }
}

MyDbContextFactory.cs

public MyDbContext CreateDbContext(string[] args)
{
    var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
    optionsBuilder.UseMySQL(GetConnectionString());
    return new MyDbContext(optionsBuilder.Options);
}

如果我将Task.Run替换为BackgroundJob.Enqueue,它可以正常工作。但是Hangfire在几分钟内创建了大量(> 1k)的Redis条目,因为这个方法被频繁调用。此外,如果它能在Hangfire中工作,那么它也应该能在Task.Run中工作。

英文:

Using Task.Run causes "object disposed" exception in my application if using DbContext.

The code looks like (see the whole chain):

UserController.cs

[Route(&quot;api/[controller]&quot;)]
public class UsersController : ControllerBase
{
    private readonly UserService userService;

    /// &lt;summary&gt;
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;userService&quot;&gt;&lt;/param&gt;
    public UsersController(UserService userService)
    {
        this.userService = userService;
    }


    public async Task&lt;ActionResult&gt; DoSomething()
    {
        await this.userService.MyMethod();
		return this.Ok();
    }
}

UserService.cs

public class UserService
{
    private readonly UserRepository userRepository;

    public UserService(UserRepository userRepository)
    {
        this.userRepository = userRepository;
    }
	
	public async Task MyMethod()
	{
		// some logic
		Task.Run(() =&gt; MethodCallAsync());
	}

	void MethodCallAsync()
	{
		// some logic
		// calls UserRepository, which uses the DbContext by DI
	}	
}

UserRepository:

public class UserRepository
{
    private MyDbContext dbContext;

    public UserRepository(MyDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public async Task DoSomethingToo(string username)
    {
        var user = await this.dbContext.Users.SingleOrDefaultAsync(u =&gt; u.Username == username);
        // some logic
    }
}

Causes the following exception (message):
> Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

How i did configure my db context and UserRepository:

Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        // some logic
        if (MyDbContextFactory.GetConnectionString() != null)
        {
            services.AddDbContext&lt;MyDbContext&gt;(options =&gt; options.UseMySQL(MyDbContextFactory.GetConnectionString())
            .LogTo(s =&gt; System.Diagnostics.Debug.WriteLine(s)));
        }
        
        services.AddScoped(typeof(UserService));
        services.AddScoped(typeof(UserRepository));
		// some logic
    }
	
	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
	    // some logic
        using (var serviceScope = app.ApplicationServices.CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService&lt;MyDbContext&gt;();

            if (dbContext != null)
            {
                dbContext.Database.Migrate();
            }
        }
		// some logic
    }
	
	public class MysqlEntityFrameworkDesignTimeServices : IDesignTimeServices
	{
		public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
		{
			serviceCollection.AddEntityFrameworkMySQL();
			new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
				.TryAddCoreServices();
		}
	}

MyDbContextFactory.cs

    public MyDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder&lt;MyDbContext&gt;();
        optionsBuilder.UseMySQL(GetConnectionString());
        return new MyDbContext(optionsBuilder.Options);
    }

If i replace Task.Run with BackgroundJob.Enqueue it works fine. But hangfire creates a lot (> 1k) of entries in redis within some minutes, since this method is called very often. Besides that, if it works with hangfire, it should also work with Task.Run,

答案1

得分: 4

代码部分不需要翻译,以下是已翻译的内容:

"The core problem is that you now have code that runs outside the scope of a request (i.e., request-extrinsic code). You want to return early and then update the database after the user receives the response (or more accurately, after the response is sent)."

"核心问题在于,您现在有一段在请求范围之外运行的代码(即请求外部的代码)。您希望尽早返回,然后在用户收到响应后(或更准确地说,在响应发送后)更新数据库。"

"Part of tearing down that request scope is disposing any instances that are scoped to that request, including DbContext and friends."

"拆除请求范围的一部分是处理与该请求有关的所有实例的销毁,包括DbContext等。"

"Besides that, if it works with Hangfire, it should also work with Task.Run"

"除此之外,如果它可以与Hangfire一起工作,那么也应该可以与Task.Run一起工作。"

"No."

"不可以。"

"The entire point of Hangfire is to provide reliable request-extrinsic code. Task.Run does not do this; it just throws work onto the thread pool. If, for example, you want to do a rolling update, then your app will be shut down once all its responses are sent, and work just tossed onto the thread pool may be lost. Hangfire prevents work loss by using a durable queue; this is usually a database but it sounds like yours is set up to use Redis (note that Redis is not durable by default, and it should be configured to be durable if you're using it as a backend for Hangfire)."

"Hangfire的整个目的是提供可靠的请求外部代码。Task.Run无法做到这一点;它只是将工作投放到线程池中。例如,如果您想执行滚动更新,那么一旦所有响应都被发送,您的应用程序将关闭,而只是投放到线程池的工作可能会丢失。Hangfire通过使用耐用队列来防止工作丢失;通常这是一个数据库,但听起来您的设置使用Redis(请注意,默认情况下Redis不是耐用的,如果您将其用作Hangfire的后端,则应配置为耐用)。"

"Since MethodCallAsync accesses the database, I interpret it as work you don't want to ever lose. If that's correct, then your options are:"

"由于MethodCallAsync访问数据库,我将其解释为您绝不希望丢失的工作。如果是正确的,那么您的选择是:"

"1. Call await MethodCallAsync directly without using Hangfire or Task.Run. This increases your response time but gives you one nice guarantee: if your client receives a successful response, then it knows the work completed successfully."

"1. 直接调用await MethodCallAsync,而不使用Hangfire或Task.Run。这会增加响应时间,但可以提供一个很好的保证:如果您的客户端收到成功的响应,那么它知道工作已成功完成。"

"2. Use Hangfire (with a proper durable queue) or build your own basic distributed architecture (as explained on my blog). This allows you to return early and then do the work later. Note that the work is done in its own scope, independent of the request scope. There is also some complexity dealing with failing work; the usual approach is to use a dead-letter queue and set up an alerting system so you can fix it manually."

"2. 使用Hangfire(带有适当的耐用队列)或构建您自己的基本分布式架构(如我在博客中所解释的)。这允许您提前返回,然后稍后执行工作。请注意,工作是在其自己的范围内完成的,独立于请求范围。处理失败的工作也有一些复杂性;通常的方法是使用死信队列并设置一个警报系统,以便您可以手动修复它。"

"Note that Task.Run is not a valid option, because it's work you can't lose, and Task.Run can lose work."

"请注意,Task.Run不是一个有效的选项,因为这是您不能丢失的工作,而Task.Run可能会丢失工作。"

"But hangfire creates a lot (> 1k) of entries in redis within some minutes, since this method is called very often."

"但是Hangfire在一些分钟内创建了大量(> 1k)的Redis条目,因为这个方法经常被调用。"

"It sounds like your actual problem is that Hangfire isn't very efficient, which is true. First, I'd recommend segregating the Hangfire data from any application data; e.g., if your app also uses the same Redis instance, then move Hangfire's data to a different Redis instance. Then scale up if necessary. If you still can't get it efficient enough, then I recommend using your own durable queue (e.g., Azure Storage Queues / Amazon SQS / Google Cloud Tasks / Kafka / RabbitMQ if configured to be durable / etc) instead of Hangfire."

"听起来您的真正问题是Hangfire效率不高,这是正确的。首先,我建议将Hangfire数据与应用程序数据分开;例如,如果您的应用程序也使用相同的Redis实例,那么将Hangfire的数据移动到不同的Redis实例中。然后,根据需要进行扩展。如果仍然无法达到足够的效率,那么我建议使用您自己的耐用队列(例如,Azure存储队列/Amazon SQS/Google Cloud任务/Kafka/RabbitMQ如果配置为耐用等),而不是使用Hangfire。"

英文:

The core problem is that you now have code that runs outside the scope of a request (i.e., request-extrinsic code). You want to return early and then update the database after the user receives the response (or more accurately, after the response is sent).

Part of tearing down that request scope is disposing any instances that are scoped to that request, including DbContext and friends.

> Besides that, if it works with hangfire, it should also work with Task.Run

No.

The entire point of Hangfire is to provide reliable request-extrinsic code. Task.Run does not do this; it just throws work onto the thread pool. If, for example, you want to do a rolling update, then your app will be shut down once all its responses are sent, and work just tossed onto the thread pool may be lost. Hangfire prevents work loss by using a durable queue; this is usually a database but it sounds like yours is set up to use Redis (note that Redis is not durable by default, and it should be configured to be durable if you're using it as a backend for Hangfire).

Since MethodCallAsync accesses the database, I interpret it as work you don't want to ever lose. If that's correct, then your options are:

  1. Call await MethodCallAsync directly without using Hangfire or Task.Run. This increases your response time but gives you one nice guarantee: if your client receives a successful response, then it knows the work completed successfully.
  2. Use Hangfire (with a proper durable queue) or build your own basic distributed architecture (as explained on my blog). This allows you to return early and then do the work later. Note that the work is done in its own scope, independent of the request scope. There is also some complexity dealing with failing work; the usual approach is to use a dead-letter queue and set up an alerting system so you can fix it manually.

Note that Task.Run is not a valid option, because it's work you can't lose, and Task.Run can lose work.

> But hangfire creates a lot (> 1k) of entries in redis within some minutes, since this method is called very often.

It sounds like your actual problem is that Hangfire isn't very efficient, which is true. First, I'd recommend segregating the Hangfire data from any application data; e.g., if your app also uses the same Redis instance, then move Hangfire's data to a different Redis instance. Then scale up if necessary. If you still can't get it efficient enough, then I recommend using your own durable queue (e.g., Azure Storage Queues / Amazon SQS / Google Cloud Tasks / Kafka / RabbitMQ if configured to be durable / etc) instead of Hangfire.

答案2

得分: 1

您的问题本质上是Task.Run(() => MethodCallAsync());启动了一个独立的线程,但仍然依赖于类字段userRepository(以及其中的DbContext),这在本质上是不安全的,设计上也不应该如此。您可以通过将更高级别的实例传递给异步方法来修复这个问题,例如,不仅仅替换userRepositoryDbContext本身,而是替换为IDbContextFactory<DbContext>。下面是示例代码:

public class UserService
{
    private readonly IDbContextFactory<MyDbContext> dbContextFactory;

    public UserService(IDbContextFactory<MyDbContext> dbContextFactory)
    {
        this.dbContextFactory = dbContextFactory;
    }
    
    public async Task MyMethod()
    {
        // 一些逻辑
        Task.Run(() => MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // 一些逻辑
        var userRepository = new UserRepository(dbContextFactory);
        // ...
    }   
}

public class UserRepository
{
    private MyDbContext dbContext;

    public UserRepository(IDbContextFactory<MyDbContext> dbContextFactory)
    {
        this.dbContext = dbContextFactory.CreateDbContext();
    }

    public async Task DoSomethingToo(string username)
    {
        var user = await this.dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
        // 一些逻辑
    }
}

DbContextFactory 应该是线程安全的,所以这可能会起作用。但是,如果我们深入研究您的代码,我会加入 Panagiotis Kanavos 的建议,说您可能根本不需要 UserRepository 类。DbContext 已经是您的 Repository 和 Unit of Work,为什么不直接使用它呢?

public class UserService
{
    private readonly IDbContextFactory<MyDbContext> dbContextFactory;

    public UserService(IDbContextFactory<MyDbContext> dbContextFactory)
    {
        this.dbContextFactory = dbContextFactory;
    }
    
    public async Task MyMethod()
    {
        // 一些逻辑
        Task.Run(() => MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // 一些逻辑
        using var dbContext = dbContextFactory.CreateDbContext();
        var user = await dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
        // ...
    }   
}

这样,对于相同的功能,您将减少一半的代码,而且它将以线程安全的方式工作。

英文:

Your problem essentially is that Task.Run(() =&gt; MethodCallAsync()); starts a separate thread that is still relying on the class field userRepository (and DbContext inside it) - this is inherently not thread-safe and is not supposed to be by design. One way you can fix this is by passing a higher-level instance into your async method - e.g. replacing userRepository not just with DbContext itself, but rather with IDbContextFactory&lt;DbContext&gt;. This is about how it would look like:

public class UserService
{
    private readonly IDbContextFactory&lt;MyDbContext&gt; dbContextFactory;

    public UserService(IDbContextFactory&lt;MyDbContext&gt; dbContextFactory)
    {
        this.dbContextFactory = dbContextFactory;
    }
    
    public async Task MyMethod()
    {
        // some logic
        Task.Run(() =&gt; MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // some logic
        var userRepository = new UserRepository(dbContextFactory);
        ...
    }   
}

public class UserRepository
{
    private MyDbContext dbContext;

    public UserRepository(IDbContextFactory&lt;MyDbContext&gt; dbContextFactory)
    {
        this.dbContext = dbContextFactory.CreateDbContext();
    }

    public async Task DoSomethingToo(string username)
    {
        var user = await this.dbContext.Users.SingleOrDefaultAsync(u =&gt; u.Username == username);
        // some logic
    }
}

DbContextFactory is supposed to be thread-safe, so this would probably work. However, if we dive deeper into your code, I would join Panagiotis Kanavos and say that you likely don't need the UserRepository class at all. DbContext is supposed to be your Repository and Unit of Work already, so why not use it directly?

public class UserService
{
    private readonly IDbContextFactory&lt;MyDbContext&gt; dbContextFactory;

    public UserService(IDbContextFactory&lt;MyDbContext&gt; dbContextFactory)
    {
        this.dbContextFactory = dbContextFactory;
    }
    
    public async Task MyMethod()
    {
        // some logic
        Task.Run(() =&gt; MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // some logic
        using var dbContext = dbContextFactory.CreateDbContext();
        var user = await dbContext.Users.SingleOrDefaultAsync(u =&gt; u.Username == username);
        ...
    }   
}

You will have two times less code for the same thing and it will be working in a thread-safe manner.

huangapple
  • 本文由 发表于 2023年3月7日 18:40:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/75660917.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定