ASP.NET Core中DbContext多线程操作冲突问题及解决方案咨询
"An attempt was made to use the context while it is being configured. A DbContext instance cannot be used inside 'OnConfiguring' since it is still being configured at this point. This can happen if a second operation is started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe."
异常触发场景及代码
异常出现在多线程同时初始化DbContext时(比如前端并发请求同一方法),触发代码如下:
public async Task<IEnumerable<T>> QueryAsync<T>(string sqlStmt, Parameter[] inputParameters = null) { if (inputParameters is null) return await Database.GetDbConnection().QueryAsync<T>(sqlStmt); paramsList = new DynamicParameters(); paramsList.AddParamater(inputParameters); return await Database.GetDbConnection().QueryAsync<T>(sqlStmt, paramsList); // 异常触发行 }
其中Database是EF Core 6.0.9中DbContext的DatabaseFacade属性,相关定义:
namespace Microsoft.EntityFrameworkCore{ public class DbContext : IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbContextPoolable { private readonly DbContextOptions _options; private IDictionary<(Type Type, string? Name), object>? _sets; private IDbContextServices? _contextServices; private IDbContextDependencies? _dbContextDependencies; private DatabaseFacade? _database; /// <summary> /// Provides access to database related information and operations for this context. /// </summary> public virtual DatabaseFacade Database { get { CheckDisposed(); return _database ??= new DatabaseFacade(this); } } }}
当前DbContext注入配置(已设为Transient生命周期):
services.AddDbContextFactory<MyQueryDbContext>(options => options.UseSqlServer(MyQuery, sqlServerOptions => sqlServerOptions.CommandTimeout(databaseTimeout)), ServiceLifetime.Transient);
DbContext类定义:
public class MyQueryDbContext : MyDbCtx<MyQueryDbContext>, IMyQuery { public MyQueryDbContext(DbContextOptions<MyQueryDbContext> model) : base(model) { } }
public class MyDbCtx<TContext> : DbContext { public MyDbCtx(DbContextOptions<TContext> model) : base(model) { } }
注:未显式重写OnConfiguring方法,配置由外部提供。
替代解决方案(除改为同步执行外)
确保DbContext实例的线程独占性:
由于使用了AddDbContextFactory,需保证每个请求/操作通过工厂创建独立实例,使用后及时释放。在服务类中注入IDbContextFactory<MyQueryDbContext>,每次查询时创建新实例:private readonly IDbContextFactory<MyQueryDbContext> _contextFactory; public YourService(IDbContextFactory<MyQueryDbContext> contextFactory) { _contextFactory = contextFactory; } public async Task<IEnumerable<T>> QueryAsync<T>(string sqlStmt, Parameter[] inputParameters = null) { using var context = _contextFactory.CreateDbContext(); var connection = context.Database.GetDbConnection(); if (inputParameters is null) return await connection.QueryAsync<T>(sqlStmt); var paramsList = new DynamicParameters(); paramsList.AddParamater(inputParameters); return await connection.QueryAsync<T>(sqlStmt, paramsList); }每个查询使用独立DbContext实例,避免多线程共享同一实例导致的初始化冲突。
预初始化DbContext的Database属性:
在DbContext构造函数中提前触发Database属性的初始化,避免多线程环境下的延迟初始化竞争:public MyQueryDbContext(DbContextOptions<MyQueryDbContext> model) : base(model) { // 预初始化Database属性,消除多线程初始化竞争 _ = Database; }实例创建时完成
DatabaseFacade初始化,后续多线程访问时不会再触发初始化逻辑。使用锁保护DbContext初始化逻辑:
若无法避免共享DbContext实例(不推荐),可在访问Database属性时添加线程锁,确保同一时间只有一个线程执行初始化:private readonly object _dbInitLock = new object(); public async Task<IEnumerable<T>> QueryAsync<T>(string sqlStmt, Parameter[] inputParameters = null) { DatabaseFacade database; lock (_dbInitLock) { database = Database; } var connection = database.GetDbConnection(); if (inputParameters is null) return await connection.QueryAsync<T>(sqlStmt); var paramsList = new DynamicParameters(); paramsList.AddParamater(inputParameters); return await connection.QueryAsync<T>(sqlStmt, paramsList); }注意:此方式仅适用于特殊场景,DbContext本身并非线程安全,优先推荐使用独立实例方案。
内容的提问来源于stack exchange,提问作者zed101

