Add AsyncValueTaskMethodBuilder in support of C# feature #10201

Merged
merged 2 commits into from Jul 22, 2016

Projects

None yet

9 participants

@stephentoub
Member
stephentoub commented Jul 20, 2016 edited

The C# compiler just merged a feature to allow arbitrary Task-like types in the return position of async methods. In support of that, this commit adds the necessary builder to allow ValueTask<T> to be used in this way.

I've not yet tried this with compiler bits.

cc: @cston, @ljw1004, @mellinoe, @terrajobst, @jaredpar
Related to dotnet/roslyn#12518

@ljw1004
ljw1004 commented Jul 20, 2016

LGTM

@stephentoub
Member

Thanks, Lucian. Anyone else want to review before I merge this?

@jaredpar jaredpar commented on the diff Jul 21, 2016
...tensions/src/System.Threading.Tasks.Extensions.csproj
@@ -4,7 +4,7 @@
<PropertyGroup>
<ProjectGuid>{F24D3391-2928-4E83-AADE-B34423498750}</ProjectGuid>
<AssemblyName>System.Threading.Tasks.Extensions</AssemblyName>
- <AssemblyVersion>4.0.1.0</AssemblyVersion>
+ <AssemblyVersion>4.1.0.0</AssemblyVersion>
@jaredpar
jaredpar Jul 21, 2016 Member

Why this vs. 4.0.2?

@stephentoub
stephentoub Jul 21, 2016 Member

I've been told that adding APIs requires bumping the minor version. @weshaggard, is that correct?

@cston cston and 1 other commented on an outdated diff Jul 21, 2016
....Extensions/tests/AsyncValueTaskMethodBuilderTests.cs
+ Assert.Equal(42, vt.Result);
+ }
+
+ [Fact]
+ public void SetResult_AfterAccessTask_ValueTaskContainsValue()
+ {
+ AsyncValueTaskMethodBuilder<int> b = ValueTask<int>.CreateAsyncMethodBuilder();
+ ValueTask<int> vt = b.Task;
+ b.SetResult(42);
+ Assert.True(vt.IsCompletedSuccessfully);
+ Assert.Same(vt.AsTask(), vt.AsTask()); // will be safe if completed asynchronously
+ Assert.Equal(42, vt.Result);
+ }
+
+ [Fact]
+ public void SetException_BeforeAccessTask_ValueTaskContainsValue()
@cston
cston Jul 21, 2016

Should the suffix be something like _IsFaulted rather than _ValueTaskContainsValue? Same question for test below.

@stephentoub
stephentoub Jul 21, 2016 Member

Yup, copy/paste error in the name. Will fix. Thanks.

@cston
cston commented Jul 21, 2016

LGTM

@mellinoe mellinoe and 1 other commented on an outdated diff Jul 21, 2016
....Extensions/tests/AsyncValueTaskMethodBuilderTests.cs
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+using Xunit;
+
+namespace System.Threading.Tasks.Tests
+{
+ public class AsyncValueTaskMethodBuilderTests
+ {
+ [Fact]
+ public void Create_ReturnsDefaultInstance()
+ {
+ AsyncValueTaskMethodBuilder<int> b = ValueTask<int>.CreateAsyncMethodBuilder();
+ Assert.Equal(default(AsyncValueTaskMethodBuilder<int>), b);
@mellinoe
mellinoe Jul 21, 2016 Member

This was a bit confusing to me at first, because AsyncValueTaskMethodBuilder.Create does assign to its _methodBuilder field. But it turns out that it assigns it to default anyways (or technically calls a method returning default and assigns that). I guess we would need to change this test if that ended up returning something else? (There's a comment suggesting we might here)

@stephentoub
stephentoub Jul 21, 2016 Member

I guess we would need to change this test if that ended up returning something else?

Yup

@mellinoe
Member

LGTM

@stephentoub stephentoub Add AsyncValueTaskMethodBuilder in support of Roslyn feature
The C# compiler just merged a feature to allow arbitrary Task-like types in the return position of async methods.  In support of that, this commit adds the necessary builder to allow ```ValueTask<T>``` to be used in this way.
0458fc9
@stephentoub stephentoub and 1 other commented on an outdated diff Jul 21, 2016
...ntime/CompilerServices/AsyncValueTaskMethodBuilder.cs
+
+ /// <summary>Creates an instance of the <see cref="AsyncValueTaskMethodBuilder{TResult}"/> struct.</summary>
+ /// <returns>The initialized instance.</returns>
+ public static AsyncValueTaskMethodBuilder<TResult> Create() =>
+ new AsyncValueTaskMethodBuilder<TResult>() { _methodBuilder = AsyncTaskMethodBuilder<TResult>.Create() };
+
+ /// <summary>Begins running the builder with the associated state machine.</summary>
+ /// <typeparam name="TStateMachine">The type of the state machine.</typeparam>
+ /// <param name="stateMachine">The state machine instance, passed by reference.</param>
+ public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
+ {
+ // We can't provide the same semantics as AsyncTaskMethodBuilder with regards to
+ // ExecutionContext as we don't have access to any of the relevant methods. We also can't
+ // call _methodBuilder.Start, as then we'll always end up creating a Task<T>. At the moment the
+ // only solution is to skip the ExecutionContext barrier that we'd want to put in place.
+ stateMachine.MoveNext();
@stephentoub
stephentoub Jul 21, 2016 Member

@ericeil, not to block this PR, but for subsequently, any thoughts about what to do here?

Outside of mscorlib, the options are relatively limited. If we were to bump up to the latest .NET Standard version, we could use ExecutionContext.Capture/Run to simulate what's being done internally in AsyncTaskMethodBuilder.Start, but that seems unfortunately expensive, at least for desktop, and it means either we don't support earlier standards, or we have multiple builds and end up with different behaviors based on which standard you're targeting.

I'm tempted to leave this as a wart for now. The impact as you know will be that any ExecutionContext-related changes that occur in the async method prior to it yielding will be visible to the caller, but none of the options seem particularly good.

@ericeil
ericeil Jul 21, 2016 Contributor

Could you just replace the call to MoveNext with this?

AsyncTaskMethodBuilder.Create().Start(ref stateMachine);
@ericeil
ericeil Jul 21, 2016 Contributor

At least in the CoreCLR implementation, that looks like it would do the trick without allocating unnecessarily.

@stephentoub
stephentoub Jul 21, 2016 Member

Could you just replace the call to MoveNext with this?

I'd convinced myself that the answer was "no", that doing so would cause it to access the AsyncTaskMethodBuilder's Task and force the allocation that all of this is trying to avoid. But looking at it again, Start's whole purpose is really just to wrap the stateMachine's invocation exactly the way we want to, so it should be fine. I'll verify, but... I think this works :)

@stephentoub
stephentoub Jul 21, 2016 Member

Thanks, Eric! Fixed.

@stephentoub stephentoub Fix AsyncValueTaskMethodBuilder.Start behavior with ExecutionContext
309b2d2
@stephentoub
Member

Thanks, all.

@stephentoub stephentoub merged commit ce3e10f into dotnet:master Jul 22, 2016

8 checks passed

Innerloop CentOS7.1 Debug Build and Test Build finished.
Details
Innerloop CentOS7.1 Release Build and Test Build finished.
Details
Innerloop OSX Debug Build and Test Build finished.
Details
Innerloop OSX Release Build and Test Build finished.
Details
Innerloop Ubuntu14.04 Debug Build and Test Build finished.
Details
Innerloop Ubuntu14.04 Release Build and Test Build finished.
Details
Innerloop Windows_NT Debug Build and Test Build finished.
Details
Innerloop Windows_NT Release Build and Test Build finished.
Details
@stephentoub stephentoub deleted the stephentoub:valuetask_builder branch Jul 22, 2016
@jamesqo jamesqo commented on the diff Jul 26, 2016
...ntime/CompilerServices/AsyncValueTaskMethodBuilder.cs
+
+namespace System.Runtime.CompilerServices
+{
+ /// <summary>Represents a builder for asynchronous methods that returns a <see cref="ValueTask{TResult}"/>.</summary>
+ /// <typeparam name="TResult">The type of the result.</typeparam>
+ [StructLayout(LayoutKind.Auto)]
+ public struct AsyncValueTaskMethodBuilder<TResult>
+ {
+ /// <summary>The <see cref="AsyncTaskMethodBuilder{TResult}"/> to which most operations are delegated.</summary>
+ private AsyncTaskMethodBuilder<TResult> _methodBuilder;
+ /// <summary>The result for this builder, if it's completed before any awaits occur.</summary>
+ private TResult _result;
+ /// <summary>true if <see cref="_result"/> contains the synchronous result for the async method; otherwise, false.</summary>
+ private bool _haveResult;
+ /// <summary>true if the builder should be used for setting/getting the result; otherwise, false.</summary>
+ private bool _useBuilder;
@jamesqo
jamesqo Jul 26, 2016 Collaborator

Just curious, maybe these two bools could be aggregated into an int instead? If I remember correctly, bools actually take up 4 bytes since the CLR pads them to align field accesses. Maybe it could be changed to a single field (byte, short, or int) that uses flags.

@stephentoub
stephentoub Jul 26, 2016 Member

Making these bools into an int will make it worse if the TResult is two bytes or less; on 32-bit, the TResult and the two bools can then be placed in the same word. Other than that, on both 32-bit and 64-bit, size-wise it's the same, and using an int would just make the code more complicated.

@karelz karelz modified the milestone: 1.1.0 Dec 3, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment