небезопасный поток asyn c в List <>. ForEach () - PullRequest
2 голосов
/ 06 августа 2020

Почему небезопасно выполнять await внутри .ForEach()?

В проекте. Net Core 3.1 Я выбираю из некоторого WebApi, список пользователей, соответствующих критериям, затем я удаляю их и, наконец, снова прошу список пользователей. Естественно, на этот раз я ожидаю, что список будет пустым.

ошибочный код

    var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

    existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));

    var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
    ensure.Count.Should().Be(0);  <-- ERROR WAS 1!

Я обнаружил, что когда я вставляю Thread.Sleep(50) перед var ensure код работает. Это ясно указывает на то, что возникают проблемы с потоками, которых я не понимаю.

рабочий код (с задержкой)

            var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

            existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));
            Thread.Sleep(50);

            var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
            ensure.Count.Should().Be(0);

Альтернатива Рабочий код (с использованием foreach)

            var existing = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();

            foreach (var x in existing)
            {
                await client.DeleteByIdAsync(x.Id);
            }

            var ensure = (await client.GetByMatchAsync(new SearchParameters() {..})).ToList();
            ensure.Count.Should().Be(0);

декомпиляция .ForEach() Я не вижу, где возникает проблема потоковой передачи

    public void ForEach(Action<T> action)
    {
        if (action == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
        }

        int version = _version;

        for (int i = 0; i < _size; i++)
        {
            if (version != _version)
            {
                break;
            }
            action!(_items[i]);
        }

        if (version != _version)
            ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
    }

1 Ответ

5 голосов
/ 06 августа 2020

List ForEach просто принимает Action<T>, поэтому действие, которое вы предоставляете, не ожидается на каждой итерации, и выполнение может продолжаться до завершения вызова.

В противном случае это, вероятно, будет иметь перегрузку с подписью

Task ForEach(Func<T, Task> func)

Что мне показалось странным, так это то, что следующая строка компилируется, но вы не можете назначить Func<T, Task> Action<T>.

Action<string> a = async (value) => await Task.CompletedTask;

Тот факт, что вы можете вызвать ForEach этот способ вводил в заблуждение.

existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));
...