I have a simple ITimer
interface that has just a classic Elapsed
.NET event that is raised after the a certain timespan.
interface ITimer
{
void Start(TimeSpan interval);
void Stop();
public event EventHandler Elapsed;
}
Now I would like to create a functionality that is similar to Task.Delay(TimeSpan)
but it should use that ITimer
abstraction instead of the "real" time. Also different form Task.Delay(TimeSpan)
I though it might be a good idea to return a ValueTask
instead of a Task
because it could be implemented allocation-free and might generally be more performant. Also the completion of the delay only needs to be awaited once anyway.
Now a somewhat naive implementation could probably look like this. There problem here is, that there is no benefit of using a ValueTask
at all. How could an implementation look like that takes full benefit of what a ValueTask
promises (cheap and allocation-free).
ValueTask Delay(TimeSpan duration, CancellationToken cancellationToken)
{
// We get the ITimer instance from somwhere, doesn't matter for this question.
ITimer timer = timerFactory.Create();
var tcs = new TaskCompletionSource<bool>();
timer.Elapsed += (_, __) => { tcs.SetResult(true); };
cancellationToken.Register(() =>
{
timer.Stop();
throw new OperationCanceledException();
});
timer.Start(duration);
return new ValueTask(tcs.Task);
}
Not much has been written on
ManualResetValueTaskSourceCore<TResult>
. It's essentially aTaskCompletionSource<T>
but forValueTask<T>
.It's really intended to be used inside a type like your timer, but you can create a wrapper for it if you want to use it as an external method:
You can then use it as such (cancellation removed for simplicity):
Note that, since this is an external method, you'd still be allocating the
ManualResetValueTaskSource<T>
(as well as the delegate). So this doesn't really buy you anything, and I would choose to useTaskCompletionSource<T>
here. This is especially true when you addCancellationToken
support and have to handle the race condition between the timer firing (vts.SetResult
) and the cancellation (vts.SetException
), since I'm pretty sure only one of those is supposed to happen per ValueTask operation, and there's noTry*
variants in ourManualResetValueTaskSource<T>
likeTaskCompletionSource<T>
has.ValueTask
is really intended to be more of a first-class citizen; i.e., ideally you would update the actual timer implementation (and interface) to supportValueTask Delay(TimeSpan)
in its own class instead of an external method. Inside the timer instance, it can reuse theValueTask
backing type (ManualResetValueTaskSource<T>
), it can know when to callReset
, and it can avoid the delegate/event completely.