SPListItemCollection用Parallel.ForEach触发0x80010102异常的解决求助
这个问题的核心原因很明确:SharePoint SSOM的核心对象(SPListItem、SPListItemVersion等)是基于COM的,并且绑定到创建它们的线程上下文,完全不支持跨线程访问。当你用Parallel.ForEach在多个线程中直接操作这些对象时,就会触发RPC_E_ATTEMPTED_MULTITHREAD异常——COM对象不允许在单线程公寓(STA)模式下被多线程调用。
下面是具体的解决方案和优化步骤:
1. 核心思路:分离数据提取与转换逻辑
我们需要先在单线程中把SharePoint对象里的所有需要的数据提取出来,转换成线程安全的POCO(Plain Old CLR Object),然后再用多线程处理这些POCO完成对象转换,彻底避免跨线程访问SharePoint COM对象。
步骤1:定义线程安全的中间数据类
创建一个类来存储从SPListItem和它的版本中提取的所有必要数据:
public class ItemVersionData { public double CurrentVersion { get; set; } // 存储历史版本的标签和对应的字段值 public List<(double VersionLabel, Dictionary<string, object> FieldValues)> HistoryEntries { get; set; } }
步骤2:单线程提取所有版本数据
在单线程中遍历SPListItem集合,把需要的版本数据提取到中间类中——这一步必须在创建SPListItem的同一个线程(也就是SPSite/SPWeb的using块内)完成:
protected static List<SPObjectWithHistory<T>> GetSPObjectsWithHistory(SPQuery query = null, List<string> filters = null) { Type objectType = typeof(T); string listName = ""; query = query ?? Util.DEFAULT_SSOM_QUERY; if (objectType == typeof(SPProject)) listName = Util.PROJECTS_LIST_NAME; else if (objectType == typeof(SPTask)) listName = Util.TASKS_LIST_NAME; else throw new Exception($"Could not find the list name for {objectType.Name} objects."); // 单线程提取SharePoint数据到线程安全的POCO var itemVersionDatas = new List<ItemVersionData>(); using (SPSite spSite = new SPSite(sp_URL)) using (SPWeb spWeb = spSite.OpenWeb()) { SPList itemsList = spWeb.GetList($"/Lists/{listName}"); // 优化:只查询需要的字段,减少数据量 if (filters != null) { query.ViewFields = string.Join("", filters.Select(f => $"<FieldRef Name='{f}'/>")); } SPListItemCollection listItems = itemsList.GetItems(query); foreach (SPListItem spItem in listItems) { var data = new ItemVersionData { HistoryEntries = new List<(double, Dictionary<string, object>)>() }; // 提取当前版本 if (spItem.Versions.Count > 0) { double.TryParse(spItem.Versions[0].VersionLabel, out data.CurrentVersion); } // 提取历史版本并提前处理过滤逻辑 if (spItem.Versions.Count > 1) { for (int i = 1; i < spItem.Versions.Count; i++) { var currentVersion = spItem.Versions[i]; double versionLabel = double.Parse(currentVersion.VersionLabel); var fieldValues = new Dictionary<string, object>(); // 提取需要的字段(过滤或全量) var targetFields = filters ?? currentVersion.Fields.Cast<SPField>().Select(f => f.InternalName); foreach (string fieldName in targetFields) { fieldValues[fieldName] = currentVersion[fieldName]; } // 提前处理过滤逻辑:只保留字段有变化的版本 bool shouldKeep = true; if (filters != null && i < spItem.Versions.Count - 1) { var nextVersion = spItem.Versions[i + 1]; shouldKeep = filters.Any(f => !Equals(currentVersion[f], nextVersion[f])); } if (shouldKeep) { data.HistoryEntries.Add((versionLabel, fieldValues)); } } } itemVersionDatas.Add(data); } } // 多线程安全处理POCO,转换为目标对象 var resultsList = new ConcurrentBag<SPObjectWithHistory<T>>(); Parallel.ForEach(itemVersionDatas, data => { var spObjWithHistory = new SPObjectWithHistory<T>(); spObjWithHistory._currentVersion = data.CurrentVersion; spObjWithHistory.History = new Dictionary<double, T>(); foreach (var (versionLabel, fieldValues) in data.HistoryEntries) { // 用字段字典转换为目标对象,避免依赖SharePoint COM对象 var spObj = SPObject<T>.ConvertFieldValuesToSPObject(fieldValues); spObjWithHistory.History.Add(versionLabel, spObj); } resultsList.Add(spObjWithHistory); }); return resultsList.ToList(); }
步骤3:修改SPObject的转换方法
新增一个从字段字典转换为目标对象的方法,不再依赖SPListItemVersion:
public static class SPObject<T> { // 原有的ConvertSPItemVersionObjectToSPObject方法可以保留,新增这个方法 public static T ConvertFieldValuesToSPObject(Dictionary<string, object> fieldValues) { var obj = Activator.CreateInstance<T>(); // 反射赋值,和原转换逻辑对齐(根据你的实际属性映射调整) foreach (var prop in typeof(T).GetProperties()) { if (fieldValues.TryGetValue(prop.Name, out var value)) { prop.SetValue(obj, value); } } return obj; } }
2. 额外优化建议
针对8万+项的大型列表,还可以做这些优化进一步提升性能:
- 分页处理:不要一次性加载所有8万项,用
SPQuery.RowLimit和ListItemCollectionPosition分页获取,每次处理1000-2000项,避免内存溢出。 - 精简查询字段:通过
SPQuery.ViewFields只查询转换需要的字段,减少数据传输和内存占用。 - 考虑切换到CSOM:如果你的环境是SharePoint 2013及以上,CSOM(客户端对象模型)天生支持异步和多线程,因为它基于REST,对象是线程安全的,性能和扩展性更好。
内容的提问来源于stack exchange,提问作者DylanVB
相关产品推荐
相关产品推荐

