2

For PeriodicTimer (AsyncTimer at the time), regarding WaitForNextTickAsync, David Fowler mentioned "The execution context isn't captured" (here) via (here). However, given that was not necessarily the final implementation, I reviewed the PeriodicTimer documentation which makes no mention of context capturing.

Based on Stephen Toub's decade old, but still excellent, "The Task-based Asynchronous Pattern," and the following code:

private CancellationTokenSource tokenSource;

private async void start_Click(object sender, EventArgs e)
{
    tokenSource = new CancellationTokenSource();

    var second = TimeSpan.FromSeconds(1);
    using var timer = new PeriodicTimer(second);

    try
    {
        while (await timer.WaitForNextTickAsync(tokenSource.Token).ConfigureAwait(false))
        {
            if (txtMessages.InvokeRequired)
            {
                txtMessages.Invoke(() => txtMessages.AppendText("Invoke Required..." + Environment.NewLine));
            }
            else
            {
                txtMessages.AppendText("Invoke NOT Required!" + Environment.NewLine);
            }
        }
    } catch (OperationCanceledException)
    {
        //disregard the cancellation
    }
}

private void stop_Click(object sender, EventArgs e)
{
    tokenSource.Cancel();
}

If ConfigureAwait is passed true (or removed entirely), my output is as follows:

Invoke NOT Required!
Invoke NOT Required!
Invoke NOT Required!
Invoke NOT Required!
...

However, if ConfigureAwait is passed false, my output is as follows:

Invoke Required...
Invoke Required...
Invoke Required...
Invoke Required...
...

Unless I'm confusing SynchronizationContext with "executing thread," it seems like the current SynchronizationContext IS captured by default. Can anyone (maybe one of the greats) please clarify?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Novox
  • 774
  • 2
  • 7
  • 24

1 Answers1

1

The new (.NET 6) PeriodicTimer component is not like all other Timer components that raise events or execute callbacks. This one resembles more the Task.Delay method. It exposes a single asynchronous method, the method WaitForNextTickAsync. This method is not special in any way. It returns a standard ValueTask<bool>, not some kind of exotic awaitable like the Task.Yield method (YieldAwaitable).

When you await this task, you control the SynchronizationContext-capturing behavior of the await like you do for any other Task or ValueTask: with the ConfigureAwait method. If you know how to use the ConfigureAwait with the Task.Delay, you also know how to use it with the PeriodicTimer.WaitForNextTickAsync. There is absolutely no difference.

If you don't know what the ConfigureAwait does, or you want to refresh your memory, there is a plethora of good articles to read. For the sake of variance I'll suggest this old 5-minute video by Lucian Wischik: Tip 6: Async library methods should consider using Task.ConfigureAwait(false)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Thank you, @Theodor. I think I understand what `ConfigureAwait` does, but was confused when my example code didn't seem to exhibit behavior that the `PeriodicTimer` (`AsyncTimer`) would not capture the current execution context by default. I was previously using `await Task.Delay(1000).ConfigureAwait(false);` properly I think. I think it was perhaps your? response to another SO post that caused me to look at `PeriodicTimer` because `WaitForNextTickAsync` is more performant than `Task.Delay` due to the `ValueTask` return (vs. `Task`). – Novox Jul 15 '22 at 20:24
  • 1
    Yasss. Found it https://stackoverflow.com/questions/30462079/run-async-method-regularly-with-specified-interval – Novox Jul 15 '22 at 20:32
  • @Novox actually it's more performant also because it creates [internally](https://github.com/dotnet/runtime/blob/32f5873a1d2d72722f29618fd9f1d5556dae8f65/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs#L20) a single `TimerQueueTimer` instance, instead of creating a new `TimerQueueTimer` each time you use the `Task.Delay` method (the internal [`DelayPromise`](https://github.com/dotnet/runtime/blob/32f5873a1d2d72722f29618fd9f1d5556dae8f65/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5604) class). – Theodor Zoulias Jul 15 '22 at 20:48
  • Thanks again, @Theodor. All very good information, but do you know if the execution context is captured, by default, for `WaitForNextTickAsync`? My investigation seems to indicate no. – Novox Jul 18 '22 at 14:01
  • @Novox TBH I don't know much about the `ExecutionContext`, because of this advice: *"ExecutionContext is one of those things that the vast majority of developers never need to think about."* from [this](https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/ "ExecutionContext vs SynchronizationContext") article by Stephen Toub. The same article also says: *"ExecutionContext is flowed across await points regardless of what kind of awaitable is being used."* – Theodor Zoulias Jul 18 '22 at 15:08
  • I read that article, thank you. I feel like the answer should be in there, somewhere, given it's titled, "ExecutionContext vs SynchronizationContext." However, I only have more questions... While I may not need to think about the `ExecutionContext`, I DO need to worry about the `SynchronizationContext`. In my example code, the default behavior causes the `TextBox`'s `AppendText` to occur in the UI thread. I don't necessarily want this because I want the UI to be as responsive as possible. `ConfigureAwait(false)` seems to account for this, invoking `AppendText` in a non-UI thread. – Novox Jul 18 '22 at 15:46
  • Aside from their return types, as far as I can tell, `await Task.Delay(1000, tokenSource.Token)` and `timer.WaitForNextTickAsync(tokenSource.Token)` are functionally equivalent. – Novox Jul 18 '22 at 15:55
  • @Novox my suggestion is to avoid using the `ConfigureAwait` (or the `Task.Yield`) with the purpose of controlling the continuation thread. If you want to offload work to the `ThreadPool`, you can use the `Task.Run`. [Here](https://stackoverflow.com/questions/72738858/what-exactly-in-an-awaitable-is-being-awaited-how-do-i-make-a-task-properly/72739056#72739056) is a recent answer of mine, that offers a bit more context. – Theodor Zoulias Jul 18 '22 at 15:56
  • I tried running my try/catch code within `Task.Run(async () => { ... });` Now, both cases (`ConfigureAwait` with `false` vs. `true` (or removed entirely)) indicate invocation is required. However, this is expected since `Task.Run` is explicitly using a `ThreadPool` thread (not my main UI thread). Capturing the current `SynchronizationContext` (with `ConfigureAwait(true)` or removing the use of `ConfigureAwait`) *appears* to work the same as not-capturing (with `ConfigureAwait(false)`) because neither the current context nor a new context are my main UI thread (both requiring invocation). – Novox Jul 18 '22 at 16:23
  • @Novox if you want a specific behavior and have troubles producing it, you could ask a new question about your scenario, including expected and actual behavior. Most likely the solution will be something pretty simple. The simple solutions are the best solutions! – Theodor Zoulias Jul 18 '22 at 16:40