.NET API 소개(1) : PeriodicTimer

오늘은 .NET 6 에서 사용할 수 있는 PeriodicTimer를 소개합니다.

PeriodicTimer는 비동기 방식으로 타이머 틱을 처리하는 최신 타이머 API 입니다. 다음처럼 사용할 수 있고요,

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

while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine(DateTime.Now);
}

1초 간격으로 실행되는 결과를 확인할 수 있습니다.

| 실행 결과

2021-11-18 오후 11:14:17
2021-11-18 오후 11:14:18
2021-11-18 오후 11:14:19
2021-11-18 오후 11:14:20
2021-11-18 오후 11:14:21

얼핏 보면 Task.Delay()의 기능과 별반 차이가 없어 보이죠? 차이가 있습니다. 다음의 코드를 보시죠.

var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));

while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine($"Wake Up!: {DateTime.Now}");

    // 1500 ms 소요되는 처리가 발생했다고 가정
    Thread.Sleep(1500);
}

| 실행 결과

Wake Up!: 2021-11-18 오후 11:18:56
Wake Up!: 2021-11-18 오후 11:18:58
Wake Up!: 2021-11-18 오후 11:19:00
Wake Up!: 2021-11-18 오후 11:19:02
Wake Up!: 2021-11-18 오후 11:19:04

강제로 1500 ms 만큼의 딜레이를 줬음에도 불구하고 지정한 period 만큼의 타이머 틱을 제공합니다.

타이머 동작 중 취소하는 기능이 있을까요? WaitForNextTickAsync(cancellationToken)를 통해 취소도 가능합니다. 하지만 좀 더 쉽게 취소하는 방법은 timerDispose하는 것입니다.

using System.Diagnostics;

var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));

var task = Task.Run(async () =>
{
    var sw = Stopwatch.StartNew();
    while (await timer.WaitForNextTickAsync())
    {
        Console.WriteLine($"Wake Up!: {DateTime.Now} {sw.ElapsedMilliseconds}");

        // 1500 ms 소요되는 처리가 발생했다고 가정
        Thread.Sleep(1500);

        sw.Restart();
    }

    Console.WriteLine("타이머가 종료되었습니다.");
});

Console.WriteLine("엔터를 누르면 타이머를 종료합니다.");
Console.ReadLine();

timer.Dispose();

Console.WriteLine("엔터를 누르면 종료합니다.");

Console.ReadLine();

| 실행 결과

엔터를 누르면 타이머를 종료합니다.
Wake Up!: 2021-11-18 오후 11:21:59 2014
Wake Up!: 2021-11-18 오후 11:22:01 483
Wake Up!: 2021-11-18 오후 11:22:03 484
Wake Up!: 2021-11-18 오후 11:22:05 484
Wake Up!: 2021-11-18 오후 11:22:07 485
Wake Up!: 2021-11-18 오후 11:22:09 486
Wake Up!: 2021-11-18 오후 11:22:11 496
Wake Up!: 2021-11-18 오후 11:22:13 486

엔터를 누르면 종료합니다.
타이머가 종료되었습니다.

이 타이머를 사용하면 완전히 비동기적으로 동작하며 객체 수명 문제라던가 비동기 콜백이 없는 등을 처리하는 번거로움을 없앨 수 있어 안전하게 타이머를 이용할 수 있습니다.

9개의 좋아요

Task.Delay() 비교 예시에서
SynchronizationContext 할당이 있는 경우에도 동일하게 동작할까요?

예를 들면 WPF 환경에서

var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));

while (await timer.WaitForNextTickAsync())
{
    // 이 구간은 dispatcher 에 의해 수행.
    Console.WriteLine($"Wake Up!: {DateTime.Now}");

    Thread.Sleep(1500);
}

이 코드가 표시해주신 결과처럼 동작하는 지 궁금합니다.

2개의 좋아요

제가 지금 테스트 할 수 있는 환경이 아니라… 짐작하기로는 await이후의 코드들이 SynchronizationContext 의 스레드로 동작할 것으로 예상되는데, 그러면 Thread.Sleep(1500)에서 화면이 멈출듯 합니다. 있다가 저녁 즈음에 확인해볼께요.

4개의 좋아요

예상한 것 처럼 SynchronizationContext에 의해 이후 코드는 SynchronizationContext의 스레드로 실행이 됩니다. 즉, 1500 ms 동안 화면이 멈춥니다. 이를 해결하려면 timer.WaitForNextTickAsync().ConfigureAwait(false)를 해서 SynchronizationContext를 무시하도록 해야 합니다.

4개의 좋아요

이거보고 찾아왔습니다.

이런 게 있었다니…b

3개의 좋아요

타이머 생성이후 Period를 조작하려했는데,
이부분은 닷넷8 이후에서만 지원하네요
혹시 닷넷 6,7에서 최신 PeriodicTimer를 적용하는 공식 패키지 없을까요?

1개의 좋아요

없는것으로 알고 있습니다. PeriodicTimer를 한번 더 감싸 사용자 클래스를 만든 후 Period 속성을 구현해서 사용하시는 것이 최상인 것 같습니다.

2개의 좋아요