Я выполняю запрос, который возвращает примерно 11 000 строк. Каждая из этих строк сопоставлена с уникальным POCO, для создания которого я написал облегченную ORM, называемую ToCustomObject<T>
. Прямо сейчас у меня уже есть асинхронный метод, который получает результаты этого запроса и помещает их в DataTable
. Затем я перебираю все строки и вызываю для них ToCustomObject
, чтобы сгенерировать свой объект, а затем добавляю его в List<T>
. Сам запрос занимает всего полсекунды, но цикл генерации всех моих POCO занимает несколько секунд.
public async Task<List<Foo>> GetFoosAsync()
{
//empty list to populate and return
List<Foo> foos = [];
//example, this is not how the query actually executes, do not need assistance with this part
string query = "select t.Field1, t.Field2 from Table";
DataTable results = await customDbHandlerThatsNotRelevant.ExecuteQueryAsync(query);
//async way to handle this?
foreach (DataRow row in results.Rows)
foos.Add(row.ToCustomObject<Foo>());
return foos ;
}
Я думал о том, чтобы использовать Task.WhenAll()
, чтобы поместить все поколения в список задач и выполнить таким образом, но foos.Add()
не ожидается. Я также изучал Parallel.ForEach()
, но я не настолько знаком с этим и, насколько я могу судить, не смог бы выполнить то, что пытаюсь сделать.
TL;DR: Как я могу сделать так, чтобы все появления foos.Add(row.ToCustomObject<Foo>());
происходили асинхронно?
Почему ToCustomObject
так медленно? POCO должны быть простыми и легкими. Вы выполняете запросы для каждого POCO?
Отражение, если его использовать не очень осторожно, скорее всего, будет работать очень плохо. Вам нужно заставить эту реализацию работать гораздо эффективнее, а не пытаться распараллелить крайне неэффективную реализацию.
Загрузите «результаты» в ConcurrentQueue. Используйте дополнительные «воркеры»/задачи для исключения из очереди; процесс; и добавьте (не в список) целевую параллельную коллекцию: например. ConcurrentDictionary, Stack, Bag или другая очередь; затем перейдите к наблюдаемой коллекции.
Прежде всего, я рекомендую использовать .NET Dapper, он делает именно то, что вам нужно: https://www.nuget.org/packages/Dapper
Я использую этот пакет уже несколько лет и являюсь его поклонником.
Вы можете использовать Parallel.ForEach следующим образом:
ConcurrentBag<Foo> foos = new ConcurrentBag<Foo>();
Parallel.ForEach(results.Rows, row =>
{
foos.Add(row.ToCustomObject<Foo>());
}
);
При добавлении объектов в список в нескольких потоках вам необходимо использовать потокобезопасный объект, например. одновременный пакет является потокобезопасным.
Не существует абсолютно никакого способа не утонуть в коде синхронизации потоков по сравнению с реальной выполняемой работой.
Запрос к базе данных отсутствует, это правильно. Вместо этого я написал, чтобы порекомендовать Dapper, и дал ссылку на него. Пожалуйста, объясните, почему цикл не должен работать.
Ну это сильно зависит. Видите ли вы в этом цикле узкое место в производительности? Если да, то возможно стоит распараллелить.
Но, как правило, он не предназначен для асинхронного выполнения, поскольку ваш метод ToCustomObject
не является асинхронным, поэтому вам придется заключать каждое выполнение в Task
, что уже является накладными расходами. Если вы сделаете это для всех элементов списка, ваша производительность может даже снизиться!
НО, если вы видите проблемы с производительностью, я могу предложить несколько способов:
ToCustomObject
звоню Task
, создаю Task
для каждого предмета и ожидаю их всех одновременноParallel.ForEach
ToCustomObject
работает плохо, возможно, внутри него есть какой-то синхронный код, который можно выполнить асинхронно? Тогда вы могли бы сделать асинхронную версию ToCustomObject
Конечно, вам нужно позаботиться о потенциальном доступе к вашему List
из нескольких потоков, нужно ли вам использовать некоторую синхронизацию или потокобезопасную коллекцию.
Опять же, если он содержит или может содержать асинхронную операцию, конечно, сделайте его асинхронным, но если он синхронный, это не имеет смысла.
Кажется, что большинство моих проблем с производительностью на самом деле просто ждали, пока страница Swagger загрузит все результаты. Тестирование с помощью Postmans привело к возврату всего за 1 секунду. Тем не менее, Parallel.Foreach
даже на этом сэкономил неплохой процент, так что я буду считать это принятым.
List<T>
не является потокобезопасным. Одновременные вызовыAdd()
просто разорвут список и приведут к сбою приложения. Можете ли вы рассмотреть какую-нибудь другую (одновременную) коллекцию?