Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion thread for arbitrary async returns #10902

Closed
ljw1004 opened this issue Apr 26, 2016 · 67 comments
Closed

Discussion thread for arbitrary async returns #10902

ljw1004 opened this issue Apr 26, 2016 · 67 comments

Comments

@ljw1004
Copy link
Contributor

ljw1004 commented Apr 26, 2016

This is a discussion thread for arbitrary async returns, as specified in https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md

You can try it out online! tryroslyn.azurewebsites.net

Please check the spec and design rationale before adding your comment - it might have been addressed in the latest draft.

(The previous discussion thread was here: #7169. I closed that thread after we more or less settled on a proposal.)

@benaadams
Copy link
Member

AsyncEnumerator? Not quite sure how the syntax would go (e.g. where await is) Kind smilar or same as the Observable.

foreach (var row in await dataStream)
{
    // ...
}

@ljw1004
Copy link
Contributor Author

ljw1004 commented Apr 27, 2016

@benaadams, that's for a different topic: #261

@ashmind
Copy link
Contributor

ashmind commented Apr 28, 2016

@ljw1004 Just FYI, I've added your branch to TryRoslyn, e.g. http://is.gd/Yjvb2P.
Can't promise timely builds at the moment, but might be easier for a demo than a VSIX download.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Apr 28, 2016

@ashmind That is totally awesome! I've updated links.

@ljw1004
Copy link
Contributor Author

ljw1004 commented May 1, 2016

I'm having doubts about the [Tasklike] attribute to distinguish a type as being tasklike. I amended the prototype so it also supports a static method on the tasklike type itself, which seems a lot cleaner...

class MyTasklike<T>
{
   [EditorBrowsable(EditorBrowsable.Never)]
   public static MyBuilder<T> GetAsyncMethodBuilder() => ...
}

The problem is that this mechanism can never work for tasklike interfaces like IObservable and IAsyncAction and ITask.

@benaadams
Copy link
Member

@ljw1004 could it pick up interfaces via extension? e.g.

static class MyTasklikeExtensions<T>
{
    [EditorBrowsable(EditorBrowsable.Never)]
    public static MyBuilder<T> GetAsyncMethodBuilder(this IAsyncAction asyncAction) => ...
}

@ljw1004
Copy link
Contributor Author

ljw1004 commented May 1, 2016

We'd discussed this when we first implemented async and were pretty down on the idea of an extension method where you deliberately have to pass a null this argument...

I'm also a bit worried that the question of whether something is tasklike is no longer an inherent property of the type but instead depends on the context you're looking it up from. I think this context information has never previously had to be threaded into this part of overload resolution.

More notes on the subject: https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-connection-between-tasklike-and-builder

@benaadams
Copy link
Member

@ljw1004 quite tricky! I suppose as long it doesn't preclude a future use on interfaces.

It could be deferred and either be resolved as option 1, or in conjunction with one of the other interface proposals as a follow up, after seeing where they go?

e.g. Proposal: Virtual extension methods #258, Proposal: Static Interfaces #2204

@chkimes
Copy link

chkimes commented May 3, 2016

I'm loving this. Having access to the task-like type construction lets you do all sorts of things that aren't really in the spirit of async but are cool nonetheless.

For example, I threw together an implementation of NullableTaskLike that acts like Haskell's Maybe monad with do-notation. You can check it out here:

https://github.com/ckimes89/arbitrary-async-return-nullable/

edit: I should note that the current implementation isn't compatible with asynchronous Tasklikes.

@benaadams
Copy link
Member

An addition to the rational of why things like ValueTask are important.

Due to async's viral nature its never just one async method but a whole stack chain of async methods.

So if one method at the bottom of the chain is actually async and the call chain is 30 methods deep with each on awaiting the method below; that's 30 tasks that are being allocated and awaited with associated costs.

@aL3891
Copy link

aL3891 commented May 9, 2016

Love to see this as well, it always felt a little off to me that async, the language feature, had a dependency on a specific type instead of a pattern as is common with most of the other language features

@chkimes
Copy link

chkimes commented May 9, 2016

@aL3891 Well there's precedent for the language having a dependency on a type, given how yield works with IEnumerable. However, I'd like to see that given the same treatment as async/await and Task.

@aL3891
Copy link

aL3891 commented May 9, 2016

True, but at least that an interface :)

@chkimes
Copy link

chkimes commented May 12, 2016

Continuing on the theme of abusing this feature, I managed to mimic yield/IEnumerable with Tasklikes:

static void Main()
{
    var list = DoListThings();
    foreach(var item in list)
        Console.WriteLine(item);

    // prints 1, 2, 3
}

static async ListTaskLike<int> DoListThings()
{
    await ListTaskLike.Yield(1);
    await ListTaskLike.Yield(2);
    await ListTaskLike.Yield(3);
    return await ListTaskLike.Break<int>();
}

Also, it's got a neat little monadic property to it as well in that you can await child ListTaskLikes and all of the values will be included (like LINQ's SelectMany). Doing this in C# with yield requires a foreach loop:

static async ListTaskLike<int> DoListThings()
{
    await ListTaskLike.Yield(1);
    await DoNestedListThings();
    return await ListTaskLike.Break<int>();
}

static async ListTaskLike<int> DoNestedListThings()
{
    await ListTaskLike.Yield(2);
    await ListTaskLike.Yield(3);
    return await ListTaskLike.Break<int>();
}

foreach(var item in DoListThings())
    Console.WriteLine(item);

// prints 1, 2, 3

Code is here: https://github.com/ckimes89/arbitrary-async-return-nullable/blob/list-tasklike/NullableTaskLike/ListTaskLike.cs

@ljw1004
Copy link
Contributor Author

ljw1004 commented May 16, 2016

I want to step back and take stock of the key open issues remaining in the design rationale:

Q1. How to identify a tasklike, and find its builder? [link]

  • A public static GetAsyncMethodBuilder() method on the tasklike
  • An extension method GetAsyncMethodBuilder() method on default(Tasklike)
  • An attribute on the tasklike, named either TasklikeAttribute or AsyncMethodBuilderAttribute
  • An attribute on the async method

Q2. Can we reduce the number of heap allocations? [link]

  • Currently it allocates two heap objects on the cold path -- (1) the returned Task itself, (2) the boxed state machine struct, of which the builder struct is a field.
  • I wonder if we can do better and allocate only one heap object -- the builder, which coincidentally is also the returned tasklike type, and which contains the state machine struct as a field?

Q3. Should await even be handled by the builder? [link]

  • We could say that a tasklike's builder is solely a Tasklike Completion Source, and leave the job of AwaitOnCompleted to be hard-coded by the compiler+BCL in terms of how they capture execution context and box.
  • Or we could say that this job has to be done by each builder

Q4. Can an async method body interact with its builder? [link]

  • I'd be interested to make async a reserved keyword inside async methods, similar to this and base, and have it refer to the Async property of the builder.
  • That way you could get hold of things like async.CancellationToken or async.Progress, if your tasklike is one where cancellation and progress are inherent to the tasklike rather than parameters to the method.

Q5. Debugging support missing? [link]

  • If we ship arbitrary-async-returns in C#7, I think it's unlikely there'd be enough time for the debugger team to support them in the same release (for async callstacks, for debug-step-over-await, for the Tasks window).
  • Is it better to ship the feature first and wait for debugging tools to catch up, or delay until we can ship them both simultaneously?

Q6. Dynamic? [link]

  • I think it's fine to skip adding Tasklike support to the late binder.

Q7. IObservable? [link]

  • I don't think we've yet figured out the full story for IObservable. Probably that will rely upon async iterators.
  • Is it okay to ship arbitrary-async-returns before we've completed the design for IObservable and async iterators?

Q8. Back-compat breaks? [link]

  • If you're using a tasklike type (e.g. ValueTask) from a C#6 app, and then upgrade to C#7, the upgrade process might break your code
  • I don't know how to deal with this, or whether it's acceptable.

@benaadams
Copy link
Member

benaadams commented May 16, 2016

Q2. Always nice 😸
Q5. Soft launch feature with C#7, big announce when debugging working?
Q6. dynamic would defeat a lot of the advantages anyway?
Q7. suppose it depends on probably of a better design arising for arbitrary-async-returns based on the further exploration of IObservable and async iterators.

Arbitrary-async-returns so far is a very powerful feature by itself and stands on its own merits.

However, if there is likely to be conflict between the two designs then might be cause for pause.

Would really like the arbitrary-async-returns though so it would make me 😢

@chkimes
Copy link

chkimes commented May 16, 2016

Q1. How to identify a tasklike, and find its builder?

I'm a big fan of the static method since it seems way cleaner, but as you mentioned elsewhere it doesn't work with interfaces. It could be combined with another way of specifying the builder, but it seems really goofy to have two different ways to do it.

If a builder can be specified by an extension method, are there any requirements on where that extension method is located (e.g. same namespace, assembly)? If not, how are conflicts resolved? Normal extension methods can be invoked statically but that's obviously not going to work here. Here, I lean towards enforcing that the extension method is defined in the same assembly and namespace. It doesn't allow you to extend third-party types, but I'd argue that it's up to the designer of the type whether or not it should be used as an async return. Also, if you want to extend a third-party type then you can still just extend the actual type and create your own builder.

It seems like an attribute would work for the majority of cases, but it disallows some edge case uses of builder construction. For example, with an extension or static class method you could return different concrete instances of builders, switched on the generic type, that may have some type-specific behavior or optimizations. It seems like an unlikely use case, but it's completely precluded when using an attribute. EDIT: Disregard this paragraph, forgot that the attributed type requires a static Create method.

I don't like attributing the async method at all. It's incredibly cumbersome.

@chkimes
Copy link

chkimes commented May 16, 2016

Q3. Should await even be handled by the builder?

I'm a bit torn on this one. On the one hand, implementing AwaitOnCompleted correctly will probably have plenty of landmines for an implementer to trip on so they shouldn't be exposed to what amounts to an implementation detail. On the other hand, you would lose the ability to do some of the weirder stuff I did like the ListTaskLike (though it could be argued that that's a benefit).

Is there some middle ground, like inheriting from a TasklikeBuilder class that has some methods implemented for you already?

@bbarry
Copy link

bbarry commented May 16, 2016

Q2. Maybe possible for some new tasklikes that haven't yet been written? I think you would need to inline some of the stuff in AsyncMethodBuilder.cs from the clr to get the cold path down to one allocation (and keep a hot path at zero). A bunch of that code is internal though.

Q3. By default we can shell out to an async method builder that already exists (this is what I did in the enumerable value task builder), but allowing us to inject at this point on the ASM is nice.

Q4. I think intellisense/tooling around this is at least as important as debugging features.

Q6. Would not changing the late-binder now introduce backwards compat issues if/when we do consider changing it?

Q7. Option 2 sounds right from my perspective and should be related to IAsyncEnumerable features I think. On the other hand, why not both? In the IAsyncEnumerable code it feels a little strange to call .Start() on the builder when that doesn't actually start anything until the first bool e.MoveNext() is called. I think a better solution would be producing

public IObservable<string> Option1()
{
  var m = new SM1();
  m.State = -1;
  m.Builder = ObservableBuilder<string>.Create();
  m.Builder.Start(ref machine);
  return m.Builder.Task;
}

by virtue of not having any yield in it and having return <string> and

public IObservable<string> Option2()
{
  var m = new SM2();
  m.State = -1;
  m.Builder = ObservableBuilder<string>.Create();
  m.Builder.Prepare(ref machine);
  return m.Builder.TaskFactory;
}

when yield is there?

@ljw1004
Copy link
Contributor Author

ljw1004 commented May 16, 2016

I added a new "key question":

Q8. Back-compat breaks? [link]

  • If you're using a tasklike type (e.g. ValueTask) from a C#6 app, and then upgrade to C#7, the upgrade process might break your code
  • I don't know how to deal with this, or whether it's acceptable.
// Example 1: in C#6 this code compiles fine and picks the first overload,
// but in C#7 it picks the second for being better
void f(Func<Task<double>> lambda)
void f(Func<ValueTask<int>> lambda)
f(async () => 3);

// Example 2: in C#6 this code compiles fine and picks the first overload,
// but in C#7 it gives an ambiguity error
void g(Func<Task<int>> lambda)
void g(Func<ValueTask<int>> lambda)
g(async () => 3);

// Example 3: in C#6 this code compiles fine and picks the first overload with T=Task<int>,
// but in C#7 it picks the second with T=int for being more specific.
void h<T>(Func<T> lambda)
void h<T>(Func<ValueTask<T>> lambda)
h(async () => 3);

@ljw1004
Copy link
Contributor Author

ljw1004 commented May 16, 2016

@bbarry could you spell out what additional "intellisense/tooling" you're thinking about, beyond debugger support?

@bbarry
Copy link

bbarry commented May 16, 2016

Editor features mostly, for example on the method:

async IAsyncActionWithProgress<string> TestAsync(HttpClient client)
{
   await client.GetStringAsync(url, async.CancellationToken);
   async.Progress?.Invoke("10%");
}

It would be nice if the keyword async identified as an instance of some IAsyncActionWithProgressAsync<string> class (because that is what the builder's Async property is) for the purposes of hover help text, property and method discovery and so on.

@benaadams
Copy link
Member

benaadams commented May 16, 2016

Q8. should it break? In that the ValueTask path would all be sync currently with no use of await. Or will awaiting on a ValueTask change in behaviour?

If it did break, users of ValueTask are innovators and it won't have hit early adopters yet (using the Technology adoption life cycle terms) and would likely see this as an acceptable breaking change for the gain in being able to use them in async methods; which increases their utility enormously and opens the door for wider adoption.

However... if the await has breaking behaviour then there might be upstream breaks on people that use the libraries; but haven't written the code?

Which might mean new type other than ValueTask that has the same functionality StructTask (Not so good though and maybe confusing having two)

@benaadams
Copy link
Member

benaadams commented May 16, 2016

What I was trying to express is better summarised by: What would compat issues mean cross version e.g. C#6 app using C#7 lib and C#7 app using C#6 lib

@ufcpp
Copy link
Contributor

ufcpp commented Aug 24, 2016

I don't like an attribute-based approach because the following disadvantages are too large.

  • That could only be done by the authors
  • a cumbersome story about where and when to ship a package

@ashmind
Copy link
Contributor

ashmind commented Aug 24, 2016

Two cents on the modifier approach -- if you go with explicit annotation, why not use [Tasklike] directly instead of a keyword (tasklike or async)? After all there is no keyword for [Serializable] or [CallerMemberName] and this feature feels even more edge-case than those.

Also I think static extension methods are not the only future extensibility story. I'm sure there are cases for extension attributes as well (even though it might require a framework change).

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 24, 2016

@ashmind For whatever reason there's a strong reluctance in C# to let attributes influence language semantics. That's why C# uses "this" keyword to indicate an extension method, rather than [Extension] like VB does.

@ashmind
Copy link
Contributor

ashmind commented Aug 24, 2016

@ljw1004 It would be great if C# team described this goal and its exceptions — e.g. tasklike vs caller attributes. This would be useful for future issues as well. For example if the choice is based on target audience or usage frequency, this issue seems OK — advanced feature, likely to be used for only a few classes.

@bbarry
Copy link

bbarry commented Aug 24, 2016

Disadvantage: Proposal mixed allows for a goofy state where a type looks and feels and smells tasklike in some cases, but not in others.

I think this alone is enough to discount the mixed proposal.

If the modifier proposal went ahead, I think it would be best to accept both a keyword modifier and an attribute modifier form; both async class MyTask and [Tasklike] class MyTask should work within and across assemblies. In addition if this is how it is implemented, it would be nice to have a (separate proposal... wholly optional) attribute on async methods when they get compiled that could also be used in place of the keyword there (there is a bit of reflection wierdness when looking at a method and determining if it is intended to be awaited).

As to a decision of async being a reserved contextual keyword vs a magic identifier, I'd prefer a keyword, but that sounds like it might be too much of a breaking change. Either it would not be a contextual keyword in async Task<> methods (only in non-Task tasklikes?), or it would potentially conflict with variables named async as well as members and types in scope in existing code.

@gafter
Copy link
Member

gafter commented Aug 24, 2016

@ashmind Caller attributes do not affect the language's static semantics.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 25, 2016

C# LDM notes from 2016.08.24 -- topic "how to identify a tasklike"

This was the topic for yesterday's LDM. We rejected my three proposals, and picked up the suggestion of @ashmind above (which indeed several people have independently proposed). Thanks @ashmind. I'll be updating the feature spec shortly. In the meantime, here are the raw notes from LDM.

DECISION: USE AN ATTRIBUTE

Previously we recognized a tasklike by the presence of a static CreateAsyncMethodBuilder() on it. Now instead we want to recognize it by an attribute: e.g.

// non-generic tasklike
[AsyncBuilder(typeof(MyBuilder))] struct MyTasklike {...}
struct MyBuilder { public static MyBuilder Create() {...} }

// generic tasklike
[AsyncBuilder(typeof(MyBuilder<>))] struct MyTasklike<T> {...}
struct MyBuilder<T> { public static MyBuilder<T> Create() {...} }

namespace System.Runtime.CompilerServices {
    internal class AsyncBuilderAttribute : Attribute {
        public AsyncBuilderAttribute(Type t) { }
    }
}

If an attribute with the fully-qualified name System.Runtime.CompilerServices.AsyncBuilderAttribute is present on a type, and that type has arity 0 or 1 or is Task or Task<T>, then the type is considered to be tasklike.

The typeof argument to the attribute is called the tasklike's builder type.

The attribute can be defined anywhere. The compiler merely checks for it by name. One interesting thing about the way C# works, you can have internal attribute classes (as shown in the above example). It doesn't have to be internal but we recommend this is how to write tasklike types in a library DLL. When a user writes a project that references your DLL, the compiler will know that your type is tasklike, but the user's code won't be able to use that attribute. (that said, it's possible that a future version of mscorlib will include the attribute as public, and so if you also declare it in your library you'll get a compile-time error; at that time you'd have to split into two versions of your library.)

Requirement 1: a tasklike's builder type must have the same generic arity as the tasklike type itself.

Requirement 2: a builder type must have a public static parameterless method "static BuilderType Create()" which returns a value of the builder type.

Requirement 3: the attribute must have a constructor which takes a System.Type

It's implementation-defined whether the above two requirements are validated when we declare a tasklike type or validated when we declare an async method/lambda that returns that tasklike type. If the former, then we would also have to validate the requirements upon meta-import of a tasklike type, and deem it non-tasklike if the requirements aren't met. I prefer the latter. (Requirement 3 might instead be rolled into the test of whether a type is tasklike).

Discussion

We had discussed the idea of using attributes early on, and indeed my first prototype used attributes. In my evaluation at the time I had said "it's ugly" and "it requires us to ship the attribute". The second objection proved false because of the "internal" trick described above, which I hadn't known. The first objection -- well, it is ugly, but we also came to feel the alternatives were a little ugly too.

Should we have used a modifier like async(typeof(BuilderType<>)) rather than an attribute? Maybe, but that would be a crazy amount of language design work for a tiny audience. We estimate that in the eventual C# ecosystem maybe 5 people will write tasklike types that get mainstream adoption. (Most people will just write async methods that produce pre-existing tasklikes such as ValueTask).

What about the objection from people like @gafter that it's wrong for an attribute to influence the static semantics of the language (i.e. typing, type inference, type-directed overload resolution)? Well, @gafter was mollified by the thought that whenever he sees a tasklike type with the attribute, he'll close his eyes and imagine his "happy place" where it really is a modifier. Only the above 5 people will write it as an attribute anyway.

The attribute has one neat benefit -- it can be used on WinRT types like IAsyncAction as-is, without requiring an additional language feature "extension statics". I will kick off a discussion with the WinRT and CLR-projection-layer folks to see if that's possible.

There's also a downside to the attribute. It means that you can't take someone else's pre-existing type and make it tasklike via "extension statics" or similar, as noted by @ufcpp. That is a shame. We figure that the need for this is pretty small. It will be ameliorated if the type authors themselves do the work in a timely fashion (e.g. like IAsyncAction above), and by the paragraph below...

We discussed one avenue for future expansion that was advocated by @stephentoub, and I think @bbarry that this is what you described too. Imagine if you wanted to return a pre-existing type, but for sake of your own method, you want to pick a different builder type. We might decide to re-use the attribute for this purpose but place it on the async method rather than the tasklike type. (This won't work for async lambdas of course since they have no place to hang the attribute; it's also not clear if we'd limit this feature solely to return types which are already considered tasklike).

[AsyncBuilder(typeof(MyOwnTaskBuilder))]
async Task FredAsync() {...}

[AsyncBuilder(typeof(MyOwnBufferBlockBuilder))]
async BufferBlock<int> JonesAsync() {...}

We discussed another avenue for future expansion that was described by @MadsTorgersen. The previous proposal let us have a builder type whose generic arity differed from that of the tasklike. If we ever decide that this flexibility is desirable in future, we're open to achieving the same thing with a [AsyncBuilderFactory] attribute.

// Previous proposal:
public class MyTasklike<T>
{
   public static MyBuilder<string,int,T> CreateAsyncBuilder() => ...
}

// How that might be done with attributes
[AsyncBuilderFactory(typeof(BF))] public class MyTasklike<T> {...}

internal struct BF
{
  public static MyBuilder<string,int,T> Create<T>(MyTasklike<T> dummy) => ...
  // The compiler would construct default(tasklike), and invoke this function with it,
  // and that's how it would get an instance of the builder.
}

@bbarry
Copy link

bbarry commented Aug 25, 2016

Yeah it could be nice to be able to write:

[AsyncBuilder(typeof(MyOwnBufferBlockBuilder))]
async BufferBlock<int> JonesAsync() {...}

though it isn't exactly what I was suggesting (I did think it might be nice but left that out of my comment as extra fluff that might be distracting). As you have said, it is likely there might be 5 people who write their own tasklikes. I suspect it is more likely that there will be several people who come up with interesting unforseen abuses of the system to do things like @ckimes89's nullable stuff above, so this level of customization seems unnecessary.

I was suggesting that the compiler adds such an attribute when building the method to annotate that the method is in fact built by some async builder to reflection. There is a slight tooling issue today in that Task is a return type used both for async and for concurrency, but unless the author decides to follow conventions and name their method *Async there is no way for a user to know the method is intended to be awaited (vs a background compute task or an unstarted task). Thus I was suggesting that an async method get an attribute when compiled:

user writes:

public static async Task Foo() { await ...; }

builds as:

[AsyncBuilder(typeof(TaskBuilder))]
public static Task Foo() ...

And the user (and tools) can see this attribute via reflection and present a richer experience when suggesting await in code analysis and editing.

Further I was suggesting it would be nice for authors to be able to omit the async keyword if they provide the attribute:

[AsyncBuilder(typeof(TaskBuilder))]
public static Task Foo() { await ...; }

(in this hypothetical, the attribute and keyword are interchangeable and if both are used, the attribute wins)

Impact on existing libraries would be perhaps worse tooling interactions (though suggesting an await pattern on every use of Task today is wrong as well).

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 25, 2016

@bbarry wrote:

There is a slight tooling issue today in that Task is a return type used both for async and for concurrency, but unless the author decides to follow conventions and name their method *Async there is no way for a user to know the method is intended to be awaited (vs a background compute task or an unstarted task).

I'm suspicious of that... I think you generally still want to await a background compute task or (after starting it) an unstarted task.

Could you spell out (maybe with concrete examples) what kinds of things are intended to be awaited and which ones aren't?

[this is a derail from the topic of this issue, but I figure we can continue with it for a couple of posts before it merits its own issue...]

@svick
Copy link
Contributor

svick commented Aug 25, 2016

@ljw1004

(that said, it's possible that a future version of mscorlib will include the attribute as public, and so when you write

Did you mean to continue this sentence?

@bbarry
Copy link

bbarry commented Aug 25, 2016

suppose I was writing some parallel code:

static int ParallelTaskImageProcessing(Bitmap source1, 
   Bitmap source2, Bitmap layer1, Bitmap layer2, 
   Graphics blender)
{
  Task toGray = Task.Factory.StartNew(() => 
     SetToGray(source1, layer1));
  Task rotate = Task.Factory.StartNew(() => 
     Rotate(source2, layer2));
  Task.WaitAll(toGray, rotate);
  Blend(layer1, layer2, blender);
  return source1.Width;
}

Someone could have instead done this:

static int ParallelTaskImageProcessing(Bitmap source1, 
   Bitmap source2, Bitmap layer1, Bitmap layer2, 
   Graphics blender)
{
  Task toGray = SetToGray(source1, layer1);
  Task rotate = Rotate(source2, layer2);
  Task.WaitAll(toGray, rotate);
  Blend(layer1, layer2, blender);
  return source1.Width;
}

which could be written:

static Task<int> ParallelTaskImageProcessing(Bitmap source1, 
   Bitmap source2, Bitmap layer1, Bitmap layer2, 
   Graphics blender)
{
  return Task.WhenAll(
    SetToGray(source1, layer1),
    Rotate(source2, layer2)
    )
    .ContinueWith(t =>
    {
      Blend(layer1, layer2, blender);
      return source1.Width; 
    });
}

and could have a tool assisted conversion:

static async Task<int> ParallelTaskImageProcessing(Bitmap source1, 
   Bitmap source2, Bitmap layer1, Bitmap layer2, 
   Graphics blender)
{
  await Task.WhenAll(
    SetToGray(source1, layer1),
    Rotate(source2, layer2)
    );
  Blend(layer1, layer2, blender);
  return source1.Width; 
}

I am fairly sure that would still work, but now the code is going through the machinery for SetContinuationForAwait instead of ContinueWith and is compiling a state machine and so on... which may not be a decision I want to make without a profiler around.

Then again perhaps I'm thinking too hard and shouldn't worry about trying to await IO bound workflows that are likely to wait and not awaiting CPU bound ones.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 26, 2016

@bbarry I think the only solution is convention and developer education. Consider a "mixed" async method:

async Task DoStuffAsync()
{
  var buf = await GetBufAsync();
  for (int i=0; i<buf.Length; i++) do_cpu_bound_stuff;
}

Should you be awaiting this or not? Ultimately the answer has nothing to do with whether or not the method was written with the async modifier. The answer is that, as an "abstraction" principle, you should write methods which are either CPU-light (and they may very well return an awaitable) or CPU-heavy (and they shouldn't block on IO and the caller should be the one to decide which and how many threads to allocate to them).

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 26, 2016

I've now implemented the [AsyncBuilder(typeof(builderType))] approach. During implementation I noticed a few things that we hadn't thought of. They're not important; I'm just calling them out for completeness.

When is something tasklike?

Which of the following are tasklike?

[AsyncBuilder("hello")] class Tasklike1 { }

[AsyncBuilder(typeof(void))] class Tasklike2 { }

[AsyncBuilder(typeof(B3))] class Tasklike3 { }
class B3<T> { } // generic arity differs from that of tasklike

[AsyncBuilder(typeof(B4))] class Tasklike4 { }
class B4<T> { private static string Create() => null; ... } // Create method returns wrong type

I adopted the following rule: A type is considered tasklike if it has the AsyncBuilder(System.Type) attribute constructor on it. All of the above are tasklike types. They generate other errors of course...

  • Tasklike1 generates the error that a string literal can't be converted to System.Type.
  • Tasklikes2/3 generate an error (only when you try to declare a Tasklike2/3-returning async method) because void isn't a good builder
  • Tasklike4 generates an error (only when you try to declare a Tasklike4-returning async method) because it is looking for a public Create method with the exact right signature

Nesting and generics

Which of the following should be allowed?

class Outer<T>
{
   class Builder { }
   [AsyncBuilder(typeof(Builder))] class Tasklike1 { }
   [AsyncBuilder(typeof(Outer<>.Builder))] class Tasklike2 { }
   [AsyncBuilder(typeof(Outer<T>.Builder))] class Tasklike3 { }
   [AsyncBuilder(typeof(Outer<int>.Builder))] class Tasklike4 { }
}
  • Tasklike1 is actually shorthand for Tasklike2 in the C# language.
  • I disallowed Tasklike2 because how do you know how to instantiate the generic type Outer<> ?
  • Tasklike3 is disallowed by the C# language because you're not allowed generic type arguments in attributes
  • Tasklike4 I arbitrarily disallowed in order to get a clean simple principle: The argument to AssyncBuilder() must not be contained in a generic class. There are several other places with a similar rule in C# so it felt clean.

The following is allowed, because the builder isn't in a generic class:

class Outer
{
   class Builder { }
   [AsyncBuilder(typeof(Outer.Builder))] class Tasklike { }
}

Accessibility

On the builder type, it looks for a public Create method and a public Task property with the correct signatures. I honestly couldn't be bothered spending effort on cases where they have more limited accessibility.

Can you can make the builder type itself have less accessibility, e.g. make it internal? That will allow your library to declare async methods that return your tasklike type, but no one else will be able to. I think this is a recipe for confusion and I'll disallow it.

@bbarry
Copy link

bbarry commented Aug 26, 2016

That will allow your library to declare async methods that return your tasklike type, but no one else will be able to. Is that okay? What do people think?

I think this is ok.

You are leveraging the machinery of the compiler to build a method in a particular way (as an instance of this state machine pattern). It is possible that the builder has no purpose in the public api or even that it requires particular knowledge to write and could be harmful for a casual user to build an async method with.

An example I am thinking is perhaps there is an unmanaged API out there which could otherwise be used in an async pattern, except that various parameters need to be configured before each await state. Given an async keyword someone might write something like this:

public async ITaskLike<Result> DoIt()
{
    var config = SetItUp();
    async.Configure(config);
    var step1 = await Step1();
    var nextconfig = Determine(config, step1);
    async.Configure(nextconfig);
    return await Step2();
}

where

[AsyncBuilder(typeof(BarTaskLikeBuilder<>))]
public interface ITaskLike<T> ...
internal sealed class BarTaskLike<T> : ITaskLike<T> ...
internal sealed class BarTaskLikeBuilder<T> ...

It would be possible to require the library to hand write the machinery here, but why should the compiler enforce such an arbitrary restriction?

@chkimes
Copy link

chkimes commented Aug 26, 2016

It seems strange to me to have a tasklike that only you can actually use as a tasklike. The reason for using an internal builder would be that it provides some useful functionality for you, but not useful enough that you would want to expose it to a caller of your library - which seems like a pretty strange situation. Also, given the attribute based approach, no caller of your library could introduce their own tasklike functionality for your ostensibly tasklike type.

On the other hand, allowing internal builder types is a superset of the functionality offered by requiring all public builder types, so it's not like we're sacrificing capabilities to support internal builders. If a strange situation arises (like the example from @bbarry) then you're not forced by the language to make a potentially unsafe API.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 31, 2016

I switched to requiring the builder to have the same accessibility as the tasklike... it's a minor issue, and the arguments in both directions (keep them the same vs allow them to be different) aren't all that major, and the implementation turned out to be easy+clean to require them to be the same.

The PR for the change is here: #13405

@jaredpar spotted a flaw with the attribute approach. We had proposed that a DLL which declares a tasklike type would also declare its own internal copy of the AsyncBuilderAttribute class. But consider what happens when you try to produce a reference assembly out of this DLL. You'd need to include the internal type! This seems crummy and against the spirit of either reference assemblies or the internal modifier or both.

So instead: we won't rely on the "internal" trick. Instead, ValueTask.dll will declare AsyncBuilderAttribute as public. Library authors who wish to declare tasklikes can either declare a public AsyncBuilderAttribute themselves too, or take a dependency on ValueTask, whichever is easier.

In future we still do anticipate adding AsyncBuilderAttribute to mscorlib (and to System.Runtime in the .NETCore nuget world). On such platforms, ValueTask would switch to having a type forwarder for its AsyncBuilderAttribute.

@svick
Copy link
Contributor

svick commented Aug 31, 2016

@ljw1004

Instead, ValueTask.dll will declare AsyncBuilderAttribute as public. Library authors who wish to declare tasklikes can either declare a public AsyncBuilderAttribute themselves too, or take a dependency on ValueTask, whichever is easier.

Won't that cause issues if I want to reference both ValueTask and a library that declares its own public AsyncBuilderAttribute? Something similar happened with [Serializable] in NUnit: https://github.com/dotnet/corefx/issues/10822.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Aug 31, 2016

@svick it will cause an issue, but only if you wish to refer to the attribute (i.e. if you're authoring a tasklike). If you merely write an async method that returns a tasklike, then your code won't refer to the attribute, and you won't suffer the ambiguity warning.

There will be far fewer people authoring tasklikes than writing [Serializable] data-structures!

I think "easier" cuts both ways in this case. I'd hope that tasklike authors will take care to optimize for ease of consumption of their tasklikes.

  • If I ship my tasklike in a NuGet library, then I could make my NuGet have a dependency on ValueTask nuget package. This will require a little more nuspec authoring effort on my part. It will be beautiful for users to consume who are using project.json projects. It will be a bit ugly for users to consume who are using packages.config.
  • If I ship my tasklike in just a DLL which people add references to, then I could declare the type public myself (easy for most folks to consume, but harder for those who declare their own tasklikes). Or I could rely upon users either adding their own reference to ValueTask or referencing a new enough version of .NET.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Sep 1, 2016

"Why would a type be considered task-like in overload resolution if the builder type is inaccessible?"

I want to delve into this question a little further.


The way I implemented is that the compiler calls a IsCustomTasklike() function, which merely checks for the presence of the [AsyncBuilder(System.Type)] attribute, nothing more. That’s a nice fast check (could even be done at meta-import time) and is independent of anything else. And I deferred all further validation of the builder type to the point where it’s actually needed for codegen to construct a tasklike-returning async method/lambda.

In this design, a type type can pass the IsCustomTasklike() test (as used in overload resolution) even despite having inaccessible builder.


Why should it be so? Why not come up with a different scheme, to which the question alludes, where IsCustomTasklike()does some more validation of builder (e.g. of its accessibility) and maybe even all validation (e.g. of its AwaitOnCompleted method)?

I didn’t go for this option because it would be a more costly check without benefit. And unless we do the full costly check, it would feel like a halfway-house. Halfway never feels right!


In C# Language Design Meeting, @gafter pushed for us not to use a static CreateAsyncMethodBuilder() because of the following argument, which folks generally agreed with:

  • The changes to OverloadResolution and TypeInference feel “languagey”. Whether or not a type participates in these in a tasklike way, feels like it should be an inherent property of the type -- not something affected by internal details. We seriously entertained a modified tasklike class C { } to control OverloadResolution+TypeInference for this reason, again independently of any implementation details about the builder.
  • The ability for an async method/lambda to return a tasklike feels “implementationy”. All it needs is a way to find a valid builder. The static CreateAsyncMethodBuilder() was a fine way to do this.

We settled on using an attribute to control both bullets, [AsyncBuilder(typeof(builder))] class Tasklike { }.

There are two opposing language design philosophies in play here: (1) the two bullets are conceptually different; (2) it's confusing if the two bullets are out of sync.

In respect of (2), I required builders have the same accessibility as the tasklike. This rules out the ability to have a type which consumers of your library think of in some technical ways as something that can be returned from an async method/lambda without themselves being able to return it from an async method/lambda.

In respect of (1), and in a design that only really is noticeable in error situations, I said that the first bullet is controlled solely by the presence of [AsyncBuilder(System.Type)] attribute irrespective of its argument, and the second bullet is controlled by its argument being valid.

One thing I like about this approach is that it lets us relax the rules for what is a valid builder type in future (e.g. allow it to be more generic, or covariant, or come from an inheritance hierarchy) without it being a breaking change. The typical way the language gets broken is because relaxed rules cause overload resolution changes. But, with this approach, that won't happen.

@ljw1004
Copy link
Contributor Author

ljw1004 commented Sep 7, 2016

Now that the final PR has been merged, I think it's time to close this thread.
#13405 (comment)

Thanks everyone for your help -- it's been a fun ride!

@roji
Copy link
Member

roji commented Jan 22, 2017

Thanks for this really great feature.

I'm about to release a new version of my package, and I wanted to be reasonably sure that this language feature isn't going to be rolled back at the last minute (as happened with some C# 6.0 features in VS2015). I understand we're still in RC and no guarantees can be made, but is it reasonable to depend on generalized async return types?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests