475

Task<T>がいくつかの特別なルールで完了するのを待ちたい: X ミリ秒後に完了していない場合、ユーザーにメッセージを表示したい。Y ミリ秒経過しても完了しない場合は、自動的にキャンセルを要求したいと考えています。

Task.ContinueWithを使用して、タスクが完了するのを非同期的に待機できます (つまり、タスクが完了したときに実行されるアクションをスケジュールします) が、タイムアウトを指定することはできません。Task.Waitを使用して、タスクがタイムアウトで完了するのを同期的に待機できますが、それによってスレッドがブロックされます。タスクがタイムアウトで完了するのを非同期的に待機するにはどうすればよいですか?

4

20 に答える 20

674

これはどう:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

そして、これは素晴らしいブログ記事「Crafting a Task.TimeoutAfter Method」(MS Parallel Library team から) で、この種のことに関する詳細情報があります

追加:私の回答に対するコメントのリクエストに応じて、キャンセル処理を含む拡張ソリューションを次に示します。キャンセルをタスクとタイマーに渡すということは、コード内でキャンセルが発生する可能性がある複数の方法があることを意味することに注意してください。それらすべてを適切に処理できることを確認し、確実に処理する必要があります。さまざまな組み合わせを偶然に任せず、コンピューターが実行時に正しいことを行うことを期待してください。

int timeout = 1000;
var task = SomeOperationAsync(cancellationToken);
if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task)
{
    // Task completed within timeout.
    // Consider that the task may have faulted or been canceled.
    // We re-await the task so that any exceptions/cancellation is rethrown.
    await task;

}
else
{
    // timeout/cancellation logic
}
于 2012-06-25T14:18:12.927 に答える
289

Andrew Arnott がanswerへのコメントで提案したように、元のタスクが完了したときにタイムアウトのキャンセルを組み込んだ拡張メソッド バージョンを次に示します。

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout) {

    using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {

        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task) {
            timeoutCancellationTokenSource.Cancel();
            return await task;  // Very important in order to propagate exceptions
        } else {
            throw new TimeoutException("The operation has timed out.");
        }
    }
}
于 2014-02-27T19:54:14.483 に答える
51

Task.WaitAny複数のタスクの最初を待つために使用できます。

2つの追加タスク(指定されたタイムアウトの後に完了する)を作成してWaitAnyから、どちらかが最初に完了するのを待つために使用できます。最初に完了したタスクが「作業」タスクである場合は、これで完了です。最初に完了したタスクがタイムアウトタスクである場合は、タイムアウトに対応できます(リクエストのキャンセルなど)。

于 2010-11-21T15:15:00.280 に答える
17

このようなものはどうですか?

    const int x = 3000;
    const int y = 1000;

    static void Main(string[] args)
    {
        // Your scheduler
        TaskScheduler scheduler = TaskScheduler.Default;

        Task nonblockingTask = new Task(() =>
            {
                CancellationTokenSource source = new CancellationTokenSource();

                Task t1 = new Task(() =>
                    {
                        while (true)
                        {
                            // Do something
                            if (source.IsCancellationRequested)
                                break;
                        }
                    }, source.Token);

                t1.Start(scheduler);

                // Wait for task 1
                bool firstTimeout = t1.Wait(x);

                if (!firstTimeout)
                {
                    // If it hasn't finished at first timeout display message
                    Console.WriteLine("Message to user: the operation hasn't completed yet.");

                    bool secondTimeout = t1.Wait(y);

                    if (!secondTimeout)
                    {
                        source.Cancel();
                        Console.WriteLine("Operation stopped!");
                    }
                }
            });

        nonblockingTask.Start();
        Console.WriteLine("Do whatever you want...");
        Console.ReadLine();
    }

別のタスクを使用してメイン スレッドをブロックすることなく、Task.Wait オプションを使用できます。

于 2010-11-21T14:39:52.410 に答える
16

これは、上位投票の回答に基づいた完全に機能する例です。これは次のとおりです。

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

この回答の実装の主な利点は、ジェネリックが追加されているため、関数 (またはタスク) が値を返すことができることです。これは、既存の関数をタイムアウト関数でラップできることを意味します。たとえば、次のようになります。

前:

int x = MyFunc();

後:

// Throws a TimeoutException if MyFunc takes more than 1 second
int x = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));

このコードには .NET 4.5 が必要です。

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

namespace TaskTimeout
{
    public static class Program
    {
        /// <summary>
        ///     Demo of how to wrap any function in a timeout.
        /// </summary>
        private static void Main(string[] args)
        {

            // Version without timeout.
            int a = MyFunc();
            Console.Write("Result: {0}\n", a);
            // Version with timeout.
            int b = TimeoutAfter(() => { return MyFunc(); },TimeSpan.FromSeconds(1));
            Console.Write("Result: {0}\n", b);
            // Version with timeout (short version that uses method groups). 
            int c = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
            Console.Write("Result: {0}\n", c);

            // Version that lets you see what happens when a timeout occurs.
            try
            {               
                int d = TimeoutAfter(
                    () =>
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(123));
                        return 42;
                    },
                    TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", d);
            }
            catch (TimeoutException e)
            {
                Console.Write("Exception: {0}\n", e.Message);
            }

            // Version that works on tasks.
            var task = Task.Run(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 42;
            });

            // To use async/await, add "await" and remove "GetAwaiter().GetResult()".
            var result = task.TimeoutAfterAsync(TimeSpan.FromSeconds(2)).
                           GetAwaiter().GetResult();

            Console.Write("Result: {0}\n", result);

            Console.Write("[any key to exit]");
            Console.ReadKey();
        }

        public static int MyFunc()
        {
            return 42;
        }

        public static TResult TimeoutAfter<TResult>(
            this Func<TResult> func, TimeSpan timeout)
        {
            var task = Task.Run(func);
            return TimeoutAfterAsync(task, timeout).GetAwaiter().GetResult();
        }

        private static async Task<TResult> TimeoutAfterAsync<TResult>(
            this Task<TResult> task, TimeSpan timeout)
        {
            var result = await Task.WhenAny(task, Task.Delay(timeout));
            if (result == task)
            {
                // Task completed within timeout.
                return task.GetAwaiter().GetResult();
            }
            else
            {
                // Task timed out.
                throw new TimeoutException();
            }
        }
    }
}

注意事項

この答えを与えたので、絶対に必要でない限り、通常の操作中にコードに例外をスローすることは一般的に良い習慣ではありません。

  • 例外がスローされるたびに、それは非常に重い操作であり、
  • 例外がタイトなループにある場合、例外によってコードが 100 倍以上遅くなる可能性があります。

呼び出している関数を絶対に変更できない場合にのみ、このコードを使用して、特定のTimeSpan.

この回答は、タイムアウトパラメータを含めるためにリファクタリングできないサードパーティのライブラリライブラリを扱う場合にのみ適用されます。

堅牢なコードの書き方

堅牢なコードを書きたい場合、一般的なルールは次のとおりです。

無期限にブロックされる可能性のあるすべての操作には、タイムアウトが必要です。

このルールを守らないと、コードは何らかの理由で失敗する操作に最終的にヒットし、無期限にブロックされ、アプリは永久にハングします。

しばらくして妥当なタイムアウトが発生した場合、アプリは極端な時間 (30 秒など) ハングし、エラーを表示してそのまま続行するか、再試行します。

于 2014-07-18T14:08:44.393 に答える
9

Timerを使用して、メッセージと自動キャンセルを処理します。タスクが完了したら、タイマーで Dispose を呼び出して、タイマーが起動しないようにします。以下に例を示します。taskDelay を 500、1500、または 2500 に変更して、さまざまなケースを確認します。

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

namespace ConsoleApplication1
{
    class Program
    {
        private static Task CreateTaskWithTimeout(
            int xDelay, int yDelay, int taskDelay)
        {
            var cts = new CancellationTokenSource();
            var token = cts.Token;
            var task = Task.Factory.StartNew(() =>
            {
                // Do some work, but fail if cancellation was requested
                token.WaitHandle.WaitOne(taskDelay);
                token.ThrowIfCancellationRequested();
                Console.WriteLine("Task complete");
            });
            var messageTimer = new Timer(state =>
            {
                // Display message at first timeout
                Console.WriteLine("X milliseconds elapsed");
            }, null, xDelay, -1);
            var cancelTimer = new Timer(state =>
            {
                // Display message and cancel task at second timeout
                Console.WriteLine("Y milliseconds elapsed");
                cts.Cancel();
            }
                , null, yDelay, -1);
            task.ContinueWith(t =>
            {
                // Dispose the timers when the task completes
                // This will prevent the message from being displayed
                // if the task completes before the timeout
                messageTimer.Dispose();
                cancelTimer.Dispose();
            });
            return task;
        }

        static void Main(string[] args)
        {
            var task = CreateTaskWithTimeout(1000, 2000, 2500);
            // The task has been started and will display a message after
            // one timeout and then cancel itself after the second
            // You can add continuations to the task
            // or wait for the result as needed
            try
            {
                task.Wait();
                Console.WriteLine("Done waiting for task");
            }
            catch (AggregateException ex)
            {
                Console.WriteLine("Error waiting for task:");
                foreach (var e in ex.InnerExceptions)
                {
                    Console.WriteLine(e);
                }
            }
        }
    }
}

また、Async CTPは、タイマーをタスクにラップする TaskEx.Delay メソッドを提供します。これにより、Timer が起動したときに継続のために TaskScheduler を設定するなど、より多くのことを制御できます。

private static Task CreateTaskWithTimeout(
    int xDelay, int yDelay, int taskDelay)
{
    var cts = new CancellationTokenSource();
    var token = cts.Token;
    var task = Task.Factory.StartNew(() =>
    {
        // Do some work, but fail if cancellation was requested
        token.WaitHandle.WaitOne(taskDelay);
        token.ThrowIfCancellationRequested();
        Console.WriteLine("Task complete");
    });

    var timerCts = new CancellationTokenSource();

    var messageTask = TaskEx.Delay(xDelay, timerCts.Token);
    messageTask.ContinueWith(t =>
    {
        // Display message at first timeout
        Console.WriteLine("X milliseconds elapsed");
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    var cancelTask = TaskEx.Delay(yDelay, timerCts.Token);
    cancelTask.ContinueWith(t =>
    {
        // Display message and cancel task at second timeout
        Console.WriteLine("Y milliseconds elapsed");
        cts.Cancel();
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    task.ContinueWith(t =>
    {
        timerCts.Cancel();
    });

    return task;
}
于 2010-11-21T15:19:01.210 に答える
6

この問題を解決する別の方法は、Reactive Extensions を使用することです。

public static Task TimeoutAfter(this Task task, TimeSpan timeout, IScheduler scheduler)
{
        return task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

単体テストで以下のコードを使用して上記をテストしてください。それは私にとってはうまくいきます

TestScheduler scheduler = new TestScheduler();
Task task = Task.Run(() =>
                {
                    int i = 0;
                    while (i < 5)
                    {
                        Console.WriteLine(i);
                        i++;
                        Thread.Sleep(1000);
                    }
                })
                .TimeoutAfter(TimeSpan.FromSeconds(5), scheduler)
                .ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted);

scheduler.AdvanceBy(TimeSpan.FromSeconds(6).Ticks);

次の名前空間が必要になる場合があります。

using System.Threading.Tasks;
using System.Reactive.Subjects;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Microsoft.Reactive.Testing;
using System.Threading;
using System.Reactive.Concurrency;
于 2015-06-17T02:33:31.027 に答える