Skip to content

Add guard to DisposeAsync#31764

Merged
jcouv merged 3 commits intodotnet:masterfrom
jcouv:guard-dispose
Jan 4, 2019
Merged

Add guard to DisposeAsync#31764
jcouv merged 3 commits intodotnet:masterfrom
jcouv:guard-dispose

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented Dec 13, 2018

The behavior for DisposeAsync is only specified if the method is at the beginning, the end or a yield return. It is unspecified if the method is suspended at an await or running.
Per discussion with LDM, although it doesn't not guarantee that users couldn't call DisposeAsync at the wrong time (there are still possible race conditions that we can't guard against) we'll throw in such cases.

To that effect:

  • a new state is introduced to represent the beginning of the method (-3)
  • the states for yield return and await are split: yield return uses negative numbers (-4 and down), await uses positive numbers (0 and up, as in regular async methods)

The APIs could be mis-used in other ways , but we will not guard those:

  • calling MoveNextAsync() on two separate threads is unspecified,
  • calling MoveNextAsync() after DisposeAsync() is unspecified, but the implementation just returns false (no items left).
ValueTask IAsyncDisposable.DisposeAsync()
{
    if (state >= StateMachineStates.NotStartedStateMachine /* -1 */)
    {
        throw new NotSupportedException();
    }
    if (state == StateMachineStates.FinishedStateMachine /* -2 */)
    {
        return default;
    }
    disposeMode = true;
    _valueOrEndPromise.Reset();
    var inst = this;
    _builder.Start(ref inst);
    return new ValueTask(this, _valueOrEndPromise.Version);  // note this leverages the state machine's implementation of IValueTaskSource
}
        DisposeAsync                              await
 +------------------------+             +------------------------> N
 |                        |             |                          |
 v   GetAsyncEnumerator   |             |        resuming          |
-2 --------------------> -3 --------> -1 <-------------------------+    Dispose mode = false
 ^                                   |  |                          |
 |         done and disposed         |  |      yield return        |
 +-----------------------------------+  +-----------------------> -N
 |                                   |                             |
 |                                   |                             |
 |                             yield |                             |
 |                             break |           DisposeAsync      |
 |                                   |  +--------------------------+
 |                                   |  |
 |                                   |  |
 |         done and disposed         v  v    suspension (await)
 +----------------------------------- -1 ------------------------> N
                                        ^                          |    Dispose mode = true
                                        |         resuming         |
                                        +--------------------------+

Fixes #30109

@jcouv jcouv added this to the 16.0.P2 milestone Dec 13, 2018
@jcouv jcouv self-assigned this Dec 13, 2018
@jcouv jcouv requested a review from a team as a code owner December 13, 2018 20:17
{
managedThreadId = MakeCurrentThreadId();

var thenBuilder = ArrayBuilder<BoundStatement>.GetInstance();
Copy link
Copy Markdown
Member

@agocke agocke Dec 19, 2018

Choose a reason for hiding this comment

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

Give the size, since it's known statically. #Resolved

}
}");
CompileAndVerify(comp);
CompileAndVerify(comp, verify: Verification.Skipped);
Copy link
Copy Markdown
Member

@agocke agocke Dec 19, 2018

Choose a reason for hiding this comment

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

Why does this fail verification? #Resolved

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My bad. Forgot to undo this


In reply to: 243095742 [](ancestors = 243095742)


When in dispose mode, MoveNext continues to suspend (N) and resume (-1) until the end of the method is reached (-2).

The result of invoking `DisposeAsync` from states -1 or N is unspecified. This compiler throws `NotSupportException` for those cases.
Copy link
Copy Markdown
Member

@agocke agocke Dec 19, 2018

Choose a reason for hiding this comment

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

What about calling DisposeAsync simultaneously on multiple threads? Undefined? #Resolved

Copy link
Copy Markdown
Member Author

@jcouv jcouv Dec 20, 2018

Choose a reason for hiding this comment

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

Correct. That's also unspecified #Resolved

);
}

[ConditionalFact(typeof(WindowsDesktopOnly))]
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

What bug is tracking this being enabled on CoreClr again? #Resolved

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#30438


In reply to: 244785283 [](ancestors = 244785283)

yield return 1;
while (true)
{
await Task.Delay(10);
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

Why not Task.Yield? #Resolved

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Habit and didn't know about Yield(). The main goal is to avoid the optimization for already completed tasks.


In reply to: 244785798 [](ancestors = 244785798)

Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

Understood. The problem with Delay is that over the totality of our unit tests it ends up causing a noticable performance issue. At least historically it's done that. #Resolved

token.ThrowIfCancellationRequested();
}
}
public static async System.Threading.Tasks.Task Main()
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

Why the fully qualified name here? #Resolved

{
await task;
}
catch (System.OperationCanceledException) { }
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

Consider logging that the operation was cancelled successfully. #Resolved

{
var enumerator = M().GetAsyncEnumerator();
await enumerator.DisposeAsync();
System.Console.Write(""done"");
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

Consider a test where you call MoveNext after DisposeAsync. I did not see one in the file (may have missed it). #Resolved

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think that is covered by DisposeAsyncTwiceAfterRunning below.


In reply to: 244787127 [](ancestors = 244787127)

private readonly bool _isEnumerable;

// We use state=-3 to distinguish the initial state from the running state (-1)
private const int InitialState = -3;
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

-3; [](start = 45, length = 3)

The -3 constant is used twice in this PR. Consider adding a named constant to StateMachinesState. #Resolved

/// <summary>
/// States for `yield return` are decreasing from -3.
/// </summary>
private int _nextYieldReturnState = StateMachineStates.FinishedStateMachine - 1; // -3
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 2, 2019

Choose a reason for hiding this comment

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

StateMachineStates.FinishedStateMachine - 1; // -3 [](start = 43, length = 52)

This is another location where we could use a named constant for -3 #Resolved

@jaredpar
Copy link
Copy Markdown
Member

jaredpar commented Jan 2, 2019

Done with review pass (iteration #2) #Resolved

Copy link
Copy Markdown
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

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

:shipit:

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Jan 4, 2019

@agocke Did you have any more feedback? Thanks #Resolved


When in dispose mode, MoveNext continues to suspend (N) and resume (-1) until the end of the method is reached (-2).

The result of invoking `DisposeAsync` from states -1 or N is unspecified. This compiler throws `NotSupportException` for those cases.
Copy link
Copy Markdown
Contributor

@cston cston Jan 4, 2019

Choose a reason for hiding this comment

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

This compiler throws [](start = 74, length = 20)

Perhaps "The compiler generates throw NotSupportedException ..." or "The generated code throws NotSupportedException ..." #Pending

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Definitely. I'll include this fix in my next async-streams PR.


In reply to: 245376450 [](ancestors = 245376450)


/// <summary>
/// Lower the body, adding an entry state (-3) at the start,
/// so that we can differentiate a async-iterator that was never moved forward with MoveNextAsync()
Copy link
Copy Markdown
Contributor

@cston cston Jan 4, 2019

Choose a reason for hiding this comment

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

a [](start = 41, length = 1)

an? #Pending

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll include this fix in my next async-streams PR.


In reply to: 245378700 [](ancestors = 245378700)

Copy link
Copy Markdown
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

LGTM

F.Assignment(F.Local(resultVariable), F.This()));

var extraReset = GetExtraResetForIteratorGetEnumerator();
if (extraReset != null)
Copy link
Copy Markdown
Contributor

@cston cston Jan 4, 2019

Choose a reason for hiding this comment

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

extraReset != null [](start = 20, length = 18)

Would it make sense to pass in the builder to GetExtraResetForIteratorGetEnumerator()? (That would also allow adding multiple statements, although we probably don't have such a scenario today.) #WontFix

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That can be refactored easily when the need arises.


In reply to: 245383654 [](ancestors = 245383654)

thenBuilder.Add(
method.IsStatic || method.ThisParameter.Type.IsReferenceType ? // if this is a reference type, no need to copy it since it is not assignable
F.Goto(thisInitialized) : // goto thisInitialized
(BoundStatement)F.StatementList());
Copy link
Copy Markdown
Contributor

@cston cston Jan 4, 2019

Choose a reason for hiding this comment

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

(BoundStatement)F.StatementList() [](start = 24, length = 33)

Why add an empty statement list? #Pending

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't know. That's existing code though, so I'll leave as-is given the deadline.


In reply to: 245386255 [](ancestors = 245386255)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll fix in next PR


In reply to: 245392475 [](ancestors = 245392475,245386255)

@jcouv jcouv merged commit 966dcfc into dotnet:master Jan 4, 2019
@jcouv jcouv deleted the guard-dispose branch January 4, 2019 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants