끝없는 작업을 구현하는 적절한 방법. (타이머 대 작업)


92

따라서 내 앱은 앱이 실행 중이거나 취소가 요청되는 동안 거의 연속적으로 (각 실행 사이에 10 초 정도 일시 중지) 작업을 수행해야합니다. 수행해야하는 작업에는 최대 30 초가 소요될 수 있습니다.

System.Timers.Timer를 사용하고 AutoReset을 사용하여 이전 "틱"이 완료되기 전에 작업을 수행하지 않는지 확인하는 것이 더 낫습니까?

아니면 취소 토큰이있는 LongRunning 모드에서 일반 태스크를 사용하고 호출 사이에 10 초 Thread.Sleep으로 작업을 수행하는 작업을 호출하는 내부에 규칙적인 무한 while 루프가 있어야합니까? async / await 모델의 경우 작업에서 반환 값이 없기 때문에 여기에 적절할지 모르겠습니다.

CancellationTokenSource wtoken;
Task task;

void StopWork()
{
    wtoken.Cancel();

    try 
    {
        task.Wait();
    } catch(AggregateException) { }
}

void StartWork()
{
    wtoken = new CancellationTokenSource();

    task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            wtoken.Token.ThrowIfCancellationRequested();
            DoWork();
            Thread.Sleep(10000);
        }
    }, wtoken, TaskCreationOptions.LongRunning);
}

void DoWork()
{
    // Some work that takes up to 30 seconds but isn't returning anything.
}

또는 AutoReset 속성을 사용하는 동안 간단한 타이머를 사용하고 .Stop ()을 호출하여 취소 하시겠습니까?


당신이 달성하려는 것을 고려할 때 과제는 과잉처럼 보입니다. en.wikipedia.org/wiki/KISS_principle . OnTick () 시작시 타이머를 중지하고, 부울을 확인하여 수행하지 않는 작업을 수행해야하는지 확인하고, 완료되면 타이머를 다시 시작합니다.
Mike Trusov 2012

답변:


94

이를 위해 TPL Dataflow 를 사용합니다 (.NET 4.5를 사용하고 Task내부적으로 사용하기 때문에). ActionBlock<TInput>작업을 처리하고 적절한 시간을 기다린 후 항목을 자신에게 게시하는 항목을 쉽게 만들 수 있습니다 .

먼저 끝없는 작업을 생성 할 공장을 만드십시오.

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.
        action(now);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

나는 구조ActionBlock<TInput> 를 취하기 위해을 선택했다 . 유형 매개 변수를 전달해야하며 유용한 상태를 전달할 수도 있습니다 (원하는 경우 상태의 특성을 변경할 수 있음).DateTimeOffset

또한는 ActionBlock<TInput>기본적으로 한 번에 하나의 항목 만 처리 하므로 하나의 작업 만 처리됩니다. 즉, 확장 메서드호출 할 때 재진입 을 처리 할 필요가 없습니다.Post 다시 ).

또한 의 생성자 와 메서드 호출 모두에 CancellationToken구조 를 전달했습니다 . 프로세스가 취소되면 가능한 첫 번째 기회에 취소됩니다.ActionBlock<TInput>Task.Delay

여기에서 구현 된 ITargetBlock<DateTimeoffset>인터페이스 를 저장하는 코드를 쉽게 리팩토링 할 수 있습니다 ActionBlock<TInput>(이것은 소비자 인 블록을 나타내는 상위 수준 추상화이며 Post확장 메서드 호출을 통해 소비를 트리거 할 수 있기를 원합니다 ).

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

귀하의 StartWork방법 :

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now);
}

그리고 당신의 StopWork방법 :

void StopWork()
{
    // CancellationTokenSource implements IDisposable.
    using (wtoken)
    {
        // Cancel.  This will cancel the task.
        wtoken.Cancel();
    }

    // Set everything to null, since the references
    // are on the class level and keeping them around
    // is holding onto invalid state.
    wtoken = null;
    task = null;
}

여기서 TPL Dataflow를 사용하려는 이유는 무엇입니까? 몇 가지 이유 :

우려의 분리

CreateNeverEndingTask방법은 이제 말하자면 "서비스"를 만드는 공장입니다. 시작 및 중지시기를 제어 할 수 있으며 완전히 독립적입니다. 타이머의 상태 제어를 코드의 다른 측면과 섞을 필요가 없습니다. 블록을 만들고 시작하고 완료되면 중지하기 만하면됩니다.

스레드 / 작업 / 리소스를보다 효율적으로 사용

TPL 데이터 흐름의 블록에 대한 기본 스케줄러 Task는 스레드 풀인 에서 동일 합니다. 를 사용 ActionBlock<TInput>하여 작업을 처리하고에 대한 호출을 Task.Delay사용하면 실제로 아무것도하지 않을 때 사용하던 스레드를 제어 할 수 있습니다. 물론, Task연속을 처리 할 새 항목 을 생성 할 때 실제로 약간의 오버 헤드가 발생 하지만, 타이트한 루프 (호출 사이에 10 초를 기다림)에서 처리하지 않는다는 점을 고려할 때 적어야합니다.

경우 DoWork실제로 (그것은을 반환에, 즉 awaitable 할 수 기능 Task), 다음 (아마도) 더 위의 팩토리 메소드를 조정하여이를 최적화 할 수는을하기 Func<DateTimeOffset, CancellationToken, Task>대신 Action<DateTimeOffset>, 그래서 같은 :

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.  Wait on the result.
        await action(now, cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Same as above.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

물론 CancellationToken여기에서 수행되는 방법 (만약 허용하는 경우)에 연결하는 것이 좋습니다.

DoWorkAsync, 다음 서명 이있는 메서드 를 갖게됩니다 .

Task DoWorkAsync(CancellationToken cancellationToken);

StartWork메서드에 전달 된 새 서명을 설명하는 CreateNeverEndingTask메서드를 다음과 같이 변경해야합니다 (약간만 여기에서 문제를 분리하지 않습니다) .

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now, wtoken.Token);
}

안녕하세요,이 구현을 시도하고 있지만 문제가 있습니다. 내 DoWork가 인수를받지 않으면 task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); 빌드 오류가 발생합니다 (유형 불일치). 반면에 DoWork가 DateTimeOffset 매개 변수를 사용하면 동일한 줄에서 다른 빌드 오류가 발생하여 DoWork에 대한 오버로드가 0 인수를 취하지 않는다는 것을 알려줍니다. 이걸 알아 내도록 도와 주 시겠어요?
Bovaz 2014-08-29

1
실제로 작업을 할당하고 매개 변수를 DoWork에 전달하는 줄에 캐스트를 추가하여 문제를 해결했습니다. task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz 2014-08-29

"ActionBlock <DateTimeOffset> 작업"유형을 변경할 수도 있습니다. ITargetBlock <DateTimeOffset> 작업에;
XOR

1
나는 이것이 영원히 메모리를 할당 할 가능성이 있다고 믿고 결국 오버플로로 이어집니다.
Nate Gardner

@NateGardner 어느 부분에서?
casperOne 2018-08-08

75

새로운 작업 기반 인터페이스가 이와 같은 작업을 수행하는 데 매우 간단하다는 것을 알았습니다. Timer 클래스를 사용하는 것보다 훨씬 쉽습니다.

예제를 약간 조정할 수 있습니다. 대신에:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

다음과 같이 할 수 있습니다.

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

이렇게하면 취소가 완료 Task.Delay될 때까지 기다릴 필요없이 내부에있는 경우 즉시 취소됩니다 Thread.Sleep.

또한 Task.Delayover를 사용 Thread.Sleep한다는 것은 잠자는 동안 아무것도하지 않는 스레드를 묶지 않음 을 의미합니다.

가능하다면 DoWork()취소 토큰 을 수락 할 수도 있습니다. 그러면 취소가 훨씬 더 빠르게 반응합니다.


1
당신이 Task.Factory.StartNew의 매개 변수로 비동기 람다를 사용하는 경우 당신이 얻을 무슨 작업 밖으로 Whatch - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx을 당신이 task.Wait 작업을 수행 할 때 ( ); 취소를 요청하면 잘못된 작업을 기다리게됩니다.
Lukas Pirkl 2014

예, 실제로는 올바른 오버로드가있는 Task.Run이되어야합니다.
porges

http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx 에 따르면 Task.Run스레드 풀을 사용하는 것처럼 보이 므로 with Task.Run대신 사용 하는 예제 는 정확히 동일한 작업을 수행하지 않습니다. - 옵션 을 사용하는 데 필요한 작업이 필요한 경우 표시된 것처럼 사용할 수 없습니까? 아니면 뭔가 빠졌나요? Task.Factory.StartNewTaskCreationOptions.LongRunningLongRunningTask.Run
제프

@Lumirris : async / await의 요점은 실행되는 전체 시간 동안 스레드를 묶는 것을 피하는 것입니다 (여기서는 Delay 호출 중에 작업이 스레드를 사용하지 않음). 따라서 사용 LongRunning은 스레드를 묶지 않는다는 목표와 호환되지 않습니다. 자체 스레드에서 실행 되도록 보장 하려면이를 사용할 수 있지만 여기서는 대부분의 시간 동안 휴면중인 스레드를 시작합니다. 사용 사례는 무엇입니까?
porges

@Porges 포인트 촬영. 내 사용 사례는 무한 루프를 실행하는 작업으로, 각 반복은 작업 덩어리를 수행하고 다음 반복에서 다른 작업을 수행하기 전에 2 초 동안 '완화'합니다. 영원히 실행되지만 정기적으로 2 초 휴식을 취합니다. 그러나 내 의견 LongRunningTask.Run구문을 사용하여 지정할 수 있는지 여부에 관한 것 입니다. 문서에서 Task.Run사용하는 기본 설정에 만족하는 한 더 깨끗한 구문 처럼 보입니다 . TaskCreationOptions인수 를받는 과부하가없는 것 같습니다 .
제프

4

내가 생각해 낸 것은 다음과 같습니다.

  • 원하는 작업으로 메서드를 상속 NeverEndingTask하고 재정의합니다 ExecutionCore.
  • 변경을 ExecutionLoopDelayMs통해 루프 사이의 시간을 조정할 수 있습니다 (예 : 백 오프 알고리즘을 사용하려는 경우).
  • Start/Stop 작업을 시작 / 중지하는 동기 인터페이스를 제공합니다.
  • LongRunning당 하나의 전용 스레드를 얻게됩니다 NeverEndingTask.
  • 이 클래스는 ActionBlock위 의 기반 솔루션 과 달리 루프에서 메모리를 할당하지 않습니다 .
  • 아래 코드는 스케치이며 반드시 프로덕션 코드는 아닙니다. :)

:

public abstract class NeverEndingTask
{
    // Using a CTS allows NeverEndingTask to "cancel itself"
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    protected NeverEndingTask()
    {
         TheNeverEndingTask = new Task(
            () =>
            {
                // Wait to see if we get cancelled...
                while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs))
                {
                    // Otherwise execute our code...
                    ExecutionCore(_cts.Token);
                }
                // If we were cancelled, use the idiomatic way to terminate task
                _cts.Token.ThrowIfCancellationRequested();
            },
            _cts.Token,
            TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning);

        // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable
        TheNeverEndingTask.ContinueWith(x =>
        {
            Trace.TraceError(x.Exception.InnerException.Message);
            // Log/Fire Events etc.
        }, TaskContinuationOptions.OnlyOnFaulted);

    }

    protected readonly int ExecutionLoopDelayMs = 0;
    protected Task TheNeverEndingTask;

    public void Start()
    {
       // Should throw if you try to start twice...
       TheNeverEndingTask.Start();
    }

    protected abstract void ExecutionCore(CancellationToken cancellationToken);

    public void Stop()
    {
        // This code should be reentrant...
        _cts.Cancel();
        TheNeverEndingTask.Wait();
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.