System.Threading.Timer에 대한 작업 기반 대체가 있습니까?


91

저는 .Net 4.0의 Tasks를 처음 접했고 Task 기반 대체 또는 Timer 구현 (예 : 주기적 작업)을 찾을 수 없었습니다. 그런 것이 있습니까?

업데이트 저는 CancellationToken을 모두 활용하는 자식 작업으로 작업 내부의 "타이머"기능을 래핑하고 추가 작업 단계에 참여할 수 있도록 작업을 반환하는 내 요구에 대한 해결책이라고 생각하는 것을 생각해 냈습니다.

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      

7
Thread.Sleep 메커니즘을 사용하는 대신 Task 내에서 Timer를 사용해야합니다. 더 효율적입니다.
Yoann. B

답변:


85

4.5에 따라 다르지만 작동합니다.

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

분명히 인수를 취하는 제네릭 버전을 추가 할 수 있습니다. 이것은 Task.Delay가 작업 완료 소스로 타이머 만료를 사용하기 때문에 실제로 다른 제안 된 접근 방식과 유사합니다.


1
방금이 접근 방식으로 전환했습니다. 그러나 나는 조건부 action()!cancelToken.IsCancellationRequested. 그게 더 낫죠?
HappyNomad

3
감사합니다. 동일한 방법을 사용하고 있지만 작업이 끝날 때까지 지연을 이동했습니다 (작업을 즉시 호출 한 다음 x 이후에 반복해야하므로 우리에게 더 의미가 있습니다)
Michael Parker

2
감사합니다. 하지만이 코드는 "X 시간마다"실행되지 않습니다. "X 시간마다 + action실행 시간마다 "실행됩니다. 맞습니까?
Alex

옳은. 실행 시간을 고려하려면 수학이 필요합니다. 그러나 실행 시간이 기간을 초과하면 까다로울 수 있습니다.
Jeff

57

UPDATE 나는 아래의 답변을 마킹 이 된만큼 우리가 비동기 / await를 패턴을 사용하는 것이 이제부터 "대답"으로한다. 더 이상 찬성 할 필요가 없습니다. LOL


Amy가 대답했듯이 Tasked 기반주기 / 타이머 구현은 없습니다. 그러나 내 원래 UPDATE를 기반으로 우리는 이것을 매우 유용하고 생산 테스트를 거친 것으로 발전 시켰습니다. 공유 할 생각 :

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

산출:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

1
이것은 훌륭한 코드처럼 보이지만 async / await 키워드가 이제 필요한지 궁금합니다. 귀하의 접근 방식은 stackoverflow.com/a/14297203/122781 의 접근 방식과 어떻게 비교 됩니까?
HappyNomad

1
@HappyNomad, PeriodicTaskFactory 클래스가 .Net 4.5를 대상으로하는 애플리케이션에 대해 async / await를 활용할 수있는 것처럼 보이지만 아직 .Net 4.5로 이동할 수 없습니다. 또한 PeriodicTaskFactory는 최대 반복 횟수 및 최대 기간과 같은 추가 "타이머"종료 메커니즘을 제공 할뿐만 아니라 각 반복이 마지막 반복에서 대기 할 수 있도록하는 방법을 제공합니다. 하지만이 사용에 적응하고자합니다 비동기 / await를 우리는 닷넷 4.5로 이동할 때

4
+1 지금 수업을 사용하고 있습니다. 감사합니다. 하지만 UI 스레드와 잘 어울리도록하려면 TaskScheduler.FromCurrentSynchronizationContext()설정하기 전에 호출 해야합니다 mainAction. 그런 다음 결과 스케줄러를 MainPeriodicTaskAction에 전달 하여 subTaskwith 을 만듭니다 .
HappyNomad

2
나는 그것이 유용한 작업을 할 수있을 때 스레드를 차단하는 것이 좋은 생각이라고 확신하지 않습니다. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)"... 그런 다음 Timer를 사용하고 하드웨어에서 대기하므로 쓰레드가 소비되지 않습니다. 그러나 솔루션에서 스레드는 아무것도 사용하지 않습니다.
RollingStone 2017 년

2
@rollingstone 동의합니다. 이 솔루션은 비동기와 같은 동작의 목적을 크게 무효화한다고 생각합니다. 타이머를 사용하고 스레드를 낭비하지 않는 것이 훨씬 좋습니다. 이것은 어떤 이점도없이 비동기의 모습을 제공하는 것입니다.
제프


9

지금까지 스레딩 타이머 대신주기적인 CPU 바인딩 백그라운드 작업에 LongRunning TPL 작업을 사용했습니다.

  • TPL 작업은 취소를 지원합니다.
  • 스레딩 타이머는 프로그램이 종료되는 동안 다른 스레드를 시작하여 폐기 된 리소스에 문제를 일으킬 수 있습니다.
  • 오버런 가능성 : 스레딩 타이머는 예상치 못한 긴 작업으로 인해 이전 스레드가 계속 처리되는 동안 다른 스레드를 시작할 수 있습니다 (타이머를 중지했다가 다시 시작하면 방지 할 수 있음).

그러나 TPL 솔루션은 항상 다음 작업을 기다리는 동안 필요하지 않은 전용 스레드를 요구합니다 (대부분의 경우). Jeff의 제안 된 솔루션을 사용하여 백그라운드에서 CPU 바운드 순환 작업을 수행하고 싶습니다. 왜냐하면 확장성에 더 좋은 작업이있을 때 (특히 간격 기간이 클 때) 스레드 풀 스레드 만 필요하기 때문입니다.

이를 달성하기 위해 4 가지 적응을 제안합니다.

  1. 추가 ConfigureAwait(false)받는 Task.Delay()실행 doWork, 그렇지 않으면, 스레드 풀 스레드에 대한 조치를doWork 병렬 처리 개념이 아닌 호출 스레드에서 수행됩니다.
  2. TaskCanceledException을 던져 취소 패턴을 고수하십시오 (여전히 필요합니까?)
  3. CancellationToken을 doWork 에 작업을 취소 할 수 있도록합니다.
  4. 작업 상태 정보 (예 : TPL 작업)를 제공하는 개체 유형의 매개 변수를 추가합니다.

포인트 2에 대해 잘 모르겠습니다. 비동기 대기에는 여전히 TaskCanceledExecption이 필요합니까? 아니면 모범 사례입니까?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

제안 된 솔루션에 대한 의견을 보내주십시오 ...

2016-8-30 업데이트

위의 솔루션은 즉시 호출하지 않습니다 doWork()만에 착공 await Task.Delay().ConfigureAwait(false)을위한 스레드 스위치를 달성하기 위해 doWork(). 아래의 솔루션은 첫 번째 doWork()호출을Task.Run() 하고 대기 합니다.

아래는 개선 된 async \ await 교체입니다. Threading.Timer 취소 가능한 순환 작업을 수행하고 다음 작업을 기다리는 동안 스레드를 차지하지 않기 때문에 확장 가능 (TPL 솔루션과 비교 .

타이머와 달리 대기 시간 ( period)은 일정하며주기 시간이 아닙니다. 주기 시간은 대기 시간과 지속 시간의 합계입니다 doWork().

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

을 사용 ConfigureAwait(false)하면 스레드 풀에 대한 메서드의 연속이 예약되므로 실제로 스레딩 타이머의 두 번째 지점을 해결하지 못합니다. 나는 또한 필요하다고 생각하지 않습니다 taskState. 람다 변수 캡처는 더 유연하고 형식에 안전합니다.
Stephen Cleary

1
내가 정말하고 싶은 교환하는 것입니다 await Task.Delay()doWork()그래서 doWork()바로 시작하는 동안 실행됩니다. 그러나 어떤 트릭 doWork()이 없으면 호출 스레드에서 처음으로 실행되고 차단됩니다. 스티븐, 그 문제에 대한 해결책이 있습니까?
Erik Stroeken

1
가장 쉬운 방법은 모든 것을 Task.Run.
Stephen Cleary

예,하지만 루프가 실행되는 한 스레드를 주장하는 지금 사용하는 TPL 솔루션으로 돌아가서이 솔루션보다 확장 성이 떨어집니다.
Erik Stroeken

1

동기 메서드에서 반복되는 비동기 작업을 트리거해야했습니다.

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

이것은 Jeff의 대답을 수정 한 것입니다. 에 걸릴로 변경 Func<Task> 또한 기간이 다음 지연 기간에서 작업의 실행 시간을 차감하여 실행하는 빈도인지 확인합니다.

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}

0

비슷한 문제가 발생 TaskTimer하여 타이머에서 완료되는 일련의 작업을 반환하는 클래스를 작성했습니다 : https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

단순한...

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.