2

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.

Perfect28
  • 11,089
  • 3
  • 25
  • 45
  • 3
    `await ...` may not be creating a thread, but continuation needs to run somewhere and it easily can be another thread. – Guru Stron Nov 16 '21 at 21:03
  • 1
    In short, if you are dealing with tasks or threads, and you are have shared resources, you will need thread safety, yes there are a lot of substiles and a lot of ways to minimizing thread safety techniques, and write no lock code. However, its kind of link bragging about you only needing one sheet of toilet paper.. Unless you fully understand all code paths and how tasks and the async and await pattern works just lock the shared resource and be done with it... – TheGeneral Nov 16 '21 at 21:03
  • 2
    Read about SynchronizationContext. Run this code within a WinForms application from the MainThread context and after `await` you will be back in the MainThread context again (because of the special SynchronizationContext of WinForms applications) – Sir Rufo Nov 16 '21 at 21:06

4 Answers4

6

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
3

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.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
2

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.

Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
0

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.