0

I'm pulling my hair out on this, and it's probably just because I haven't had that Ah-Ha moment yet. Help my Ah-Ha moment? I've distilled the example down into a small WebAPI demo project. It's .NET7 C# - nothing unusual about it. If you're following along, all the important bits could be generated in a console app just as well.

I'm using swagger to call a controller called GenerateDemoDataAsync, the controller calls off to a service instance, the service does some simulated heavy lifting, prints a whole bunch of debugging type console writes to and returns.

builder.Services.AddTransient<DemoDataService>();
    [HttpPost("api/generatedemodataasync")]
    public async Task<int> PostAsync()
    {
        await demoData.GenerateDemoDataAsync(InstanceId);
        return 0;
    }
    // string instance - a temporary unique identifier so when 
    // multiple are running in the console, I can tell which "sets" 
    // below together
    public async Task GenerateDemoDataAsync(string instance)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        await Task.Run(() => // Is this even needed?
        {
             for(int i = 0; i <= 99; i++)
            {
                Console.WriteLine($"GenerateDemoDataAsync_InLoop:: {i}");
                await InsertDataAsync(instance, i.ToString());
                // Thread.Sleep(10000);
                await Task.Delay(10000);  // In some scenarios this seems to be completely 
                                          // ignored and I must use the Thread.Sleep            
            });
        });

        sw.Stop();
        Console.WriteLine($"GenerateDemoDataAsync_End :: Time Elapsed {sw.Elapsed}");
    }

    private async Task InsertDataAsync(string instance, string message)
    {
        await Task.Run(() => // Internal Operation is not async, Task.Run
                             // should this be passed off as a Task, an async Task, not at all? 
        {
            var item = new DataQueueItem() { Instance = instance, Message = message };
            Console.WriteLine($"InsertDataAsync_InRun generated item:: {instance}:{message}");
        });
    }

I've configured about 100 different combinations of async Task Function and callers with async await, or Task.Run(() =>). In the current implementation of the code, I would expect it to do this:

POST => (calling async operation)

GENERATE_DEMO_DATA_ASYNC => drops into a background task, allowing control to be passed back to the UI while the for loop finishes. => PASS CONTROL BACK TO POST METHOD while async operation continues.

INSERT_DATA_ASYNC => Called by above, it may or may not have any bearing on this. It doesn't seem to make a difference. If I comment out the following line, the behavior seems the same.

What happens is, the whole thing ZIPS through like lightning, the Thread.Sleep or Task.Delay seem completely ignored....or if I move things around some and decorate some async stuff into some of the callers like the anonymous function in the Task.Run(), then Thread.Sleep or Task.Delay is honored, but it will block the POST from returning until the entire operation is returned.

await InsertDataAsync(instance, i.ToString());  

As documented in the code, there are several places where I don't know if I'm helping myself or shooting myself in the foot with Task.Run(() => operations. I'm probably thinking of it all wrong, and I come from a long C# and C++ background (30+ years), so it's probably my own background causing assumptions that are incorrect. I know there's some crazy smart people about. I know I could personally even much around with the combinations to get exactly what I wanted, but I'd LOVE it if someone would explain and deep dive into it and help me understand it so I can actually architect async / await pattern flow, rather than just mucking with it till it works.

Thank you everyone!

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Bryan - S
  • 99
  • 1
  • 4
  • I am afraid that your question is too broad. You are essentially asking to explain tasks, async/await, schedulers, thread-pool, synchronization context etc all in one answer. Such an answer would be so long that you wouldn't even bother to read it. That's not what StackOverflow is for. The questions here are expected to be focused. As a side note, `GENERATE_DEMO_DATA_ASYNC` is not the same as `GenerateDemoDataAsync`. C# is a case-sensitive language. For an aha moment, maybe [this](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/) will do. – Theodor Zoulias Mar 24 '23 at 20:52
  • @TheodorZoulias Thanks for the link, I'll go through it. I guess I don't see this as an overly broad question What I'm looking for is this: Given the code above, I'd like the controller to fire and return the http result immediatly, and the rest of the items finish in the appropriate order. I can do this, in fact it's not a hard task honestly, but I would just start throwing asyncs and awaits in and Task.WhenAll's and such, until It worked the way I want. Not very efficient. It'd be Awesome if someone would show me a best pratices version. – Bryan - S Mar 25 '23 at 22:43

1 Answers1

1

but it will block the POST from returning until the entire operation is returned.

Yes, it should not return immediately. Because async/await philosophy is about writing asynchronous code in a sync manner. What sync means here? - it means that return 0 code will not be executed until asynchronous operation called previously becomes completed. You can get rid of await and it will work as expected. Microsoft does good naming here - await means asynchronous waiting - no any threads are blocked, but logic will not be executed until Task completion.

but I'd LOVE it if someone would explain and deep dive into it and help me understand it so I can actually

Well, it is pretty complex (but essential) topic, main idea - do not sacrifice threads to await operations, but use special objects for that - awaiters. So, consider async/await as some sort of Memory-CPU trade-off.

Ryan
  • 609
  • 6
  • 13
  • Thanks for the insight. I guess thats part of my confusion. I can remove async's and just pass Tasks around, and it seems to work way closer than expected. But to utilize the librarys correctly, you get sent into peppering async's around or get compiler warnings. – Bryan - S Mar 25 '23 at 22:48
  • As i know, the best approach for executing background tasks is to write some service with Start ,Stop, EnqueueWorkItem methods, register it as a Singletone, and wrap it with BackgroundService. Then inject it in you controller, and enqueue work items by calling action methods. – Ryan Mar 27 '23 at 06:07
  • Yes, you're correct. I use this method quite a bit with the IHostedService and derived classes. It's kind of a go-to of mine. This would be for the work...that would be spawned out of the hosted service when there was work to be done. – Bryan - S Mar 28 '23 at 23:10