Reputation: 13832
Why is it threadunsafe to do an await
inside a .ForEach()
?
In a .Net Core 3.1 project I'm Selecting from some WebApi, the list of users matching a criteria, then I delete them, and finally I ask for the list of users again. Naturally, this time I expect the list to be empty.
The erroneous code
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!
I've found that when I insert a Thread.Sleep(50)
before var ensure
the code works. This clearly indicates to me that there are threading issues going on I don't understand.
the working code (using a delay)
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);
Alternative Working code (using 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);
decompiling the .ForEach()
I can't see where the threading problem occur
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();
}
Upvotes: 2
Views: 232
Reputation: 12567
List's ForEach just takes an Action<T>
so the action you provide is not awaited per iteration, and execution can continue before the call is completed.
Otherwise it would likely have an overload with the signature
Task ForEach(Func<T, Task> func)
What I thought was strange is that the following line compiles, but you can't assign a Func<T, Task>
to an Action<T>
.
Action<string> a = async (value) => await Task.CompletedTask;
The fact you could invoke the ForEach this way was misleading.
existing.ForEach(async x => await client.DeleteByIdAsync(x.Id));
Upvotes: 5