WinForms中避免执行长任务的Car对象重复实例化方案咨询
解决方案:确保Car实例唯一且不阻塞UI
你的核心问题是并发请求同一CarId时会重复创建实例并多次调用WebService,同时Part获取ParentCar时用Wait()阻塞了UI线程。下面是分步的修复方案:
一、先分析现有代码的关键问题
CarCache用List<Car>,并发访问不安全,且无法跟踪正在加载中的Car实例——当第一个Car还在异步调用WebService时,后续请求会因为缓存没找到而重复创建。Part.ParentCar的get方法用Task.Wait(),直接阻塞UI线程,违反WinForms的UI线程非阻塞原则。Car构造函数里直接启动Task.Run,没有对外暴露这个异步任务的状态,调用者无法知道初始化是否完成。DbClass.GetCarById没有处理并发场景,同一CarId的多个请求会触发多次数据库查询和WebService调用。
二、核心修复方案:用任务缓存代替实例缓存
我们需要用ConcurrentDictionary<string, Task<Car>>来缓存正在加载或已完成加载的Car任务,这样同一CarId的所有请求都会复用同一个Task,从根本上避免重复实例化和重复调用WebService。
1. 重构DbClass的缓存和GetCarById方法
public static class DbClass { private static SQLiteConnection sqlConn; private readonly string sqlFile = "pathToDbFile"; // 用ConcurrentDictionary缓存Car的加载任务,键是CarId private static readonly ConcurrentDictionary<string, Task<Car>> _carTaskCache = new ConcurrentDictionary<string, Task<Car>>(); public static async Task<Car> GetCarById(string carId, bool ignoreCache = false) { // 如果忽略缓存,先移除已有的任务 if (ignoreCache) { _carTaskCache.TryRemove(carId, out _); } // 尝试获取缓存的任务,没有则创建新的加载任务 var carTask = _carTaskCache.GetOrAdd(carId, async id => { Car foundCar = null; try { string sql = $"SELECT * FROM all_Cars WHERE Car = '{id}';"; using (SQLiteCommand command = new SQLiteCommand(sql, sqlConn)) { using (SQLiteDataReader reader = (SQLiteDataReader)await command.ExecuteReaderAsync()) { if (await reader.ReadAsync()) { // 一个CarId对应一条记录,用if即可 foundCar = new Car(reader); // 启动异步WebService任务,后台执行无需等待 _ = foundCar.FetchAdditionalDataAsync(); } } } if (foundCar != null) { var partsList = await GetPartsByCarId(carId); if (partsList.Count > 0) { foundCar.RelateParts(partsList); } } } catch (Exception e) { // 记录异常日志 string m = e.Message; // 加载失败时移除缓存任务,允许下次重试 _carTaskCache.TryRemove(id, out _); } return foundCar; }); // 等待任务完成,返回Car实例 return await carTask; } // 修复原GetPartsByCarId的递归问题,改为实际数据库查询 public static async Task<List<Part>> GetPartsByCarId(string carId) { List<Part> foundParts = new List<Part>(); string sql = $"SELECT * FROM all_Parts WHERE ParentCarId = '{carId}';"; using (SQLiteCommand command = new SQLiteCommand(sql, sqlConn)) { using (SQLiteDataReader reader = (SQLiteDataReader)await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { foundParts.Add(new Part(reader)); } } } return foundParts; } // 调整GetCarIndexById,从任务缓存中获取已完成的实例 public static int GetCarIndexById(string carId) { if (_carTaskCache.TryGetValue(carId, out var task) && task.IsCompletedSuccessfully) { var car = task.Result; // 兼容原CarCache逻辑,或直接用任务缓存替代 return CarCache.FindIndex(c => c.Id == carId); } return -1; } // 用任务缓存同步生成CarCache,避免维护两个缓存 private static List<Car> CarCache => _carTaskCache.Values .Where(t => t.IsCompletedSuccessfully) .Select(t => t.Result) .ToList(); // 其他方法(InitiateSqlConnection等)保留,按需调整为异步 }
2. 重构Car类:把异步任务从构造函数移出来
构造函数只做同步的数据库加载,异步的WebService调用放到单独方法中,移除不可靠的析构函数:
public class Car { private List<Part> _parts = new List<Part>(); public string Id { get; private set; } public ReadOnlyCollection<Part> Parts => _parts.AsReadOnly(); // 构造函数仅同步加载数据库属性 public Car(SQLiteDataReader sQLiteDataReader) { Id = sQLiteDataReader.GetString(sQLiteDataReader.GetOrdinal("Car")); // 加载其他数据库属性... } // 异步方法执行WebService调用 public async Task FetchAdditionalDataAsync() { try { // 耗时30秒的WebService调用 var additionalData = await CallLongRunningWebServiceAsync(); // 填充更多属性 // ... // 异步保存到数据库 await SaveToInternalDbAsync(); } catch (Exception e) { // 处理WebService调用异常 } } private async Task SaveToInternalDbAsync() { int index = await Task.Run(() => DbClass.GetCarIndexById(Id)); if (index > -1) { await DbClass.UpdateCarDataAsync(this); } else { await DbClass.AddCarAsync(this); } } public void RelateParts(IEnumerable<Part> parts) { // 加锁确保线程安全 lock (_parts) { _parts.AddRange(parts); } } // 移除析构函数,改用异步任务完成后自动保存 }
3. 修复Part类:避免阻塞UI线程
把ParentCar改为异步方法,让UI层用await获取,避免阻塞:
public class Part { public string ParentCarId { get; private set; } // 异步获取父Car,调用者需用await public async Task<Car> GetParentCarAsync() { return await DbClass.GetCarById(ParentCarId); } // Part构造函数,从数据库加载ParentCarId public Part(SQLiteDataReader reader) { ParentCarId = reader.GetString(reader.GetOrdinal("ParentCarId")); // 加载其他属性... } }
三、UI层调用注意事项
在WinForms事件中必须用await调用异步方法,绝对不能用Wait()或Result阻塞UI:
private async void btnLoadPart_Click(object sender, EventArgs e) { var part = await DbClass.GetPartById("part123"); // 假设GetPartById是异步方法 var car = await part.GetParentCarAsync(); // 更新UI控件 lblCarId.Text = car.Id; }
四、额外优化点
- 异常日志:在异步方法和数据库操作中添加完善的日志记录,方便排查问题。
- 缓存清理:可添加缓存过期逻辑,移除长时间未使用的Car任务。
- 全异步化:将所有数据库操作改为异步版本(如
ExecuteNonQueryAsync),避免阻塞线程。 - 线程安全:对
Car类的共享资源(如_parts)添加锁保护。
内容的提问来源于stack exchange,提问作者HeD_pE
相关产品推荐
相关产品推荐

