#codaza_отвечает
Что же... и в этот раз, коллективными усилиями, предложенный фрагмент кода, без преувеличения, был разобран по косточкам. В нашем сообществе не мало действительно профессиональных разработчиков. Если вы не успели пробежаться по комментариям в предыдущем посте, то прошу вас не полениться и сделать это, так как вы увидите развитие мысли и ссылки на дополнительную информацию от участников группы.
Где же был зарыт "главный слон" 🐘? Проблема, с которой данный код никак нельзя было допускать в production-среду. Проблема была на строках 12-13:
int keyWordIndex = dataSlice.Value.TextFraction.Substring(actualPosition).IndexOf(keyWord);
Вызов метода Substring() - вот основная проблема. Дело в том, что при каждом вызове этого метода, в памяти будет создаваться полная копия строки, содержащаяся в свойстве TextFraction. Это не проблема при нечастных вызовах на небольших строках и без использования циклов. Для нашей ситуации это совершенно не годится, так как методу предстоит работать с большим массивом данных. Только представьте что может произойти с оперативной памятью при частых вызовах метода ExtractStochasticDataAsync(). И первым проблему заметил Александр Максименко, указав на то что вместо вызова Substring() следует делать вызов метода расширения для класса String - AsSpan(). Такой подход позволит не создавать копию строки TextFraction в оперативной памяти, что существенно снизит нагрузку на неё.
Далее, Александр увидел возможность для параллельной обработки данных в словаре dataSlices. При параллельной обработке нужно учитывать проблемы совместного доступа, поэтому, для словаря indexes, вместо Dictionary<int, int>, Александр взял потокобезопасную коллекцию ConcurrentDictionary<int, int>.
Метод ExtractStochasticDataAsync() является асинхронным и хорошей практикой является предоставления возможности для остановки метода (тем более метод работает в цикле с большим объемом данных). На эту проблему обратил внимание Aleksandr Radchenko, предложив использование CancellationToken.
Итоговый вариант после рефакторинга стал выглядеть так:
public async Task<IDictionary<int, int>> ExtractStochasticDataAsync(string keyWord, CancellationToken ct = default)
{
IDictionary<int, AnalyticalDataSlice> dataSlices = await LoadAnalyticalDataSlicesAsync(ct);
ConcurrentDictionary<int, int> indexes = new();
var tasks = dataSlices.Select(async ds =>
{
ct.ThrowIfCancellationRequested();
int actualPosition = await LoadActualPositionAsync(ct);
int keyWordIndex = ds.Value.TextFraction
.AsSpan(actualPosition).IndexOf(keyWord);
indexes.TryAdd(ds.Key, keyWordIndex + actualPosition);
});
await Task.WhenAll(tasks);
return indexes;
}
Кроме того, Aleksandr Radchenko предложил еще ряд важных улучшений с примерами листингов (за что ему отдельное спасибо). Например, использовать в качестве возвращаемого значения метода ExtractStochasticDataAsync() асинхронную реализацию интерфейса IAsyncEnumerable и оператор yield, что позволит организовать асинхронную обработку данных из вызываемого метода. Это потребует изменение выходного API у метода ExtractStochasticDataAsync(), но если это возможно по имеющимся бизнес-требованиям, сделать это точно стоит. Этот вариант будет выглядеть так:
public async IAsyncEnumerable<(int Key, int Position)> ExtractStochasticDataAsync01(string keyWord, [EnumeratorCancellation] CancellationToken ct = default)
{
IDictionary<int, AnaliticalDataSlice> dataSlices = await LoadAnalyticalDataSlicesAsync(ct);
foreach(var dataSlice in dataSlices)
{
int actualPosition = await LoadActualPositionAsync(ct);
int keyWordIndex = dataSlice.Value.TextFraction
.AsSpan(actualPosition).IndexOf(keyWord);
ct.ThrowIfCancellationRequested();
yield return (dataSlice.Key, keyWordIndex + actualPosition);
};
}
Из таких code review можно получить гораздо больше ценных и практических знаний нежели из очередной статьи в интернете.
Всем активным участникам огромное спасибо за вклад! 💙