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.- I understand that
await
may be run on the threadpool, but I didn't expect that the code resuming theawait
will be completely multi-threaded.
According to the some answers/blogs, async/await should not create a new thread :
- Does the use of async/await create a new thread?
- https://blog.stephencleary.com/2013/11/there-is-no-thread.html
I'm really missing something big here.
CodePudding user response:
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.
CodePudding user response:
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:
- First iteration runs up to
await
. - Second iteration runs up to
await
. - Some thread (a) resumes the code of the first iteration, and reads
totalCountB=0
- Another thread (b) resumes the code and reads
totalCountB=0
- Thread a assigns
totalCountB=1
- Thread b assigns
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.
CodePudding user response:
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.
CodePudding user response:
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.