따라서 내 앱은 앱이 실행 중이거나 취소가 요청되는 동안 거의 연속적으로 (각 실행 사이에 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.Delay
over를 사용 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();
}
}