Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

stephentoub
Copy link
Member

Every async state machine today has a field for an Action. That field is used to cache an Action that's lazily created to point to its MoveNext method. It's only needed, however, if the state machine awaits something that's not a known awaiter.

Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.). I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box.

As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type. We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.

Every async state machine today has a field for an Action.  That field is used to cache an Action that's lazily created to point to its MoveNext method.  It's only needed, however, if the state machine awaits something that's not a known awaiter.

Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.).  I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box.

As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type.  We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.
@ghost
Copy link

ghost commented Mar 20, 2023

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

Issue Details

Every async state machine today has a field for an Action. That field is used to cache an Action that's lazily created to point to its MoveNext method. It's only needed, however, if the state machine awaits something that's not a known awaiter.

Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.). I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box.

As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type. We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.

Author: stephentoub
Assignees: stephentoub
Labels:

area-System.Threading.Tasks

Milestone: -

Copy link
Member

@benaadams benaadams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wild 💜

Copy link
Member

@davidfowl davidfowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome!

@davidfowl
Copy link
Member

davidfowl commented Mar 22, 2023

Can you show a before and after of the debugger display after this change?

We might also need to tweak any tools that we’re making assumptions about m_action being set in certain situations (parallel tasks tooling comes to mind)

@stephentoub
Copy link
Member Author

Can you show a before and after of the debugger display after this change?

using System.Diagnostics;

Task t = SomethingAsync();
Debugger.Break();

static async Task SomethingAsync()
{
    await Task.Delay(1_000_000);
}

Before:
image

After:
image

We might also need to tweak any tools that we’re making assumptions about m_action being set in certain situations (parallel tasks tooling comes to mind)

We might, though the delegate does accurately reflect what will be invoked.

@gregg-miskelly, do you expect this will have any debugger impact?

@gregg-miskelly
Copy link
Contributor

@gregg-miskelly, do you expect this will have any debugger impact?

Do you mean the DebuggerDisplay change? Or just this PR as a whole?

Aside from tests, I can't imagine that changing the DebuggerDisplay will break anything.

The debugger does sometimes inspect the result of get_MoveNextAction, but not _moveNextAction. Since you are just optimizing the implementation things should be okay as far as I can see.

The debugger also will inspect Task.m_action in various places. It isn't super obvious to me if this will be a problem, but I am assuming even if it is, we could handle this by checking if the task is a AsyncStateMachineBox.

What scenarios are we awaiting something that isn't a known awaiter so we can make sure we test this?

@stephentoub
Copy link
Member Author

stephentoub commented Mar 23, 2023

Thanks, Gregg.

Or just this PR as a whole?

The part where we store something into m_action.

The debugger also will inspect Task.m_action in various places. It isn't super obvious to me if this will be a problem, but I am assuming even if it is, we could handle this by checking if the task is a AsyncStateMachineBox.

Inspecting for display is fine; the delegate is meaningful in that it does in fact point to what will next be executed. What might be a problem, for example, is if the debugger makes a decision based on this having been null, since it won't be in some cases.

What scenarios are we awaiting something that isn't a known awaiter so we can make sure we test this?

For example:

using System.Diagnostics;
using System.Runtime.CompilerServices;

var mres = new ManualResetEventSlim();
Task t = ExampleAsync(mres);
mres.Wait();
Debugger.Break();
GC.KeepAlive(t);

async Task ExampleAsync(ManualResetEventSlim mres)
{
    await default(SwitchToThreadPool);
    mres.Set();
    Thread.Sleep(1_000_000);
}

struct SwitchToThreadPool : ICriticalNotifyCompletion
{
    public SwitchToThreadPool GetAwaiter() => default;
    public bool IsCompleted => false;
    public void GetResult() { }
    public void OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(s => ((Action)s!)(), continuation);
    public void UnsafeOnCompleted(Action continuation) => ThreadPool.UnsafeQueueUserWorkItem(s => ((Action)s!)(), continuation);
}

At that break, the Task t's m_action will now be a delegate pointing to the state machine box's MoveNext method, whereas previously it would have been null.

@stephentoub stephentoub merged commit a1982e2 into dotnet:main Mar 23, 2023
@stephentoub stephentoub deleted the asmaction branch March 23, 2023 01:09
@ghost ghost locked as resolved and limited conversation to collaborators Apr 22, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants