0

I am trying to understand how async/await work in C#. I created the below example.

public class SynchronizedCache<TKey, TValue>
{
    private readonly System.Threading.ReaderWriterLockSlim _lock
        = new System.Threading.ReaderWriterLockSlim();
    private readonly IDictionary<TKey, TValue> _dictionary
        = new Dictionary<TKey, TValue>();

    public TValue this[TKey key]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {
                if (!_dictionary.TryGetValue(key, out var value))
                {
                    throw new InvalidOperationException($"key {key} doesn't exist");
                }

                return value;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        set
        {
            _lock.EnterWriteLock();
            _dictionary[key] = value;
            _lock.ExitWriteLock();
        }
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        try
        {
            value = this[key];
            return true;
        }
        catch (Exception)
        {
            value = default(TValue);
            return false;
        }
    }

    public void SetValue(TKey key, TValue value)
    {
        this[key] = value;
    }
}

class Program
{
    private static readonly SynchronizedCache<int, string> Cache
        = new SynchronizedCache<int, string>();
    private static readonly Random Random = new Random();
    private static readonly IList<Task> Readers = new List<Task>();
    private static readonly IList<Task> Writers = new List<Task>();

    static async Task ReaderTask(string name)
    {
        while (true)
        {
            lock (Random)
            {
                var index = Random.Next(1, 100);
                if (Cache.TryGetValue(index, out var value))
                {
                    Console.WriteLine(
                        $"Reader({name} {Thread.CurrentThread.ManagedThreadId}"
                        + $", cache[{index}]={value}");
                }
            }

            await Task.Delay(1000);
        }
    }

    static async Task WriterTask(string name)
    {
        while (true)
        {
            lock (Random)
            {
                var index = Random.Next(1, 100);
                var value = Random.Next(100, 1000);
                Cache[index] = value.ToString();
                Console.WriteLine(
                    $"Writer({name}) {Thread.CurrentThread.ManagedThreadId}"
                    + $", cache[{index}]={Cache[index]}");
            }

            await Task.Delay(500);
        }
    }

    private static async Task Main(string[] args)
    {
        Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");
        await Task.WhenAll(
            // readers
            ReaderTask("Reader 1"),
            ReaderTask("Reader 2"),
            ReaderTask("Reader 3"),
            ReaderTask("Reader 4"),

            // writers
            WriterTask("Writer 1"),
            WriterTask("Writer 2"));
    }
}

When I run the above code, I get the following output:

Main thread 1
Writer(Writer 1) 1, cache[80]=325
Writer(Writer 2) 1, cache[28]=550
Writer(Writer 2) 4, cache[95]=172
Writer(Writer 1) 5, cache[71]=132
Writer(Writer 2) 5, cache[67]=454
Writer(Writer 1) 5, cache[91]=314
Writer(Writer 2) 5, cache[39]=154
Writer(Writer 1) 5, cache[99]=921
Writer(Writer 2) 6, cache[56]=291
Writer(Writer 1) 5, cache[8]=495
Writer(Writer 2) 6, cache[74]=907
Writer(Writer 1) 5, cache[35]=101
Writer(Writer 2) 5, cache[11]=449
Writer(Writer 1) 6, cache[64]=932
Writer(Writer 2) 7, cache[34]=825
Writer(Writer 1) 5, cache[88]=869
Reader(Reader 1 4, cache[8]=495
Writer(Writer 2) 8, cache[43]=983
Writer(Writer 1) 6, cache[15]=590
Writer(Writer 2) 5, cache[22]=276
Writer(Writer 1) 6, cache[58]=845

I get that my Main() thread is with ThreadId 1 but what I don't understand is where are the other threads created? I am not creating any specific threads here. Does calling an async function creates task that is already scheduled on a threadpool thread ?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Monku
  • 2,440
  • 4
  • 33
  • 57
  • ReadWriter and most locks in C# can't be used to sync tasks. You should look for custom implementations. SemaphoreSlim can be used for tasks. Tasks is just unit of works which is not threads and should not be considered as one, they don't run in parallel, they can be run on single UI thread, it more like coroutine in games. – eocron Nov 23 '19 at 16:58
  • @eocron "Tasks is just unit of works which is not threads,.." That's exactly what my understanding was. But when I look at the console output it confuses me about what exactly is going on. – Monku Nov 23 '19 at 16:59
  • 3
    The article [Async In Depth](https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth) on Microsoft Docs gives you an idea of what happens (especially in section "Deeper Dive into Task and Task for a CPU-Bound Operation") – Marius Nov 23 '19 at 17:02
  • 3
    I highly recommend reading [There is No Thread - Stephen Cleary](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). – Erik Philips Nov 23 '19 at 17:09
  • for a simple answer there is various ways to do an async job like CPU bound (Threads) and I/O bound (like stream) which now days I/O is stronger and cheaper than CPU so async/await using I/O bound technology – Hamid Nov 23 '19 at 17:24
  • 2
    There are ample discussions already on Stack Overflow explaining how this works in excruciating detail. See marked duplicates for a handful. Short version: `await` has to return execution _somewhere_. In a console program, that "somewhere" are threads from the thread pool. Thus thread IDs are varied. In other scenarios, such as Winforms, the context for resuming execution is the main UI thread unless otherwise specificed, and so you would see the same thread ID in that case. – Peter Duniho Nov 23 '19 at 18:32

2 Answers2

1

That's an abuse of async. Your tasks are compute bound in the example, asynchronous execution does not buy you anything.

With that aside, marking a method as async or creating a new Task for that matter does not guarantee a new thread will be launched to serve it. Only TaskCreationOptions.LongRunning currently uses a new thread when provided but that is strictly an implementation detail, subject to change without notice.

When a truly asynchronous method is called, say reading from a network or file, the method executes until the first await which cannot be executed synchronously. A continuation is queued at that point using the ambient SynchronizationContext. When the awaited operation completes, the continuation is launched which may complete on ThreadPool if ConfigureAwait(false) was used or there's no ambient context. No where is a new thread guaranteed for execution in all this.

Tanveer Badar
  • 5,438
  • 2
  • 27
  • 32
  • "That's an abuse of async. Your tasks are compute bound in the example, asynchronous execution does not buy you anything." I know it's not the best of the examples but hey whatever it takes to learn the concepts. I created this example to understand how the stuff works. Best practices are a separate subject altogether which I totally agree with you. – Monku Nov 23 '19 at 18:30
-1

The threads with IDs 4, 5, 6, 7 and 8 are created by the ThreadPool class, and used every time a Task is completed, to run the registered continuation of this Task. The number of these threads can be obtained any time by querying the property ThreadPool.ThreadCount:

Gets the number of thread pool threads that currently exist.

Initially the ThreadPool class creates as many threads as the cores of the machine. If the ThreadPool is starved, it creates new threads at a rate of one every 500 msec. You can control the minimum number of thread-pool threads by calling the method ThreadPool.SetMinThreads.

The thread-pool threads are optimized for granular workloads. A single thread-pool thread can run millions of Task continuations per second. It can also run lengthy workloads that are lasting many seconds or minutes, but generally you should avoid employing a thread-pool thread for so long, to prevent the ThreadPool from starving.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104