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!