Today custom awaiters fall short for several reasons:
I haven't thought through an API proposal as yet, just throwing this out there for now.
Another alternative would be to expose Task from all APIs (like Cancellation token) but that doesn't seem like a very efficient solution.
/cc @stephentoub
For reference:
https://github.com/dotnet/corefxlab/blob/master/src/System.Threading.Tasks.Channels/System/Runtime/CompilerServices/IAwaiter.cs
Though I think the description in the issue may be conflating awaitables and awaiters. It sounds like the request is for an interface that could be implemented by any awaitable thing (rather than it just being pattern-based), which would then also lead to having an interface like that linked to above for an awaiter.
As an aside, certain combinators would lead to a more complicated interface, e.g. while WhenAll would be easily implementable on top of arbitrary awaiters, WhenAny would not be, at least not in a fashion that avoids leaks (the interface would need support for removing an awaiter as well).
It'd be helpful if you could share some of the key scenarios driving the issue, so that others could comment. As you say, the first item you call out is addressed as of C# 7. For the second and third, I'd be interested in understanding better what situations you have where you want to combine awaitable things of different types.
cc: @madstorgersen
For the second and third, I'd be interested in understanding better what situations you have where you want to combine awaitable things of different types.
I don't want to combine different types of awaitable though that might be interesting. I'm interested in using the same patterns across a number of types that provide the same functionality. For e.g.:
Replacing:
```C#
var cts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
With:
```C#
var token = await Awaitable.WhenAny(token1, token2);
I on the other hand would love to await WhenAny(task, token) or WhenAny(manualResetEventSlim, token) etc. as well as multiple cancelation tokens.
I like this idea. You could wait for both an event and a token. I am unsure how much work this would be though.
A simple workaround is Task.WhenAny(task, Task.Run(async () => await awaitable)) This still has the leak problem @stephentoub mentioned and has an additional task overhead.
There's a related work: https://github.com/jam40jeff/ITask
It is very useful for covariance.
@huoyaoyuan you can now support returning ITask directly from methods if you add support
:memo: One reason why we don't have an IAwaitable interface is you can't define such an interface to apply to all supported awaitable items. Specifically, the language allows GetResult() to have either of these signatures:
void GetResult();
T GetResult();
It is not possible to define a single (generic) interface which allows both of these definitions.
For today's api review it sounds like it wants to be something like?
// Task equivilent
public interface ITask
{
bool IsCompleted { get; }
bool IsCanceled { get; }
bool IsCompletedSuccessfully { get; }
bool IsFaulted { get; }
void GetResult();
AggregateException Exception { get; }
void OnCompleted(Action continuation);
void UnsafeOnCompleted(Action continuation);
}
public struct ValueAwaiter
{
private ITask _awaitable;
public ValueAwaiter(ITask awaitable)
{
_awaitable = awaitable;
}
public bool IsCompleted => _awaitable.IsCompleted;
public bool IsCompletedSuccessfully => _awaitable.IsCompletedSuccessfully;
public bool IsFaulted => _awaitable.IsFaulted;
public bool IsCanceled => _awaitable.IsCanceled;
public AggregateException Exception => _awaitable.Exception;
public ValueAwaitable GetAwaiter() => new ValueAwaitable(_awaitable);
// public ConfiguredAwaiter ConfigureAwait(bool continueOnCapturedContext);
public struct ValueAwaitable : ICriticalNotifyCompletion
{
private ITask _awaitable;
public ValueAwaitable(ITask awaitable)
{
_awaitable = awaitable;
}
public bool IsCompleted => _awaitable.IsCompleted;
public void GetResult() => _awaitable.GetResult();
public void OnCompleted(Action continuation) => _awaitable.OnCompleted(continuation);
public void UnsafeOnCompleted(Action continuation) => _awaitable.UnsafeOnCompleted(continuation);
}
}
// Task<TResult> equivilent
public interface ITask<out TResult>
{
bool IsCompleted { get; }
bool IsCanceled { get; }
bool IsCompletedSuccessfully { get; }
bool IsFaulted { get; }
TResult GetResult();
AggregateException Exception { get; }
void OnCompleted(Action continuation);
void UnsafeOnCompleted(Action continuation);
}
public struct ValueAwaiter<TResult>
{
private ITask<TResult> _awaitable;
public ValueAwaiter(ITask<TResult> awaitable)
{
_awaitable = awaitable;
}
public bool IsCompleted => _awaitable.IsCompleted;
public bool IsCompletedSuccessfully => _awaitable.IsCompletedSuccessfully;
public bool IsFaulted => _awaitable.IsFaulted;
public bool IsCanceled => _awaitable.IsCanceled;
public AggregateException Exception => _awaitable.Exception;
public ValueAwaitable GetAwaiter() => new ValueAwaitable(_awaitable);
// public ConfiguredAwaiter<TResult> ConfigureAwait(bool continueOnCapturedContext);
public struct ValueAwaitable : ICriticalNotifyCompletion
{
private ITask<TResult> _awaitable;
public ValueAwaitable(ITask<TResult> awaitable)
{
_awaitable = awaitable;
}
public bool IsCompleted => _awaitable.IsCompleted;
public TResult GetResult() => _awaitable.GetResult();
public void OnCompleted(Action continuation) => _awaitable.OnCompleted(continuation);
public void UnsafeOnCompleted(Action continuation) => _awaitable.UnsafeOnCompleted(continuation);
}
}
Struct wrappers are so ICriticalNotifyCompletion can be implemented multiple times by a c;lass; for different ITask, ITask<Result1>, ITask<Result2> implementations
e.g.
class Connection : ITask, ITask<ReadResult>, ITask<WriteResult>
{
ValueAwaiter<ReadResult> ReadAsync() => new ValueAwaiter<ReadResult>(this);
ValueAwaiter<WriteResult> WriteAsync(...) => new ValueAwaiter<WriteResult>(this);
ValueAwaiter WhenFinished() => new ValueAwaiter(this);
}
I'm interested to see how you think an API to await arbitrary events would look.
Rather than writing an umpteenth hardcoded-single-event awaitable and awaiter as structs, the general-purpose API (with the least heap overhead that I've come up with so far) takes an Action<Action<TEventArgs>> registerOneTimeEventCallback. Example wrapping of event: https://gist.github.com/jnm2/691bff06d499d904cc82b99aad89ef82#file-extensions-cs-L6
Always looking for improvements.
It is not possible to define a single (generic) interface which allows both of these definitions.
@sharwell @gafter know if default interface methods would allow?
public interface ITask<out TResult> : ITask
{
void ITask.GetResult() => GetResult();
new TResult GetResult();
}
If we're going to look at proposed/prototyped features to solve this problem elegantly, how about Shapes (https://github.com/dotnet/csharplang/issues/164)?
In theory, and if I have my understanding of the proposal correct, you could declare a shape as:
shape SAwaitable
{
SAwaiter GetAwaiter()
}
shape SAwaiter
{
bool IsCompleted { get; }
void OnCompleted(Action continuation);
void GetResult();
}
This would then work with any awaitable type that exists today, you wouldn't need to add a new interface and recompile.
However I think you would still need one variant for void GetResult() and another for TResult GetResult(), i.e.:
shape SAwaitable<TResult>
{
SAwaiter<TResult> GetAwaiter()
}
shape SAwaiter<TResult>
{
bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult();
}
Perhaps this could be resolved if shapes gets a keyword so that a shape can declare _"a method that returns something or is void, I really don't care. I only want to call it, not use the result"._
shape SAwaiter<TResult> : SAwaiter
{
void SAwaiter.GetResult() => GetResult();
TResult GetResult();
}
馃槈
I don't think shapes and DIM will be cross-compatible like that... but if they were, that _would_ solve this perfectly 馃槃
Would it help at all to have:?
shape SAwaiter
{
bool IsCompleted { get; }
void OnCompleted(Action continuation);
}
shape SVoidAwaiter : SAwaiter
{
void GetResult();
}
shape SResultAwaiter<TResult> : SAwaiter
{
TResult GetResult()
}
Or is that just a useless idea?
Trouble would be trying to construct a call like:
public static Awaiter WhenAll(params SVoidAwaiter[] awaiters)
Where you can use a mix of both types
In theory, WhenAll could just use SAwaiter, with something like:
public static class C
{
public static async Task WhenAll(params SAwaiter[] awaiters)
{
var cev = new CountdownEvent(awaiters.Length);
foreach (var awaiter in awaiters)
{
awaiter.OnCompleted(() => cev.Signal());
}
await cev.WaitHandle.AsTask().ConfigureAwait(false);
}
// Do some magic with ThreadPool.RegisterWaitForSingleObject
public static Task AsTask(this WaitHandle handle) => default;
}
You don't actually need the result in order to wait for it, do you?
Fun fact: The C# awaitable spec does not consider GetResult to be reponsible for waiting. In general you must always use IsCompleted and OnCompleted and provide your own blocking mechanism if you need to block. Only after IsCompleted is true or OnCompleted calls back should it be meaningful to call GetResult.
Again, that's for awaitables in general. TaskAwaiter is unusual in this regard, so if you know the type is Task it's still valid to do .GetAwaiter().GetResult() to block.
So I would expect an SAwaitable to be about the C# awaitable concept and have no guarantees around solely calling GetResult, but I would expect ITask to be about generalizing Task and I would probably expect the GetResult behavior to follow TaskAwaiter's.
Issues getting carried away with shapes and some cut an paste issues :)
Would be STask; awaiters would be structs
shape STask<TResult> : STask
{
void STask.GetResult() => GetResult();
TResult GetResult();
}
Closing this out since we now have ValueTask and we likely won't be adding an interface for this