C # Parallel.ForEach () в SPListItemCollection вызывает исключение (0x80010102)

В моем приложении 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:

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

StackTrace:

в System.Threading.Tasks.Task.ThrowIfExceptional (логическое 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, ParallelOptions parallelOptions, Action'1 body, Action'2 bodyWithState, Action'3 bodyWithStateAndIndex, Func'4 bodyWithStateAthingLocal, Func'5 bodyWithE '1 localInit, Action' 1 localFinally)

в System.Threading.Tasks.Parallel.ForEach [TSource] (источник IEnumerable'1, тело Action'1)

в GetSPObjectsWithHistory (запрос SPQuery, фильтры List`1) в ...

InnerException:

Сообщение: Попытка выполнить вызовы более чем одного потока в однопоточном режиме. (Исключение из HRESULT: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))

Тип: Microsoft.SharePoint.SPException

StackTrace:

в 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)

в строке double.TryParse(spItem.Versions[0].VersionLabel, out _currentVersion); в конструкторе SPObjectWithHistory.

InnerException:

Сообщение: Попытка выполнить вызовы более чем одного потока в однопоточном режиме. (Исключение из HRESULT: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))

Тип: System.Runtime.InteropServices.COMException

StackTrace:

в Microsoft.SharePoint.Library.SPRequestInternalClass.SetVar (String bstrUrl, String bstrName, String bstrValue)

в Microsoft.SharePoint.Library.SPRequest.SetVar (String bstrUrl, String bstrName, String bstrValue)

Будет ли кто-нибудь, кто знает, как заставить мой код работать?

Заранее спасибо!


person DylanVB    schedule 15.01.2018    source источник
comment
Похоже, что API не является потокобезопасным, поскольку он переходит в COM. Просто не делайте на нем несколько потоков.   -  person Daniel A. White    schedule 15.01.2018
comment
вы на самом деле мало что делаете для повышения производительности каким-либо образом.   -  person Daniel A. White    schedule 15.01.2018
comment
Я думаю, что было бы намного быстрее, если бы я мог использовать Parallel.ForEach, поскольку приложение выполняло бы resultsList.Add(new SPObjectWithHistory<T>(item, filters)); (+ -0,3 секунды) несколько раз одновременно. В коллекции из 80000 предметов это окажет большое влияние. Даже выполнение этого в двух параллельных потоках вместо одного имело бы огромное значение.   -  person DylanVB    schedule 15.01.2018
comment
COM является однопоточным, если в коде явно не указано иное, чтобы избежать всех проблем, присущих многопоточности. Этот компонент не означает, что он безопасен для многопоточности, поэтому он небезопасен для многопоточности, независимо от того, насколько вы этого хотите. Подумайте об использовании отложенной загрузки - действительно ли действительно необходимо получить все 80 000 элементов этого элемента списка, например? Какой пользователь будет это просматривать? Даже если вам нужны настраиваемые объекты, вы можете сохранить необходимые реферальные данные в настраиваемой коллекции и материализовать / получить их по запросу.   -  person Jeroen Mostert    schedule 15.01.2018
comment
@JeroenMostert Спасибо за поясняющий комментарий. К сожалению, в моем случае ленивая загрузка не подходит. Похоже, мне придется применить другой подход.   -  person DylanVB    schedule 16.01.2018


Ответы (1)


Видимо, то, что я пытался сделать, невозможно. Объекты SP пространства имен Microsoft.SharePoint не являются потокобезопасными, как заявил @JeroenMostert.

COM является однопоточным, если в коде явно не указано иное, чтобы избежать всех проблем, присущих многопоточности. Этот компонент не означает, что он безопасен для многопоточности, поэтому он небезопасен для многопоточности, независимо от того, насколько вы этого хотите. Рассмотрите возможность использования отложенной загрузки - действительно ли действительно необходимо получить все 80 000 элементов этого элемента списка, например? Какой пользователь будет это просматривать? Даже если вам нужны настраиваемые объекты, вы можете сохранить необходимые реферальные данные в настраиваемой коллекции и материализовать / получить их по запросу.

Поскольку ленивая загрузка не была для меня вариантом, я решил разбить свою логику на партии (используя System.Threading.Task), каждый из которых выполняет код из моего исходного сообщения (с изменением SPQuery.Query для каждого пакета). После этого результаты моего GetSPObjectsWithHistory объединяются в единый список.

person DylanVB    schedule 16.01.2018