开发者

What thread runs a Task's continuation if you don't await the task?

开发者 https://www.devze.com 2022-12-07 21:44 出处:网络
I\'m trying to wrap my head around control flow in C# when using async, Task, and await. I understand how promises work, and that the returned Task<> from an async method will eventually contain

I'm trying to wrap my head around control flow in C# when using async, Task, and await.

I understand how promises work, and that the returned Task<> from an async method will eventually contain the result of a computation/IO/whatever.

I think I understand that if you explicitly wait for that Task, then the current thread blocks until the Task is complete. I also think that means that the code in the async method that returns a Task will be running on a thread in a thread pool.

What I don't understand is what happens if I don't "await" the Task returned by an asynchronous method. It seems to me that the continuation is executed on the original thread that calls the async method, but I have no idea how control can return to that thread.

Here's an example. Here's I'm using UniTask which is basically Tasks for Unity:

    public async UniTask ConnectAsync(Connection connection)
    {
        Debug.Log(Thread.CurrentThread.Name); -> this prints "Main Thread"
        // Close Any Old Connections
        await DisconnectAsync();

        // Default Address
        if (string.IsNullOrEmpty(connection.Address)) { connectio开发者_StackOverflow中文版n.Address = "localhost:6379"; }

        // Connect
        ConfigurationOptions config = new()
        {
            EndPoints =
            {
                { connection.Address, connection.Port },
            },
            User = connection.Username,
            Password = connection.Password,
        };
        m_Connection = await ConnectionMultiplexer.ConnectAsync(config);

        // Create Graph Client
        m_Graph = new(m_Connection.GetDatabase());

        // Notify
        await Editor.Controller.OnConnect();
        Debug.Log(Thread.CurrentThread.Name); -> this prints "Main Thread"
    }

If I call this method, and then neglect to await the returned Task (UniTask), both Debug.Log() show that execution is happening on the "Main Thread" (i.e. the UI thread).

How is it that without awaiting this Task, the Main Thread is able to return to this continuation? Does C# wait until the thread is in the Suspended/WaitSleepJoin state? I'm not aware of any code putting the UI thread to sleep so I'm not sure about that. I'm certainly not putting the UI to sleep.


I understand how promises work

Good, then we can stop right there. Tasks are nothing but compiler syntactic sugar over a promise. In fact, when JavaScript copied the await/async keywords from C#, they got implemented over the native Promise object.

Now, for the remainder of this I'm going to assume that you don't know how promises work. Think of it as getting called out on your promise bluff on your CV.

There's three parts to an async method:

  1. The "synchronous" part. This is what will run when you simply call your async function, awaiting it or not, and is everything before the first await in your function. In your function this is the Debug.Log call and the synchronous part of DisconnectAsync.

  2. The "asynchronous" part, the tail of your function. This gets stored as a lambda and it captures all necessary variables on creation. This gets called after #1 and when "done" it returns the Task object from your function. When the task is fully completed, the task is set as completed. Note that this can be recursive if you have multiple tails inside your tail.

  3. All the magic of Task. For example, Task.WhenAll instantiates mutexes in your Task and then waits on them for completion. This makes Task technically disposable, and thus a memory and OS handle leak if you don't dispose every single task you create. await itself is handled through TaskCompletionSource, and you get just the task it manages. Things like that.

Note that nowhere in this did I mention threads. Tasks are to threads like what cats are to doctors. They both exist, some interact, but you have to be pretty insane to say cats are made to work only with doctors. Instead, tasks work on contexts. Thread pools are one default context. Another is single threaded contexts.

That's right, you can easily have async code run on a single thread, which is perfect for GUI in a single threaded render loop-driven game. You create a dialog, await its showing and get a result, all without any additional threads. You start an animation and await its completion, all without any threads.


What I don't understand is what happens if I don't "await" the Task returned by an asynchronous method. It seems to me that the continuation is executed on the original thread that calls the async method, but I have no idea how control can return to that thread.

As I describe on my blog, each await (by default) captures a "context", which is SynchronizationContext.Current or TaskScheduler.Current. In this particular case, the UI context is captured and used to resume the async method (i.e., execute the continuation).

How is it that without awaiting this Task, the Main Thread is able to return to this continuation? Does C# wait until the thread is in the Suspended/WaitSleepJoin state?

It has to do with contexts, not threads. The UI context schedules work by posting to the main UI message queue. So the continuation is run when the UI thread processes its message queue; it doesn't have anything to do with thread states.

I'm not aware of any code putting the UI thread to sleep so I'm not sure about that. I'm certainly not putting the UI to sleep.

Your code just needs to return to the main loop to allow the continuation to run.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号