سؤال

أريد أن أنتظر مهمةu003CT> للإكمال مع بعض القواعد الخاصة: إذا لم تكتمل بعد x milliseconds ، فأنا أريد عرض رسالة إلى المستخدم. وإذا لم يكتمل بعد y milliseconds ، فأنا أريد تلقائيًا طلب الغاء.

استطيع ان استخدم Task.Continuewith لانتظار المهمة بشكل غير متزامن (أي جدولة إجراء إجراء عند اكتمال المهمة) ، لكن هذا لا يسمح بتحديد مهلة. استطيع ان استخدم Task.WAIT لانتظار المهمة بشكل متزامن مع مهلة ، ولكن هذا يمنع موضوعي. كيف يمكنني الانتظار بشكل غير متزامن حتى تكتمل المهمة مع مهلة؟

هل كانت مفيدة؟

المحلول

وماذا عن هذا:

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

وهنا منشور مدونة رائع "صياغة طريقة task.TimeOtafter" (من فريق MS Parallel Library) مع مزيد من المعلومات حول هذا النوع من الأشياء.

إضافة: بناءً على طلب تعليق على إجابتي ، إليك حل موسع يتضمن معالجة الإلغاء. لاحظ أن تمرير الإلغاء إلى المهمة والموقت يعني أن هناك طرقًا متعددة يمكن أن يكون الإلغاء تجربة في الكود الخاص بك ، ويجب أن تكون متأكدًا من اختبارها وتكون واثقًا من أنك تتعامل معها جميعًا بشكل صحيح. لا تترك للصدفة مجموعات مختلفة وآمل أن يقوم جهاز الكمبيوتر الخاص بك بالشيء الصحيح في وقت التشغيل.

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
}

نصائح أخرى

فيما يلي إصدار طريقة تمديد يتضمن إلغاء المهلة عندما تكمل المهمة الأصلية كما اقترح أندرو أرنوت في تعليق على إجابته.

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.");
        }
    }
}

يمكنك استخدام Task.WaitAny لانتظار أول مهام متعددة.

يمكنك إنشاء مهمتين إضافيتين (تكتمل بعد المهلة المحددة) ثم استخدامها WaitAny لانتظار أيهما يكمل أولاً. إذا كانت المهمة التي تم الانتهاء منها أولاً هي مهمة "العمل" ، فعليك الانتهاء. إذا كانت المهمة التي تم الانتهاء منها أولاً مهمة مهلة ، فيمكنك الرد على المهلة (مثل إلغاء الطلب).

ماذا عن شيء مثل هذا؟

    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 دون حظر مؤشر الترابط الرئيسي باستخدام مهمة أخرى.

فيما يلي مثال يعمل بالكامل على أساس الجواب الذي تم التصويت عليه ، وهو:

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 ثانية) ، فسيعرض إما خطأ ويستمر في طريقه المرح ، أو إعادة المحاولة.

إستخدم مؤقت للتعامل مع الرسالة والإلغاء التلقائي. عندما تكمل المهمة ، اتصل على الموقتات حتى لا تطلق النار أبدًا. هنا مثال؛ تغيير 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 التي ستلف أجهزة ضبط الوقت في مهام لك. هذا يمكن أن يمنحك المزيد من التحكم في القيام بأشياء مثل تعيين 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;
}

باستخدام ستيفن كليري ممتاز Asyncex مكتبة ، يمكنك القيام بها:

TimeSpan timeout = TimeSpan.FromSeconds(10);

using (var cts = new CancellationTokenSource(timeout))
{
    await myTask.WaitAsync(cts.Token);
}

TaskCanceledException سيتم إلقاؤه في حالة حدوث مهلة.

طريقة أخرى لحل هذه المشكلة هي استخدام الامتدادات التفاعلية:

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;

نسخة عامة من إجابة @Kevan أعلاه مع امتدادات تفاعلية.

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

مع جدولة اختيارية:

public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, Scheduler scheduler = null)
{
    return scheduler == null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

راجع للشغل: عندما تحدث مهلة ، سيتم طرح استثناء مهلة

إذا كنت تستخدم LlockingCollection لجدولة المهمة ، فيمكن للمنتج تشغيل المهمة التي يمكن أن تكون طويلة المدى ، ويمكن للمستهلك استخدام طريقة Trytake التي تحتوي على مهلة وإلغاء الرمز المميز.

هذه نسخة محسنة قليلاً من الإجابات السابقة.

  • بالإضافة إلى إجابة لورانس, ، يلغي المهمة الأصلية عند حدوث المهلة.
  • في الإضافة إلى متغيرات إجابة SJB 2 و 3, ، يمكنك توفير CancellationToken للمهمة الأصلية ، وعندما تحدث المهلة ، تحصل TimeoutException بدلاً من OperationCanceledException.
async Task<TResult> CancelAfterAsync<TResult>(Func<CancellationToken, Task<TResult>> startTask, TimeSpan timeout, CancellationToken cancellationToken)
{
    using (var timeoutCancellation = new CancellationTokenSource())
    using (var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token))
    {
        var originalTask = startTask(combinedCancellation.Token);
        var delayTask = Task.Delay(timeout, combinedCancellation.Token);
        var completedTask = await Task.WhenAny(originalTask, delayTask);
        // Cancel timeout to stop either task:
        // - Either the original task completed, so we need to cancel the delay task.
        // - Or the timeout expired, so we need to cancel the original task.
        // Canceling will not affect a task, that is already completed.
        timeoutCancellation.Cancel();
        if (completedTask == originalTask)
        {
            // original task completed
            return await originalTask;
        }
        else
        {
            // timeout
            throw new TimeoutException();
        }
    }
}

بعض المتغيرات من إجابة أندرو أرنوت:

  1. إذا كنت ترغب في انتظار مهمة موجودة ومعرفة ما إذا كانت مكتملة أو توقيتها ، ولكن لا تريد إلغاؤها في حالة حدوث المهلة:

    public static async Task<bool> TimedOutAsync(this Task task, int timeoutMilliseconds)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        if (timeoutMilliseconds == 0) {
            return !task.IsCompleted; // timed out if not completed
        }
        var cts = new CancellationTokenSource();
        if (await Task.WhenAny( task, Task.Delay(timeoutMilliseconds, cts.Token)) == task) {
            cts.Cancel(); // task completed, get rid of timer
            await task; // test for exceptions or task cancellation
            return false; // did not timeout
        } else {
            return true; // did timeout
        }
    }
    
  2. إذا كنت ترغب في بدء مهمة عمل وإلغاء العمل في حالة حدوث المهلة:

    public static async Task<T> CancelAfterAsync<T>( this Func<CancellationToken,Task<T>> actionAsync, int timeoutMilliseconds)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        var taskCts = new CancellationTokenSource();
        var timerCts = new CancellationTokenSource();
        Task<T> task = actionAsync(taskCts.Token);
        if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
            timerCts.Cancel(); // task completed, get rid of timer
        } else {
            taskCts.Cancel(); // timer completed, get rid of task
        }
        return await task; // test for exceptions or task cancellation
    }
    
  3. إذا كان لديك مهمة تم إنشاؤها بالفعل تريد إلغاؤها في حالة حدوث مهلة:

    public static async Task<T> CancelAfterAsync<T>(this Task<T> task, int timeoutMilliseconds, CancellationTokenSource taskCts)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        var timerCts = new CancellationTokenSource();
        if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
            timerCts.Cancel(); // task completed, get rid of timer
        } else {
            taskCts.Cancel(); // timer completed, get rid of task
        }
        return await task; // test for exceptions or task cancellation
    }
    

تعليق آخر ، ستؤدي هذه الإصدارات إلى إلغاء الموقت إذا لم تحدث المهلة ، لذلك لن تتسبب المكالمات المتعددة في تكديس أجهزة ضبط الوقت.

SJB

شعرت Task.Delay() المهمة و CancellationTokenSource في الإجابات الأخرى كثيرًا لحالة الاستخدام الخاصة بي في حلقة شبكات ضيقة.

و رغم ذلك Joe Hoag يقوم بصياغة طريقة task.TimeOtafter على مدونات MSDN كانت ملهمة ، كنت متعبًا قليلاً من استخدامها TimeoutException للتحكم في التدفق لنفس السبب المذكور أعلاه ، لأنه من المتوقع أن تكون المهلة أكثر تواترا.

لذلك ذهبت مع هذا ، والذي يتعامل أيضًا مع التحسينات المذكورة في المدونة:

public static async Task<bool> BeforeTimeout(this Task task, int millisecondsTimeout)
{
    if (task.IsCompleted) return true;
    if (millisecondsTimeout == 0) return false;

    if (millisecondsTimeout == Timeout.Infinite)
    {
        await Task.WhenAll(task);
        return true;
    }

    var tcs = new TaskCompletionSource<object>();

    using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs,
        millisecondsTimeout, Timeout.Infinite))
    {
        return await Task.WhenAny(task, tcs.Task) == task;
    }
}

مثال على هذا النحو: على هذا النحو:

var receivingTask = conn.ReceiveAsync(ct);

while (!await receivingTask.BeforeTimeout(keepAliveMilliseconds))
{
    // Send keep-alive
}

// Read and do something with data
var data = await receivingTask;
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top