0

Within Run, I'm trying to queue 10,000 tasks. (This is just an experiment so I can better understand async await.) But the first loop takes over three seconds to complete. I would expect this to be faster since I'm just trying to queue the tasks and await them a few lines later. Still within Run, alreadyCompleted is true because the tasks seem to have run synchronously. Finally, still within Run, the second loop takes 1 millisecond to complete, again showing the tasks have already run.

How can I queue tasks to run while still allowing execution to progress through the Run method until I use await? I would understand if some of my tasks had completed by the time alreadyCompleted gets checked, but it seems weird that all of them have completed. This behavior is consistent between ASP.NET Core 3, .NET Core 3 Console, and .NET Framework Console. Thank you.

using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public static class SqlUtility
{
    public static async Task Run()
    {
        const int max = 10000;
        Stopwatch sw1 = Stopwatch.StartNew();
        Task<int>[] tasks = new Task<int>[max];
        for(int i=0;i<max;i++)
        {
            tasks[i] = GetVideoID();
        }
        sw1.Stop();//ElapsedMilliseconds:  3169
        bool alreadyCompleted = tasks.All(t => t.IsCompletedSuccessfully);//true
        Stopwatch sw2 = Stopwatch.StartNew();
        for (int i = 0; i < max; i++)
        {
            await tasks[i].ConfigureAwait(false);
        }
        sw2.Stop();//ElapsedMilliseconds:  1
    }

    public static async Task<int> GetVideoID()
    {
        const string connectionString =
            "Server=localhost;Database=[Redacted];Integrated Security=true";
        SqlParameter[] parameters = new SqlParameter[]
        {
            new SqlParameter("VideoID", SqlDbType.Int) { Value = 1000 }
        };
        const string commandText = "select * from Video where VideoID=@VideoID";
        IAsyncEnumerable<object> values = GetValuesAsync(connectionString, parameters, commandText,
            CancellationToken.None);
        object videoID = await values.FirstAsync().ConfigureAwait(false);//1000
        return (int)videoID;
    }

    public static async IAsyncEnumerable<object> GetValuesAsync(
        string connectionString, SqlParameter[] parameters, string commandText,
        [EnumeratorCancellation]CancellationToken cancellationToken)
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
            using (SqlCommand command = new SqlCommand(commandText, connection))
            {
                command.Parameters.AddRange(parameters);
                using (var reader = await command.ExecuteReaderAsync()
                    .ConfigureAwait(false))
                {
                    while (await reader.ReadAsync().ConfigureAwait(false))
                    {
                        yield return reader[0];
                    }
                }
            }
        }
    }
}

Update

While running a .NET Core application and using a max of 1,000, alreadyCompleted is false. In fact, looking at IsCompleted of each task, none of the tasks are completed where alreadyCompleted is currently. This is what I would have expected to happen in my original scenario.

But if max is set to 10,000 (as it was originally), alreadyCompleted is false, just as above. Interestingly, checking tasks[max - 1].IsCompleted results in false (checking the last task first). tasks.All(t => t.IsCompletedSuccessfully) checks the last result last, at which point IsCompleted is true.

user1325179
  • 1,535
  • 2
  • 19
  • 29
  • 1
    In an `async` method, everything before the first `await` will run synchronously. Not sure if this explains what you are having, though. – Yacoub Massad Nov 13 '19 at 15:48
  • @YacoubMassad, that agrees with what I've read as well, which means execution should "pause" (so to speak) at `await connection.OpenAsync`. – user1325179 Nov 13 '19 at 15:56
  • 1
    I think you misunderstood what async means. It is not parallelism. It is about freeing your thread while a response is preparing. And when the response will be ready, another free thread can continue the execution. If you want to figure out how it is works try with an easier example. – Che Nov 13 '19 at 15:59
  • Is your question from where come delay and what to do? It comes from 10.000 calls of `GetVideoID()`, which need that much time. What to do? Not sure, but creating a new task which will run and create all those 10000 tasks in parallel is a possibility. – Sinatr Nov 13 '19 at 15:59
  • See what happens if you add `await Task.Yield()` at the beginning of `GetVideoID`. – Jeroen Mostert Nov 13 '19 at 16:06
  • Side note: Do you understand why you are using `ConfigureAwait(false)` everywhere? It's not a good idea to get into the habit of using it everywhere. It will burn you sometime if you don't understand what it's doing. It's rarely needed. – Gabriel Luci Nov 13 '19 at 16:15
  • 1
    What type of application are you running this in? ASP.NET, desktop, console? Also, you tagged [tag:.net], so can I assume you are *not* using .NET Core? – Gabriel Luci Nov 13 '19 at 16:18
  • @Che, I didn't mention parallelism. I don't necessarily want anything to run in parallel. I want to queue tasks to run later. – user1325179 Nov 13 '19 at 16:31
  • @Sinatr, no, that's not my question. Thank you though. Please see the question. – user1325179 Nov 13 '19 at 16:32
  • @JeroenMostert, thank you. I've tried that, but it doesn't seem to affect the result. – user1325179 Nov 13 '19 at 16:33
  • 2
    @GabrielLuci, your recommendation is the opposite of what I see elsewhere. And yes, I understand what `ConfigureAwait(false)` does, and I want to do it. – user1325179 Nov 13 '19 at 16:35
  • @GabrielLuci, I just updated the tag to `.net-core`. – user1325179 Nov 13 '19 at 16:36
  • 1
    ASP.NET Core / desktop / console? – Gabriel Luci Nov 13 '19 at 16:39
  • @user1325179 Re `ConfigureAwait(false)` - as long as you understand it and know you want it, that's fine. But no one should use it unless they understand what it does and why they're using it. In ASP.NET (not Core), for example, [it can cause `HttpContext.Current` to disappear on you](https://stackoverflow.com/questions/32731197/httpcontext-current-null-in-async-after-await-calls), which makes for difficult debugging if someone was told to always use `ConfigureAwait(false)` everywhere without understanding what it does. – Gabriel Luci Nov 13 '19 at 17:32
  • Where does that `FirstAsync` method come from? I don't see an `IAsyncEnumerable` extension method for it and there's nothing in the `using`s that allude to where it lives. – Kirk Larkin Nov 13 '19 at 17:44
  • @KirkLarkin it is a method of the class [`System.Linq.AsyncEnumerable`](https://github.com/dotnet/reactive/tree/master/Ix.NET/Source/System.Linq.Async/System/Linq/Operators), that is included in the package [System.Linq.Async](https://www.nuget.org/packages/System.Linq.Async). – Theodor Zoulias Nov 13 '19 at 18:52
  • 1
    @KirkLarkin, it is indeed from `System.Linq.Async`. – user1325179 Nov 13 '19 at 23:35

1 Answers1

4

It's important to understand that async methods start running synchronously. The magic happens when await is given an incomplete Task. (if it's given a completed Task, execution continues synchronously)

So when you call GetVideoID, this is what it's doing:

  1. GetVideoID runs until GetValuesAsync()
  2. GetValuesAsync runs until await connection.OpenAsync
  3. OpenAsync runs until it returns a Task.
  4. The await in GetValuesAsync sees the incomplete Task and returns its own incomplete Task.
  5. Execution of GetVideoID resumes until records.FirstAsync() is called.
  6. FirstAsync() runs until it returns an incomplete Task.
  7. The await in GetVideoID sees the incomplete Task and returns its own incomplete Task.
  8. Execution of Run resumes.

All of that happens synchronously. Three seconds to do that 10,000 times is pretty reasonable.

So why is every Task already completed when you check them? The short answer is that the continuations of those tasks are running on other threads.

If this is not a web app (or if it was .NET Framework), then it's the ConfigureAwait(false) that is making that happen. Without that, the continuations would have to wait until execution of Run() pauses. But with ConfigureAwait(false), the continuations can run in any context, so they're run on a ThreadPool thread.

If this is ASP.NET Core, which does not have any synchronization context, it would happen anyway even without the ConfigureAwait(false). That article talks about this under the heading Beware Implicit Parallelism:

ASP.NET Core does not have a SynchronizationContext, so await defaults to the thread pool context. So, in the ASP.NET Core world, asynchronous continuations may run on any thread, and they may all run in parallel.

In summary:

  • It's taking 3 seconds to run the initial, synchronous part of those 10,000 tasks.
  • The continuations of those tasks all run in parallel on other threads.
  • By the time you check, they're all done. Hence, "awaiting" doesn't need to wait at all.

Update: If you want to avoid the initial 3-second delay, then you can start the task on another thread by using Task.Run, which will allow your method to continue on almost immediately. Just replace this:

tasks[i] = GetVideoID();

With this:

tasks[i] = Task.Run(() => GetVideoID());

However, there is overhead in that, so you should time how long the entire operation takes to complete and see if it actually helps you.

Although that might not be a good idea in ASP.NET due to the limited number of threads available.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • 2
    This is the correct answer. I tested his application and found the same thing. He is running .NET Core and the tasks themselves are completing very quickly, and due to thread starvation the main thread doesn't continue execution until all the queued requests are already completed. If you introduce an actual lengthy asynchronous delay, like `await Task.Delay(TimeSpan.FromSeconds(5))`, into the async enumerable, you see something like this as the results: https://i.imgur.com/09SRfKa.png – Trevor Elliott Nov 13 '19 at 17:01
  • Note that if you want to actually create tasks without starting them, and choose to start them later, you should simply be creating each task using `new Task(...)` and then you can later call `task.Start()`. This will ensure that all parts of the task will only run when you want them to. async/await in this context helps with thread pool starvation, since your tasks will only consume threads when you are running .NET code and not while awaiting a response from SQL server, etc. – Trevor Elliott Nov 13 '19 at 17:10
  • @GabrielLuci, thank you, but your statements don't actually answer the question itself: "How can I queue tasks to run while still allowing execution to progress through the `Run` method until I use `await`?" – user1325179 Nov 18 '19 at 16:01
  • So you don't want the tasks to even start until you call `await`? – Gabriel Luci Nov 18 '19 at 16:09
  • @GabrielLuci, I wouldn't mind if some of them started. But generally, I want the execution of `Run` to proceed as quickly as possible until I `await` them. Thanks! – user1325179 Nov 18 '19 at 16:20
  • I added an update to the end of my answer. You just call your method inside `Task.Run` and it'll start the task on another thread, allowing your method to immediately continue. – Gabriel Luci Nov 18 '19 at 16:30
  • I discovered some strange behavior after changing `max`, and I've updated my question. Thank you for your answer. I've accepted it. – user1325179 Nov 18 '19 at 19:46