特定の操作を一定時間実行したい。その時間が経過したら、別の実行コマンドを送信します。
StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();
アプリケーションの残りの部分をブロックしているスリープ ステートメントをそこに含めるのではなく、C# で Async/Task/Await を使用してこれを記述するにはどうすればよいでしょうか?
特定の操作を一定時間実行したい。その時間が経過したら、別の実行コマンドを送信します。
StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();
アプリケーションの残りの部分をブロックしているスリープ ステートメントをそこに含めるのではなく、C# で Async/Task/Await を使用してこれを記述するにはどうすればよいでしょうか?
この問題は、2011 年の Parallel Team のブログで Joe Hoag によって回答されました: Crafting a Task.TimeoutAfter Method。
ソリューションは TaskCompletionSource を使用し、いくつかの最適化 (キャプチャを回避するだけで 12%) を含み、クリーンアップを処理し、ターゲット タスクが既に完了したときに TimeoutAfter を呼び出す、無効なタイムアウトを渡すなどのエッジ ケースをカバーします。
Task.TimeoutAfter の優れた点は、タイムアウトの期限が切れたことを通知するという 1 つのことだけを行うため、他の継続と組み合わせて非常に簡単に作成できることです。タスクをキャンセルしようとはしません。TimeoutException がスローされたときに何をするかを決めることができます。
エッジケースもカバーされていませんが、Stephen Toub による簡単な実装async/await
も紹介されています。
最適化された実装は次のとおりです。
public static Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
// Short-circuit #1: infinite timeout or task already completed
if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite))
{
// Either the task has already completed or timeout will never occur.
// No proxy necessary.
return task;
}
// tcs.Task will be returned as a proxy to the caller
TaskCompletionSource<VoidTypeStruct> tcs =
new TaskCompletionSource<VoidTypeStruct>();
// Short-circuit #2: zero timeout
if (millisecondsTimeout == 0)
{
// We've already timed out.
tcs.SetException(new TimeoutException());
return tcs.Task;
}
// Set up a timer to complete after the specified timeout period
Timer timer = new Timer(state =>
{
// Recover your state information
var myTcs = (TaskCompletionSource<VoidTypeStruct>)state;
// Fault our proxy with a TimeoutException
myTcs.TrySetException(new TimeoutException());
}, tcs, millisecondsTimeout, Timeout.Infinite);
// Wire up the logic for what happens when source task completes
task.ContinueWith((antecedent, state) =>
{
// Recover our state data
var tuple =
(Tuple<Timer, TaskCompletionSource<VoidTypeStruct>>)state;
// Cancel the Timer
tuple.Item1.Dispose();
// Marshal results to proxy
MarshalTaskResults(antecedent, tuple.Item2);
},
Tuple.Create(timer, tcs),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
エッジケースのチェックなしのStephen Toubの実装:
public static async Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
await task;
else
throw new TimeoutException();
}
StartDoingStuff と StopDoingStuff が Task を返す Async メソッドとして作成されていると仮定すると、
await StartDoingStuff();
await Task.Delay(200);
await StopDoingStuff();
編集: 元の質問者が、特定の期間後にキャンセルされる非同期メソッドを必要とする場合: メソッドがネットワーク リクエストを作成するのではなく、メモリ内で何らかの処理を行うだけであり、結果をその影響を考慮せずに任意に中止できると仮定して、次に使用します。キャンセル トークン:
private async Task Go()
{
CancellationTokenSource source = new CancellationTokenSource();
source.CancelAfter(200);
await Task.Run(() => DoIt(source.Token));
}
private void DoIt(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
}
}
編集:結果の OperationCanceledException をキャッチして、タスクがどのように終了したかを示し、ブール値をいじる必要がないようにすることができると述べたはずです。
タスクキャンセルパターン(例外をスローしないオプション)を使用して、これを行う方法を次に示します。
[編集済み] Svick の提案を使用して、CancellationTokenSource
コンストラクターを介してタイムアウトを設定するように更新されました。
// return true if the job has been done, false if cancelled
async Task<bool> DoSomethingWithTimeoutAsync(int timeout)
{
var tokenSource = new CancellationTokenSource(timeout);
CancellationToken ct = tokenSource.Token;
var doSomethingTask = Task<bool>.Factory.StartNew(() =>
{
Int64 c = 0; // count cycles
bool moreToDo = true;
while (moreToDo)
{
if (ct.IsCancellationRequested)
return false;
// Do some useful work here: counting
Debug.WriteLine(c++);
if (c > 100000)
moreToDo = false; // done counting
}
return true;
}, tokenSource.Token);
return await doSomethingTask;
}
非同期メソッドから呼び出す方法は次のとおりです。
private async void Form1_Load(object sender, EventArgs e)
{
bool result = await DoSomethingWithTimeoutAsync(3000);
MessageBox.Show("DoSomethingWithTimeout done:" + result); // false if cancelled
}
通常のメソッドから呼び出して、完了を非同期で処理する方法は次のとおりです。
private void Form1_Load(object sender, EventArgs e)
{
Task<bool> task = DoSomethingWithTimeoutAsync(3000);
task.ContinueWith(_ =>
{
MessageBox.Show("DoSomethingWithTimeout done:" + task.Result); // false is cancelled
}, TaskScheduler.FromCurrentSynchronizationContext());
}