Home > front end >  Need some explanations about async/await and thread safety
Need some explanations about async/await and thread safety

Time:11-17

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 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.

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:

  1. First iteration runs up to await.
  2. Second iteration runs up to await.
  3. Some thread (a) resumes the code of the first iteration, and reads totalCountB=0
  4. Another thread (b) resumes the code and reads totalCountB=0
  5. Thread a assigns totalCountB=1
  6. 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.

  • Related