-2

I wanted to learn how async works in c# and found some basic examples that base on Task.Delay. They work fine and code like this works as expected:

static int taskNo = 0;

static async Task<Task> test(int k)
{
    int _taskNo = ++taskNo;
    Console.WriteLine($"Started task {_taskNo}");
    await Task.Delay(1000 * k);
    Console.WriteLine($"Finished task {_taskNo}");
    return Task.CompletedTask;
}

static async Task Main(string[] args)
{
    Task[] tasks = new Task[3];

    tasks[0] = test(4);
    tasks[1] = test(3);
    tasks[2] = test(2);

    await Task.WhenAll(tasks);
    Console.WriteLine("All tasks finished.");
    Console.ReadLine();
}

Originally I wanted to try making some big loops, like adding strings thousands of times, just to try it. Is it possible to do it async? When I try to use code like this, it just works on one thread (I can see just one thread working at 100%). How can I create multiple instances of that loop to work on every thread of the CPU?

//how to make this code to be able to work asynchronously?
static async Task LoopTest()
{
    int _taskNo = ++taskNo;
    Console.WriteLine($"Started task {_taskNo}");

    string s = "";
    for(int i = 0; i < 1000000; i++)
    {
        s += "qwertyuzxcvb";
    }

    Console.WriteLine($"Finished task {_taskNo}");
}

Edit: In one of the comments I asked how one thread can do operations faster than multiple threads, and here is the code, that I tested:

        static int taskNo = 0;

        static async Task LoopTest()
        {
            int _taskNo = ++taskNo;
            Console.WriteLine($"Started task {_taskNo}");

            string s = "";
            for(int i = 0; i < 30000; i++)
            {
                s += "qwertyuzxcvb";
            }

            Console.WriteLine($"Finished task {_taskNo}");
        }

        static async Task Main(string[] args)
        {
            DateTime asyncStartTime = DateTime.Now;
            Console.WriteLine($"Started async.");

            // async

            Task[] tasks = new Task[15];

            Parallel.ForEach(tasks, (Task) => LoopTest());
            
            DateTime asyncEndTime = DateTime.Now;

            taskNo = 0;
            //sync
            DateTime syncStartTime = DateTime.Now;
            Console.WriteLine($"Started sync.");

            for(int i = 0; i < tasks.Length; i++)
            {
                LoopTest();
            }

            DateTime syncEndTime = DateTime.Now;

            TimeSpan asyncTimeSpan = asyncEndTime - asyncStartTime;
            TimeSpan syncTimeSpan = syncEndTime - syncStartTime;

            Console.WriteLine($"Async finished in {asyncTimeSpan}, sync finished in {syncTimeSpan}.");
            Console.ReadLine();
        }

I can see that when async starts, all threads are pinned to 100% (this is i7 9750H, 12 threads are working, it says 100% CPU usage), and once it gets to sync, just one thread is pinned to 100% and overall CPU usage says 25%. Maybe there is just some dumb mistake, but how can it be faster in sync?

Results: results

Btw, I know that it's pretty stupid example for async and I know that there are many benefits in other situations, but I just wanted to do that looping test and now I just have more and more questions...

Colinovsky
  • 48
  • 8
  • 1
    For your test purposes, you should use `Task.Run()` to start new tasks. – Matthew Watson Sep 04 '20 at 07:44
  • 1
    `Task.Run(() => my_hard_work_method());` – Fabio Sep 04 '20 at 07:44
  • @Fabio thanks, it works! Results disappointed me, because it wasn't really much faster, but at least tasks are starting at the same time instead of starting one after another has finished. – Colinovsky Sep 04 '20 at 08:08
  • @Fabio actually async can be even slower. I tried to make 15 loops to run async, then sync and compared how much time it needed. Async was actually 1 second slower. How is that possible? I can see that all threads were pinned at 100% when running tasks async and that only one thread was used at 100% when running tasks one after the other, how can one thread do it faster? – Colinovsky Sep 04 '20 at 08:30
  • 2
    You are using wrong example to see asynchronous benefits. For splittin computation operations use `Parallel.ForEach`, which will consume multiple processor cores. For simultaneously accessing external resources (database, file system, web services or others) use actually async methods. Asynchronous approach for IO operation will use only one thread, but it wait for responses from difference resources simultaneously. – Fabio Sep 04 '20 at 08:41
  • @Colinovsky _"how can one thread do it faster?"_ if you would post both code snippets we may could explain – Ackdari Sep 04 '20 at 08:49
  • @Ackdari post is edited, you can see new code after "Edit:" and I've also posted some results. Maybe there is just some dumb mistake on my side? Or maybe c# just does it better than me forcing to use all threads? – Colinovsky Sep 04 '20 at 08:59
  • 1
    @Colinovsky I tested your code and it resulted in `Async finished in 00:00:00.4990435, sync finished in 00:00:00.5686410.`. So as expected the **parallel** version is a bit faster. Note that in your code **nothing** is async, this is because you didn't use any _true_ async method and awaited it's task. But also note that in generell async/await is not the correct tool to parallelize code. You may want to add `on {Thread.CurrentThread.ManagedThreadId}` the the output to see on which thread which `LoopTest` call gets executed. – Ackdari Sep 04 '20 at 09:26
  • @Ackdari so I guess, multiple loops like this cannot be done faster? I mean, obviously changing to StringBuilder gets the result, but that's not the case, I just wanted to try if loops can be done by different threads to finish them all faster. I thought, that if just one thread is pinned on 100%, when I use all 12 threads of the CPU, it would be like 10 times faster, but it's not. In my case, it's even slower - maybe some CPU throttle as the temp gets to almost 90*C. TLDR: what would you do to make looping like this faster? Is it achieveable? I know it's stupid, but I'm curious. – Colinovsky Sep 04 '20 at 09:37
  • 1
    No, that is not what I wanted to say. Using `Parallel.ForEach` to paralallize your code is good if it is appropriate. Also your example code with string concatination is just not a good test. If you concatenate the string `"qwertyuzxcvb"` 30000 times, one call of `LoopTest` will need at minimum 5,76 MB of Ram (probably more) so maybe what you are seeing is a problem with cache misses. I mean how many physical core has your CPU and how many Threads are using these cores at the same time and how large is the L1 cache of you CPU. – Ackdari Sep 04 '20 at 10:11
  • 2
    The term **asynchronous** is not applicable here. You probably want to achieve **parallelism**. You may want to check [this](https://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods "What is the difference between concurrency, parallelism and asynchronous methods?") question, to sort things out. – Theodor Zoulias Sep 04 '20 at 11:11
  • 1
    @Ackdari I tried with just changing from adding strings, to increasing integer from 0 to 300 millions and now parallel did the job in just a bit under 2 seconds, when completing one after the other needed over 12 seconds - which is pretty good for utilizing all 12 threads, results are much better (6x faster for all threads). You may be right about that cache thing. Thank you very much for all your input, you did great job on explaining everything to me! – Colinovsky Sep 04 '20 at 11:32

1 Answers1

3

Is it possible to do it async?

There is nothing asynchronous about concatenating strings or incrementing integers. Async is used to release threads whilst waiting for I/O.

You could use Task.Run() to fake an asynchronous approach by offloading to the ThreadPool, but this would only be appropriate if you've got a UI thread that needs to remain unblocked; it adds overhead, and still results in a thread being used to do the work.

Either way, LoopTest is not an asynchronous method, so don't try to force it into being one.

If you do have a UI thread, just call it like this:

 await Task.Run(LoopTest);

Now the UI thread is released until LoopTest has completed.

Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
  • 2
    Async is not only for IO operations, heavy computations can be split i multiple threads. – Fabio Sep 04 '20 at 08:14
  • @Fabio That is not true asynchrony; it is parallelism. You can use `async` / `await` to implement parallelism, but the [`Parallel`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel?view=netcore-3.1) class is more appropriate / optimised for that purpose. – Johnathan Barclay Sep 04 '20 at 08:23