LINQ Select在Task.Run中丢失首个值,改用ForEach正常,原因是什么?
核心原因:LINQ延迟执行而非Task.Run
这和Task.Run无关,问题出在LINQ的延迟执行特性以及_myReader.GetAllVersionsForKey(request)返回的枚举类型特性上:
LINQ Select的延迟执行
Select是延迟执行的LINQ方法,它不会立即遍历records集合、执行反序列化操作,而是返回一个“待执行”的IEnumerable<T>对象。只有当外部代码枚举这个返回值时(比如调用ToList()、foreach遍历),才会真正遍历records并执行Select中的逻辑。枚举器的单次遍历限制
如果_myReader.GetAllVersionsForKey(request)返回的是仅支持单次枚举的枚举器(比如底层依赖数据库游标、数据流指针,或者自定义枚举实现中枚举一次后指针无法重置),那么当外部代码枚举这个延迟执行的IEnumerable<T>时,可能已经错过了第一个元素——比如某些场景下records的枚举器被意外提前移动了一次,而延迟执行的Select会从当前指针位置开始遍历。foreach的立即执行差异
你用foreach循环时,会在Task.Run内部立即遍历records的所有元素,将反序列化后的结果存入List<T>。List<T>是可多次枚举的集合,因此外部调用者能拿到完整的所有记录。
解决方案
将LINQ查询的结果立即物化(转为可持久化的集合),比如调用ToList()或ToArray(),这样就能在Task.Run内部完成所有遍历和反序列化操作,避免延迟执行带来的问题:
public Task<IEnumerable<T>> GetVersionsAsync<T>(string key) { var request = GetVersionsRequestBuilder.Create(_databaseName, _collectionName).WithKey(key); return Task.Run(() => { var records = _myReader.GetAllVersionsForKey(request); var returnValue = records.Select(record => { var document = _mySerializer.Deserialize<T>(record.document); return document; }).ToList(); // 立即物化结果 return returnValue; }); }
内容的提问来源于stack exchange,提问作者Nikhil Agrawal

