C# Parallel.ForEach()在SPListItemCollection上会引发异常(0x80010102)

3
在我的ASP.NET MVC应用程序中,我正在尝试检索列表中所有带有版本历史的项目,然后将它们转换为自定义对象。为此,我正在使用Microsoft.SharePoint
最初,我是这样做的: Util.GetSPItemCollectionWithHistory方法:
public static SPListItemCollection GetSPItemCollectionWithHistory(string listName, SPQuery filterQuery)
{
    using (SPSite spSite = new SPSite(sp_URL))
    {
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPList itemsList = spWeb.GetList("/Lists/" + listName);
            SPListItemCollection listItems = itemsList.GetItems(filterQuery);

            return listItems;
        }
    }
}

GetSPObjectsWithHistory method:

protected static List<SPObjectWithHistory<T>> GetSPObjectsWithHistory(SPQuery query = null, List<string> filters = null)
{
    List<SPObjectWithHistory<T>> resultsList = new List<SPObjectWithHistory<T>>();

    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(String.Format("Could not find the list name for {0} objects.", objectType.Name));

    SPListItemCollection results = Util.GetSPItemCollectionWithHistory(listName, query);
    foreach (SPListItem item in results)
    {
        resultsList.Add(new SPObjectWithHistory<T>(item, filters));
    }

    return resultsList;
}

SPObjectWithHistory类构造函数:

public SPObjectWithHistory(SPListItem spItem, List<string> filters = null)
{
    double.TryParse(spItem.Versions[0].VersionLabel, out _currentVersion);
    History = new Dictionary<double, T>();

    if (spItem.Versions.Count > 1)
    {
        for (int i = 1; i < spItem.Versions.Count; i++)
        {
            if (filters == null)
                History.Add(double.Parse(spItem.Versions[i].VersionLabel), SPObject<T>.ConvertSPItemVersionObjectToSPObject(spItem.Versions[i]));
            else
            {
                foreach (string filter in filters)
                {
                    if (i == spItem.Versions.Count - 1 || (string)spItem.Versions[i][filter] != (string)spItem.Versions[i + 1][filter])
                    {
                        History.Add(double.Parse(spItem.Versions[i].VersionLabel), SPObject<T>.ConvertSPItemVersionObjectToSPObject(spItem.Versions[i]));
                        break;
                    }
                }
            }
        }
    }
}

这种方式在代码运行时能够实现功能,但是在处理大型列表时速度非常慢。其中一个列表包含超过80000个项目,在构造函数的逻辑中创建一个SPObjectWithHistory项目需要约0.3秒钟。
为了加快该过程,我想使用Parallel.ForEach代替普通的foreach
然后,我的GetSPObjectsWithHistory更新为以下内容:
protected static List<SPObjectWithHistory<T>> GetSPObjectsWithHistory(SPQuery query = null, List<string> filters = null)
{
    ConcurrentBag<SPObjectWithHistory<T>> resultsList = new ConcurrentBag<SPObjectWithHistory<T>>();

    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(String.Format("Could not find the list name for {0} objects.", objectType.Name));

    List<SPListItem> results = Util.GetSPItemCollectionWithHistory(listName, query).Cast<SPListItem>().ToList();
    Parallel.ForEach(results, item => resultsList.Add(new SPObjectWithHistory<T>(item, filters)));

    return resultsList.ToList();
}

当我尝试运行应用程序时,Parallel.ForEach会出现以下异常:

内容:一个或多个错误发生
类型:System.AggregateException
堆栈跟踪:
在 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
在 System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
在 System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action'1 body, Action'2 bodyWithState, Func'4 bodyWithLocal, Func'1 localInit, Action'1 localFinally)
在 System.Threading.Tasks.Parallel.ForEachWorker[TSource,TLocal](IEnumerable'1 source, ParallelOptions parallelOptions, Action'1 body, Action'2 bodyWithState, Action'3 bodyWithStateAndIndex, Func'4 bodyWithStateAndLocal, Func'5 bodyWithEverything, Func'1 localInit, Action'1 localFinally)
在 System.Threading.Tasks.Parallel.ForEach[TSource](IEnumerable'1 source, Action'1 body)
在 GetSPObjectsWithHistory(SPQuery query, List`1 filters) in...
内部异常:
内容:尝试在单线程模式下使用多个线程进行调用。(HRESULT 异常: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))
类型:Microsoft.SharePoint.SPException
堆栈跟踪:
在 Microsoft.SharePoint.SPGlobal.HandleComException(COMException comEx)
在 Microsoft.SharePoint.Library.SPRequest.SetVar(String bstrUrl, String bstrName, String bstrValue)
在 Microsoft.SharePoint.SPListItemVersionCollection.EnsureVersionsData()
在 Microsoft.SharePoint.SPListItemVersionCollection.get_Item(Int32 iIndex)
SPObjectWithHistory 构造函数中的 double.TryParse(spItem.Versions[0].VersionLabel, out _currentVersion);
内部异常:
内容:尝试在单线程模式下使用多个线程进行调用。(HRESULT 异常: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))
类型:System.Runtime.InteropServices.COMException
堆栈跟踪:
在 Microsoft.SharePoint.Library.SPRequestInternalClass.SetVar(String bstrUrl, String bstrName, String bstrValue)
在 Microsoft.SharePoint.Library.SPRequest.SetVar(String bstrUrl, String bstrName, String bstrValue)
请问哪位知道如何使我的代码正常运行?

预先感谢您!


1
听起来这个API不是线程安全的,因为它要访问COM。最好不要在它上面使用多线程。 - Daniel A. White
1
你并没有真正做出任何提高性能的努力。 - Daniel A. White
我认为如果我能使用Parallel.ForEach,实际上会更快,因为应用程序将同时执行resultsList.Add(new SPObjectWithHistory<T>(item, filters));(+ -0.3秒)多次。 在80000个项目的集合上,这将产生相当大的影响。 即使只在2个并行线程上执行,也会产生巨大的差异。 - DylanVB
1
除非代码明确指示,否则COM是单线程的,以避免多线程固有的所有问题。该组件不表示它适用于线程,因此无论您多么希望它适用于线程,它都不安全。考虑使用延迟加载--例如,是否真的有必要提前检索列表项的所有80,000个项目?哪个用户会浏览它?即使您想要自定义对象,您也可以在自定义集合中存储必要的引荐数据,并根据需要实现/检索这些数据。 - Jeroen Mostert
@JeroenMostert 感谢您的澄清评论。不幸的是,在我的情况下,惰性加载不是一个选项。看起来我将不得不采取不同的方法。 - DylanVB
1个回答

2
显然,我尝试的操作是不可能的。Microsoft.SharePoint命名空间的SP对象不是线程安全的,就像@JeroenMostert所说的那样。
COM是单线程的,除非代码明确指示否则会避免多线程带来的所有问题。此组件未指示其对线程安全,因此无论您想要多少,它都不适用于线程安全。考虑使用延迟加载-是否真的有必要提前检索那个列表项的所有80,000个项目?哪个用户会浏览它?即使您想要自定义对象,您也可以将必要的引荐数据存储在自定义集合中,并按需实现/检索这些数据。
由于延迟加载对我不可行,我决定将我的逻辑分成批次(使用System.Threading.Task),每个批次执行我原始帖子中的代码(其中SPQuery.Query为每个批次更改)。之后,从GetSPObjectsWithHistory获取的结果合并为一个列表。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接