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

따라서 내 앱은 앱이 실행 중이거나 취소가 요청되는 동안 거의 연속적으로 (각 실행 사이에 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 ()을 호출하여 취소 하시겠습니까?



답변

이를 위해 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);
}


답변

새로운 작업 기반 인터페이스가 이와 같은 작업을 수행하는 데 매우 간단하다는 것을 알았습니다. 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()취소 토큰 을 수락 할 수도 있습니다. 그러면 취소가 훨씬 더 빠르게 반응합니다.


답변

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

  • 원하는 작업으로 메서드를 상속 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();
    }
}


답변