dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.19k stars 4.72k forks source link

API proposal: Modern Timer API #31525

Closed davidfowl closed 3 years ago

davidfowl commented 4 years ago

To solve of the common issues with timers:

  1. The fact that it always captures the execution context is problematic for certain long lived operations (https://github.com/dotnet/corefx/issues/32866)
  2. The fact that is has strange rooting behavior based on the constructor used
  3. The fact timer callbacks can overlap (https://github.com/dotnet/corefx/issues/39313)
  4. The fact that timer callbacks aren't asynchronous which leads to people writing sync over async code

I propose we create a modern timer API based that basically solves all of these problems šŸ˜„ .

+namespace System.Threading
+{
+   public class AsyncTimer : IDisposable, IAsyncDisposable
+   {
+        public AsyncTimer(TimeSpan period);
+        public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken);
+        public void Stop();
+   }
+}

Usage:

class Program
{
    static async Task Main(string[] args)
    {
        var second = TimeSpan.FromSeconds(1);
        using var timer = new AsyncTimer(second);

        while (await timer.WaitForNextTickAsync())
        {
            Console.WriteLine($"Tick {DateTime.Now}")
        }
    }
}
public class WatchDog
{
    private CanceallationTokenSource _cts = new();
    private Task _timerTask;

    public void Start()
    {
        async Task DoStart()
        {
            try
            {
                await using var timer = new AsyncTimer(TimeSpan.FromSeconds(5));

                while (await timer.WaitForNextTickAsync(_cts.Token))
                {
                    await CheckPingAsync();
                }
            }
            catch (OperationCancelledException)
            {
            }
        }

        _timerTask = DoStart();
    }

    public async Task StopAsync()
    {
        _cts.Cancel();

        await _timerTask;

        _cts.Dispose();
    }
}

Risks

New Timer API, more choice, when to use over Task.Delay?

Alternatives

Alternative 1: IAsyncEnumerable

The issue with IAsyncEnumerable is that we don't have a non-generic version. In this case we don't need to return anything per iteration (the object here is basically void). There were also concerns raised around the fact that IAsyncEnumerable<T> is used for returning data and not so much for an async stream of events that don't have data.

public class Timer
{
+     public IAsyncEnumerable<object> Periodic(TimeSpan period);
}
class Program
{
    static async Task Main(string[] args)
    {
        var second = TimeSpan.FromSeconds(1);

        await foreach(var _ in Timer.Periodic(second))
        {
            Console.WriteLine($"Tick {DateTime.Now}")
        }
    }
}

Alternative 2: add methods to Timer

public class Timer
{
+     public Timer(TimeSpan period);
+     ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken);
}

cc @stephentoub

natalie-o-perret commented 4 years ago

I like the idea, but when it comes to stop the timer I found it a little bit weird (I mean I guess in your example, that would involve a break to get out of the foreach loop).

davidfowl commented 4 years ago

I like the idea, but when it comes to stop the timer I found it a little bit weird (I mean I guess in your example, that would involve a break to get out of the foreach loop).

Right, it's pull so you stop the timer by breaking out of the loop, or using a cancellation token with the IAsyncEnumerable to stop it from running (that also works if you're not directly in the loop).

NtFreX commented 4 years ago
public static class Timer
{
    public static async Task Subscribe(this IAsyncEnumerable<TimerSpan> ticker, Action<TimerSpan> action)
    {
        await foreach(var t in ticker)
        {
            action?.Invoke(t);
        }
    }
}

var second = TimeSpan.FromSeconds(1);
using(var ticker = Timer.CreateTimer(second, second))
{
    await ticker.Subscribe(t => Console.WriteLine($"Tick {t}"));
}

As long as we get some good helper methods simular to the one above I like fanciness.

davidfowl commented 4 years ago

The entire point of this is to avoid callbacks, but if you want to write that helper, have at it šŸ˜„

NicolasDorier commented 4 years ago

That is sexy. Though it is difficult to know from the look of it, what happen if a tick happen while we are not awaiting. Do they queue up so every tick will get served? or are they ignored if nobody here to listen to them?

ankitbko commented 4 years ago
public static class Timer
{
    public static async Task Subscribe(this IAsyncEnumerable<TimerSpan> ticker, Action<TimerSpan> action)
    {
        await foreach(var t in ticker)
        {
            action?.Invoke(t);
        }
    }
}

var second = TimeSpan.FromSeconds(1);
using(var ticker = Timer.CreateTimer(second, second))
{
    await ticker.Subscribe(t => Console.WriteLine($"Tick {t}"));
}

As long as we get some good helper methods simular to the one above I like fanciness.

Seems very similar to rx timer. Always felt rx should slowly integrate into coreclr/fx.

willdean commented 4 years ago

With apologies for the bike-shedding, but can I suggest it's called something other than Timer?

We already have these three famous ones: System.Threading.Timer System.Timers.Timer System.Windows.Forms.Timer Plus another few I didn't know about until I looked at Reference Source just now.

People don't always qualify which one they're talking about (#32866 is a good example), and adding another new one just makes things hard to learn for newcomers and hard to talk about.

If it was called Timer2020 then as long as there wasn't more than one new timer class added each year, that would be a scaleable naming scheme... (Not a serious suggestion, in case my audience is unappreciative.)

svick commented 4 years ago
public class Timer

I take it this is System.Threading.Timer, and not System.Timers.Timer, or some new namespace?

public static IAsyncEnumerable<TimerSpan> Create(TimeSpan dueTime, TimeSpan period);

Assuming this method is indeed added to System.Threading.Timer, I feel like calling it Create is not sufficient to differentiate it from the Timer constructor. Though I'm not sure what name would be better here.


In general, I agree that having await-based timer is useful, but I'm not sure IAsyncEnumerable<T> is the right abstraction for that.

For example, consider that I want to output items from a collection to the console, but only at a rate of one per second. With a different design, that could be easily achieved by something like:

using var timer = Timer.Create(TimeSpan.Zero, TimeSpan.FromSeconds(1));

foreach (var item in items)
{
    await timer.WaitForNext();

    Console.WriteLine(item);
}

But I think this would be much harder to write using an IAsyncEnumerable<T>-based timer, at least without Ix.NET.

vshapenko commented 4 years ago

Maybe this should be an extension to System.Threading.Channels?

davidfowl commented 4 years ago

@svick you can use IAsyncEnumerable for that as well. What you wrote is literally GetEnumerator + MoveNextAsync

alefranz commented 4 years ago
  • If user code takes longer to run than the interval, we drop the tick events that happened until the user can catch up (think bounded channel with drop and a capacity of 1).

That's definitely the right approach, but I think it would be nice to customize what happens when a tick is missed: return immediately on await vs wait for the next tick (so to get back in sync)

stephentoub commented 4 years ago

Just to make sure I understand the proposal, you're basically proposing this, right? (I'm not sure what the TimeSpan is meant to be an offset from, so I've just used DateTime here.)

public static IAsyncEnumerable<DateTime> Create(TimeSpan dueTime, TimeSpan period)
{
    var c = Channel.CreateBounded<DateTime>(1);
    using (new Timer(s => ((Channel<DateTime>)s).Writer.TryWrite(DateTime.UtcNow), c, dueTime, period))
        await foreach (DateTime dt in c.Reader.ReadAllAsync())
            yield return dt;
}

?

ankitbko commented 4 years ago

If we are open for new name, I would propose Interval.

But adding something similar to Timer under new name smells bad. Creates unnecessary confusion.

omariom commented 4 years ago

Now we need this

IAsyncEnumerable<Message> channel = channelReader.ReadAllAsync();
IAsyncEnumerable<TimeSpan> ticker = Timer.CreateTimer(second, second);

(channel, ticker) switch 
{
    TimeSpan tick => 
    {
    },
    Message msg => 
    {
    }
}

I can't find @stephentoub's proposal on the subject :neutral_face: but there was definitely one

davidfowl commented 4 years ago

@stephentoub Yep. But with BoundedChannelFullMode.DropOldest, though that really only matters if we find something useful to put in the <T>. Originally I was thinking time since last tick so if the code execution lasts longer than the period, you'd get a timespan that represnted how long between MoveNext calls but it was late, I dunno if that's useful šŸ˜„

stephentoub commented 4 years ago

Now we need this

That's problematic with IAsyncEnumerable<T>, assuming you want to perform this operation more than once, because a) you need to get an enumerator in order to read from it and that enumerator may "start over" depending on the source, but more importantly b) once you call MoveNext() you've pushed the enumerator forward and you can't take it back; that effectively ruins it for a subsequent consumer.

With channels it's feasible, because rather than MoveNext/Current, the operations are WaitForReadAsync/TryRead, which means unlike MoveNext, asking to be notified of an available item doesn't consume said item.

For a while we were on a track with IAsyncEnumerable<T> of employing a similar interface pattern, but for such a mainstream interface it brought significant complexities. https://github.com/dotnet/corefx/issues/32640#issuecomment-428190420

davidfowl commented 4 years ago

That's definitely the right approach, but I think it would be nice to customize what happens when a tick is missed: return immediately on await vs wait for the next tick (so to get back in sync)

Why?

willdean commented 4 years ago

Why?

I can't speak for the OP, nor whether this sort of an option would actually be a good thing, but sometimes I want a one-second timer to mean "run this code 3600 times an hour", and sometimes I want it to mean "wake this code up roughly once a second".

Clearly all such things are no better than 'best effort' on this sort of platform, but to me there is a discernible difference in intention.

scalablecory commented 4 years ago

Reactive's Observable.Interval is nice, but it seems weird usability-wise for the return to be an IAsyncEnumerable.

davidfowl commented 4 years ago

@scalablecory can you elaborate?

scalablecory commented 4 years ago

@davidfowl It's the IAsyncEnumerable use. Something like this seems a better way to accomplish the same thing:

var timer = new Timer(delay);

while(await timer.WaitNextPeriodAsync())
{
}

If you wouldn't ever .ToArrayAsync() on it, then it doesn't fit IAsyncEnumerable. I can't imagine ever passing this timer enumerable to anything else (like LINQ) that operates on an IAsyncEnumerable -- if I were ever doing that, Rx would probably be a better choice for such an event-based architecture. foreaching a timer also seems weird.

scalablecory commented 4 years ago

I'd also want us to look at solving the problems you called out in the existing API as well. It is very common to need an interval timer but not have a neat sequence point to await at -- instead, callbacks that happen in parallel to your bigger machine are exactly what you need.

davidfowl commented 4 years ago

I disagree with the assertion that foreaching this is weird. The difference between Rx and Ix is push vs pull, nothing else. In fact the pattern you show is just a GetAsyncEnumerator().MoveNextAsync().

Also looking around, golang has a very similar feature called a Ticker that exposes a channel of ticks. Itā€™s a pretty elegant pattern IMHO šŸ˜¬. Iā€™m it sure why ToArray matters (IEnumerable can also be infinite)

I'd also want us to look at solving the problems you called out in the existing API as well. It is very common to need an interval timer but not have a neat sequence point to await at -- instead, callbacks that happen in parallel to your bigger machine are exactly what you need.

Sure we can do that at the cost of adding more complexity to the existing API (like passing more options to the existing timer ctor?). Thatā€™s come up in the past as a potential problem.

svick commented 4 years ago

@davidfowl

In fact the pattern you show is just a GetAsyncEnumerator().MoveNextAsync().

I think the problem is that when you have an IEnumerable<T> or IAsyncEnumerable<T>, you're basically saying "you should use foreach". If it's expected that you're commonly going to be calling MoveNextAsync manually, then it's probably better to be explicit about it by having something like WaitNextPeriodAsync instead.

benaadams commented 4 years ago

you're basically saying "you should use foreach".

Generalising more its an event pattern; which you couldn't do previously with synchronous enumerators, but now can with asyncenumerators:

await foreach (var event in myEventSource)
{

}
davidfowl commented 4 years ago

I think the problem is that when you have an IEnumerable or IAsyncEnumerable, you're basically saying "you should use foreach". If it's expected that you're commonly going to be calling MoveNextAsync manually, then it's probably better to be explicit about it by having something like WaitNextPeriodAsync instead.

Generalising more its an event pattern; which you couldn't do previously with synchronous enumerators, but now can with asyncenumerators:

This is exactly the point. You should use foreach. IAsyncEnumerable + await foreach is a new paradigm for C#.

svick commented 4 years ago

@davidfowl In my opinion, it's a paradigm that does not fit well here, so it shouldn't be used.

I think with IAsyncEnumerable<T>, the important part is the data and the fact that you sometimes have to wait for the next item is mostly incidental. But here, the waiting is what you want and you can invent some data to go along with it, but it's completely incidental.

This can be also seen in the syntax: await foreach emphasizes the data and hides the mechanism of retrieving it (await MoveNextAsync()), but that's the opposite of what you want here: you don't care about the data, which is visible, but you do care about the await MoveNextAsync(), which is hidden.

One more way this surfaces is in how you would document this API. For WaitNextPeriodAsync(), it would be roughly:

Here's this one method, await it when you want to wait for the next timer tick. Surround it with whatever syntax you want.

For IAsyncEnumerable<T>, it would be (exaggerated for effect):

It's a sequence of ā€¦ timestamps, I guess? Though it doesn't behave like most other sequences, since it changes depending on how quickly you iterate it. And maybe iterating it using await foreach, like you normally would, doesn't fit your use case, in which case you should await MoveNextAsync yourself.

To sum up, I think that IAsyncEnumerable<T> doesn't fit well logically, its syntax focuses on the wrong thing and it makes it harder to explain what the behavior of the API is and how you should use it. Which is why it shouldn't be used here.

benaadams commented 4 years ago

Don't think DateTime/TimeSpan have the precision for this; but another example which WaitNextPeriodAsync() by itself doesn't capture (why do you want to tick without knowing the time?)

const int framesPerSecond = 60;

var desiredFrameRate = TimeSpan.FromSeconds(1.0 / framesPerSecond);
var ticker = Timer.CreateTimer(desiredFrameRate, desiredFrameRate);

var current = DateTime.UtcNow;
await foreach(var time in ticker)
{
    var delta = time - current;
    current = time;

    Console.WriteLine($"Elasped: {delta}, fps: {(double)TimeSpan.TicksPerSecond / delta.Ticks:N2}");
}

However, I don't think the data types have the precision for the above sample as

var current = DateTime.UtcNow;
var time = current + TimeSpan.FromSeconds(1.0 / 60);

var delta = time - current;

Outputs:

Elasped: 00:00:00.0170000, fps: 58.82

Rather than

Elasped: 00:00:00.0166667, fps: 60.00
scalablecory commented 4 years ago

@benaadams That use seems problematic, though, doesn't it?

For render lerping it would be more useful to get the time inside the loop (after your async continuation has started running) rather than have time be the moment the timer scheduled the continuation. QueryPerformanceCounter/etc.

For physics & entity ticks it would be more useful to have fixed intervals, not dynamic.

You would also not want it to skip overlapping intervals as the issue desires as that'd halve your framerate, though we could always add a flag to control that feature.

However, I don't think the data types have the precision for the above

TimeSpan has precision down to 1e-7 seconds; I assume you're seeing scheduling delay?

scalablecory commented 4 years ago

I disagree with the assertion that foreaching this is weird.

I think @benaadams' example convinced me that it's not entirely bad, though I still think it's weird.

The difference between Rx and Ix is push vs pull, nothing else.

I think that's an over-simplification by looking only at their abstraction interfaces, not at how they're currently used or the frameworks built around them -- there's a very large difference between the two. Rx is a rich event-driven programming model, whereas enumerables are currently only used for iterating data.

In fact the pattern you show is just a GetAsyncEnumerator().MoveNextAsync().

Indeed. It's not the basic idea I'm concerned with, but rather specifically the use of IAsyncEnumerable.

Right now I feel like enumerables are very data-focused, as @svick said (more eloquently than me). What other APIs in corefx do we have as an example of using enumerables for non-data work? Doing something new isn't a bad thing, I just want to make sure it's clear if we're doing something new as it would set the direction for our user's coding conventions. Enumerables currently have a very predictable usage and I'm really against polluting that by having to think about if an enumerable is there for scheduling or for data

I know that Unity uses IEnumerable with yield return for ticking entity state, so there is some prior work here at least outside of corefx.

omariom commented 4 years ago

What if WaitNextPeriodAsync was a public method and IAsyncEnumerable, as a more esoteric concept for timers, was exposed via a method (AsEnumerable?) or explicit interface implementation?

Clockwork-Muse commented 4 years ago

You absolutely do not want something like DateTime.UtcNow to drive rapidly firing (or potentially most) timers. Because even though it's an absolute stamp, it still falls victim to clock slews when trying to set itself via NTP (or from user updates). It's nonsensical in most of those cases anyways, because you don't actually care about "world time", but some sort of absolute "process time".

@scalablecory is right - you want QueryPerformanceCounter, or an equivalent.

Leave DateTime.UtcNow for use in full-featured schedulers.

benaadams commented 4 years ago

You absolutely do not want something like DateTime.UtcNow to drive rapidly firing (or potentially most) timers...

It was an example; as you wouldn't use the timer to drive something that needed an 16ms tick as its not fast or precise enough.

I was demonstrating the pattern, which for example listening to mouse moves you could also do as an AsyncEnumerable

await foreach(var mouseMove in mouseMoveEvents)
{

}
benaadams commented 4 years ago

Or perhaps a better example in a similar vein is button clicks. AsyncEnumerable enables async event handling without async void and the horrors that brings.

await foreach(var click in btn.ClickEvents)
{
    btn.Enabled = false;

    await SomeAsyncMethod();

    btn.Enabled = true;
}
ankitbko commented 4 years ago

Let me see if I can put a different perspective here.

IEnumerable (and its async counterpart) always meant I have a collection of items that I have freedom to iterate over. The items (value or Task) in the collection already exists and I can iterate over them as I want. The AsyncEnumerable just means while iterating I may have to asynchronously wait (incidentally as @svick put it) for a small amount of time to get the item. But in neither case I expect an aspect of scheduling to come while iterating. This is also represented in fact that MoveNextAsync returns ValueTask and not Task. From docs -

A method may return an instance of this (ValueTask) value type when it's likely that the result of its operation will be available synchronously, and when it's expected to be invoked so frequently that the cost of allocating a new Task for each call will be prohibitive.

A scheduler (or Interval) conceptually is different from iterator (a collection of values). A scheduler informs/trigger/send (purposefully avoided push to not confuse with rx vs ix) the subscriber when the tick occurs. This pattern is essentially same as of Rx/Channel/Observable-Subscriber and the difference between this and enumerable is more than just push vs pull.

The Timer being discussed falls into the scheduler category. This is also represented by the fact that we are proposing to use Channel (which is Observer-Observable pattern) to create the timer and then wrapping it up under facade of Enumerator.

An IAsyncEnumerable<DateTime> means I have a collection of DateTime and it may take me an itsy-bitsy of time to get the items. It does not mean that the DateTime values will be available sometime in future and I have wait for it to be available. It is for this purpose that we have Observable pattern (Rx).


Another way to put is by understanding the thought process of average developer if he gets an an enumerable (exaggerated ofcourse) -


public Task<Something> AwesomeMethod(IAsyncEnumerable<DateTime> foo, other random params)
{
  // Oh look, I get an enumerable of DateTime, this is so great. 
  // I can iterate over it as and when I want. 
  // I am never going to expect this enumerable to behave any 
  // differently than any other enumerable of DateTime (or anything else for that matter).
  // I am definitely never going to expect schedulers and intervals and timers 
  // coming into the mix when I am iterating.
}
davidfowl commented 4 years ago

Yea I donā€™t think IAsyncEnumerable is data focused, thatā€™s just something, Linq over events will be a thing (or is a thing depending on who you talk to) and this is just the beginning.

Modeling multi fire events as IAsyncEnumerable is pretty elegant and there are examples on other platforms that support this pattern.

davidfowl commented 4 years ago

Iā€™m not sure what schedulers have to do with the conversation but IAsyncEnumerable is the pull and IObservable is the push model. Thatā€™s it. Anything else you may have associated with those types arenā€™t fundamental, they were just assumptions or expectations on how they would behave based on experience with IEnumerable.

davidfowl commented 4 years ago

FWIW paradigm shifts always feel uncomfortable,l letā€™s all suspend disbelief for a second and imagine a world where events were IAsyncEnumerable šŸ˜. What problems would exist, what patterns would emerge as a result? What pitfalls would exist?

Thatā€™s the discussion I want to have (along with API approval of course).

benaadams commented 4 years ago

The AsyncEnumerable just means while iterating I may have to asynchronously wait (incidentally as @svick put it) for a small amount of time to get the item. But in neither case I expect an aspect of scheduling to come while iterating.

AsyncEnumerable is iterating values that may be "not available" yet; rather than IEnumerable which is iterating values you already have (though they may be via transform/calculation e.g. Linq).

Values "not available" yet also encompasses values that do not exist yet as they come from the future; e.g. events.

Another example would be requests in a http server:

await foreach(var request in http.Requests)
{
    await ProcessRequestAsync(request);
}

AsyncEnumerable is essentially iterating the future. In that vein, a timer based source would be equivalent to Enumerable.Range that exists for IEnumerable.

This is also represented in fact that MoveNextAsync returns ValueTask and not Task.

That also encompasses the fact that Task always allocates per result, and AsyncEnumerable only needs to allocate itself and from that it can generate infinite results without allocating even if it suspends waiting for results.

there are examples on other platforms that support this pattern.

Indeed in Dart their async enumerables are called Streams and they support await for in to achieve the same effect.

Stream even has a Stream<T>.periodic constructor to achieve a similar effect as the timer, however it goes further with the generic.

Thinking on the proposed api and Linq integration; it should also take a cancellation, so you could cancel the timer after something like .Take(50).

Also I like the generic approach from Dart; which could be encompassed with Func<T> and maybe its name is better? Also maybe a Func<(T value, bool isComplete)> so it can indicate to stop producing values and cancel the timer:

public class Timer
{
    public static IAsyncEnumerable<T> Periodic(
        Func<T> valueFactory,
        TimeSpan dueTime, 
        TimeSpan period, 
        CancellationToken cancellationToken = default);

    public static IAsyncEnumerable<T> Periodic(
        Func<(T value, bool isComplete)> boundedValueFactory,
        TimeSpan dueTime, 
        TimeSpan period, 
        CancellationToken cancellationToken = default);
}
benaadams commented 4 years ago

Further to the above

Then you could build for example a Utc Timer using the Func<DateTime>

var timer = Timer.Periodic(() => DateTime.UtcNow, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1);
await foreach (DateTime time in timer)
{
    Console.WriteLine(time);
}

Or the delta version with Func<TimeSpan>

var current = DateTime.MaxValue;

Func<TimeSpan> source = () => {
    var time = DateTime.UtcNow;
    var delta = time - current;
    if (delta < TimeSpan.Zero) delta = TimeSpan.Zero;
    current = time;
    return delta;
};

var timer = Timer.Periodic(source, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1);
await foreach (TimeSpan delta in timer)
{
    Console.WriteLine(delta);
}
ankitbko commented 4 years ago

Maybe not related to this discussion, but if the plan is to have events as IAsyncEnumerable, would it make sense to have a LINQ method Interval<T>(this IAsyncEnumerable<T> foo, TimeSpan period)?

plaisted commented 4 years ago

@benaadams I like the generic factory, might be useful to have an overload something along the lines of the below in the case that you wanted to implement the Timer example with a fixed interval (eg. DateTime object exactly 30 seconds apart) rather than when the Func was called. The input for the factory could be a TimeSpan for total time since creation (periods*iterations) or just have an int representing iterations. Even some sort of context object with the time of first iteration and the timespan since then could be nice.

public class Timer
{
    public static IAsyncEnumerable<T> Periodic(
        Func<TimeSpan, T> valueFactory,
        TimeSpan dueTime, 
        TimeSpan period, 
        CancellationToken cancellationToken = default);
}
davidfowl commented 4 years ago

@stephentoub I'd like to push this back into 6.0.0, any concerns?

stephentoub commented 4 years ago

any concerns?

Nope

3dGrabber commented 3 years ago

I like the idea.

However I'd propose a simpler, more modular API:

// CancellationToken omitted for brevity
public static async IAsyncEnumerable<long> Periodic(TimeSpan period)
{
    long i = 0; 
    while(true)
    {
        yield return i++;
        await Task.Delay(period);   
    }
}

There is no need for dueTime.
If the consumer wants to wait before producing values, he can Task.Delay before starting the loop.
Similarly, valueFactory can be moved outside.

So this:

var dueTime = TimeSpan.FromSeconds(3);
var period = TimeSpan.FromSeconds(1);
var rng = new Random();

foreach (var x in Timer.Periodic(dueTime, period, () => rng.Next())
{
    Console.WriteLine(x);
}

becomes this:


await Task.Delay(dueTime);

foreach (var _ in Timer.Periodic(period))
{
    var x = rng.Next();
    Console.WriteLine(x);
}

or with LINQ:

var stream = Timer
            .Periodic(period)
            .Delay(dueTime)           // Delay operator TBD (?)
            .Select(_ => rng.Next())  // Select available through System.Linq.Async (?)

foreach (var x in stream)
{
    Console.WriteLine(x);
}

As others have pointed out, this is similar to Observable.Interval.
(It's a shame RX has no decent official doc)

RX has a nice modular/orthogonal API, I think it'd be worth it to look at it for inspiration. Or maybe the RX team themselves make it part of System.Linq.Async?

davidfowl commented 3 years ago

I have a couple of goals with this API:

I like the timer.periodic without a callback. However the implementation is less efficient than using a timer with a period.

It needs to be in the box so we can replace a bunch of timers with this without taking a dependency on Rx or Ix.

davidfowl commented 3 years ago

I think this is ready for the first round of API review.

stephentoub commented 3 years ago

I think this is ready for the first round of API review.

davidfowl commented 3 years ago

What is the long that's yielded?

I actually would prefer this to be Void but I didn't want to define that struct. We'll need to either figure out a good use for the generic parameter or make a new type to hide it (IAsyncEnumerable non generic).

Given your desire to reuse timer instances in the other issue, I'm a little surprised this proposal doesn't encompass that, e.g. an overload that supports not starting the next period until MoveNextAsync is called.

I thought about adding the overload but wasn't sure what to call the bool, that can be brought up in API review. Open to ideas and I don't think it changes consumption, it's just an overload.

Can you point to one or more places this will be used in ASP.NET, extensions, etc. in 6.0 to help validate the design?

ASP.NET Core has a type for this today https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/common/Shared/TimerAwaitable.cs

Used here https://github.com/dotnet/aspnetcore/blob/46359cd0a28f551d4021c9c9823752b049eaec79/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs#L125

https://github.com/dotnet/aspnetcore/blob/46359cd0a28f551d4021c9c9823752b049eaec79/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs#L1806

davidfowl commented 3 years ago

OK I updated the proposal and moved away from IAsyncEnumerable for reasons stated above and listed it as an alternative instead.