Reputation: 11317
Yet another question about thread safety and async/await. I don't understand why my second increment is not thread safe.
class Program
{
static int totalCountB = 0;
static int totalCountA = 0;
static async Task AnotherTask()
{
totalCountA++; // Thread safe
await Task.Delay(1);
totalCountB++; // not thread safe
}
static async Task SpawnManyTasks(int taskCount)
{
await Task.WhenAll(Enumerable.Range(0, taskCount)
.Select(_ => AnotherTask()));
}
static async Task Main()
{
await SpawnManyTasks(10_000);
Console.WriteLine($"{nameof(totalCountA)} : " + totalCountA); // Output 10000
Console.WriteLine($"{nameof(totalCountB)} : " + totalCountB); // Output 9856
}
}
What I understand :
totalCountA++
is thread safe because, until that point, the code is completely sync.await
may be run on the threadpool, but I didn't expect that the code resuming the await
will be completely multi-threaded.According to the some answers/blogs, async/await should not create a new thread :
I'm really missing something big here.
Upvotes: 2
Views: 2027
Reputation: 456977
totalCountA++ is thread safe because, until that point, the code is completely sync.
Yes. This is because async
methods begin executing on the calling thread, just like synchronous methods (attribution: me).
I understand that await may be run on the threadpool, but I didn't expect that the code resuming the await will be completely multi-threaded.
The await
doesn't "run" anywhere. That's the point of my There Is No Thread article you linked to.
Now, after the await
, the rest of the code needs to run somewhere. await
by default will capture a "context" and use that to execute the remainder of the async
method (attribution: me). That "context" is SynchronizationContext.Current
, unless it is null
, in which case it is TaskScheduler.Current
. In a Console application, this means that the context is the thread pool context, so any available thread pool thread will execute the remainder of the async
method.
Upvotes: 6
Reputation: 32063
totalCountA++ is thread safe because, until that point, the code is completely sync.
Yes, that is correct.
I understand that await may be run on the threadpool, but I didn't expect that the code resuming the await will be completely multi-threaded.
Well, the code after the await
is not multi-threaded per se. The code after the await
, in this case the totalCountB++
operation, will run in some thread. Which thread picks it up depends on many factors and cannot be relied upon when there is no SynchronizationContext
.
Think of this case:
await
.await
.totalCountB=0
totalCountB=0
totalCountB=1
totalCountB=1
It can also happen that thread a finishes before thread b gets started, or even for the same thread a to pick up the next iteration.
Upvotes: 3
Reputation: 12181
I don’t have the reference in front of me, but lack of a synchronization context in a console application dramatically alters the behavior. There are much fewer guarantees as to which thread your code will run on.
If you run the same thing in a WPF application, you should find that it behaves more how you expect.
Upvotes: 0
Reputation: 10874
What Stephen Cleary is talking about in “There is no thread” is that you do not need to occupy a thread while waiting for async IO to complete.
Your claim “await may be run on the threadpool” doesn’t make sense. Await doesn’t run anywhere, it suspends execution until the task is complete.
Rather, it is true that in some execution models, the code after the await may be run on a threadpool thread.
Upvotes: 2