รอแบบอะซิงโครนัสสำหรับงาน <T> เพื่อให้หมดเวลา


387

ฉันต้องการรอให้ภารกิจ <T>เสร็จสมบูรณ์ด้วยกฎพิเศษบางอย่าง: หากยังไม่เสร็จหลังจาก X มิลลิวินาทีฉันต้องการแสดงข้อความถึงผู้ใช้ และถ้ามันยังไม่เสร็จสมบูรณ์หลังจาก Y มิลลิวินาทีผมต้องการที่จะทำงานโดยอัตโนมัติยกเลิกคำขอ

ฉันสามารถใช้Task.ContinueWithเพื่อรอแบบอะซิงโครนัสเพื่อให้งานเสร็จสมบูรณ์ (เช่นกำหนดเวลาการดำเนินการที่จะดำเนินการเมื่องานเสร็จสมบูรณ์) แต่ไม่อนุญาตให้ระบุการหมดเวลา ฉันสามารถใช้Task.Waitเพื่อรองานให้เสร็จสมบูรณ์พร้อมกับการหมดเวลา แต่จะบล็อกเธรดของฉัน ฉันจะรอแบบอะซิงโครนัสเพื่อให้งานเสร็จสมบูรณ์ด้วยการหมดเวลาได้อย่างไร


3
คุณพูดถูก ฉันประหลาดใจที่ไม่มีการหยุดพักชั่วคราว อาจอยู่ใน. NET 5.0 ... แน่นอนว่าเราสามารถสร้างการหมดเวลาในภารกิจของตัวเอง แต่นั่นไม่ดีสิ่งต่าง ๆ ต้องมาฟรี
Aliostad

4
ในขณะที่มันยังคงต้องใช้ตรรกะสำหรับสองชั้นหมดเวลาคุณอธิบาย .NET 4.5 CancellationTokenSourceไม่แน่นอนมีวิธีการที่ง่ายสำหรับการสร้างหมดเวลาตาม มีการโหลดเกินสองตัวไปยังตัวสร้างพร้อมใช้งานการหนึ่งทำให้เกิดการหน่วงเวลาเป็นจำนวนเต็มมิลลิวินาทีและอีกอันหนึ่งใช้การหน่วงเวลา TimeSpan
patridge

แหล่ง lib แบบง่ายที่สมบูรณ์ที่นี่: stackoverflow.com/questions/11831844/…

วิธีการแก้ปัญหาสุดท้ายกับรหัสที่มาทำงานเต็ม? ตัวอย่างที่ซับซ้อนมากขึ้นสำหรับการแจ้งข้อผิดพลาดในแต่ละเธรดและหลังจาก WaitAll แสดงข้อมูลสรุป
Kiquenet

คำตอบ:


565

เกี่ยวกับสิ่งนี้:

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

และนี่คือโพสต์บล็อกที่ยอดเยี่ยม "การสร้างงาน Task.TimeoutAfter Method" (จากทีม 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
}

86
มันควรจะกล่าวว่าแม้ว่า Task.Delay สามารถทำงานให้เสร็จก่อนที่จะใช้งานได้นานช่วยให้คุณสามารถจัดการกับสถานการณ์การหมดเวลาได้ แต่สิ่งนี้จะไม่ยกเลิกงานที่ใช้เวลานานนั้นเอง เมื่อใดก็ตามเพียงแจ้งให้คุณทราบว่าหนึ่งในงานที่ส่งผ่านไปนั้นเสร็จสมบูรณ์ คุณจะต้องใช้ CancellationToken และยกเลิกภารกิจระยะยาวด้วยตัวเอง
เจฟฟ์ชูมัคเกอร์

30
นอกจากนี้ยังอาจสังเกตได้ว่าTask.Delayงานได้รับการสนับสนุนโดยตัวจับเวลาระบบซึ่งจะยังคงติดตามจนกว่าการหมดเวลาจะหมดอายุไม่ว่าSomeOperationAsyncจะใช้เวลานานเท่าใด ดังนั้นหากข้อมูลโค้ดโดยรวมนี้ทำงานได้เป็นจำนวนมากในวงแคบคุณจะต้องใช้ทรัพยากรระบบสำหรับตัวนับจนกว่าจะหมดเวลาทั้งหมด วิธีการแก้ไขที่จะมีสิ่งCancellationTokenที่คุณส่งให้Task.Delay(timeout, cancellationToken)คุณยกเลิกเมื่อSomeOperationAsyncเสร็จสิ้นการปล่อยทรัพยากรตัวจับเวลา
Andrew Arnott

12
รหัสการยกเลิกกำลังทำงาน waaaay ทำงานมากเกินไป ลองทำดังนี้: หมดเวลา = 1,000 var cancelTokenSource = ใหม่ CancellationTokenSource (หมดเวลา); var cancelToken = tokenSource.Token; var task = SomeOperationAsync (cancelToken); ลอง {รองาน; // เพิ่มรหัสที่นี่เพื่อความสำเร็จที่สำเร็จ} catch (OperationCancelledException) {// เพิ่มรหัสที่นี่สำหรับกรณีหมดเวลา}
srm

3
@ilans โดยการรอTaskข้อยกเว้นใด ๆ ที่จัดเก็บโดยงานจะถูก rethrown ณ จุดนั้น สิ่งนี้จะช่วยให้คุณมีโอกาสที่จะจับOperationCanceledException(ถ้ายกเลิก) หรือข้อยกเว้นอื่น ๆ (ถ้าผิดพลาด)
Andrew Arnott

3
@TomexOu: คำถามคือวิธีการรอแบบอะซิงโครนัสเสร็จสิ้นภารกิจ Task.Wait(timeout)จะบล็อกพร้อมกันแทนที่จะรอแบบอะซิงโครนัส
Andrew Arnott

220

นี่เป็นวิธีขยายรุ่นที่รวมเอาการยกเลิกการหมดเวลาเมื่อเสร็จงานเดิมที่แนะนำโดยแอนดรู Arnott ในความคิดเห็นที่คำตอบของเขา

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

8
ให้คะแนนผู้ชายคนนี้ ทางออกที่สง่างาม และถ้าคุณโทรไม่ได้มีประเภทผลตอบแทนให้แน่ใจว่าคุณเพียงแค่ลบ TResult
ลูคัส

6
CancellationTokenSource เป็นแบบใช้ครั้งเดียวและควรอยู่ในusingบล็อก
PeterM

6
@ It'satrap รองานสองครั้งเพียงแค่ส่งคืนผลลัพธ์เมื่อรอครั้งที่สอง มันไม่ทำงานสองครั้ง คุณสามารถพูดได้ว่ามันเท่ากับtask.Result เมื่อดำเนินการสองครั้ง
M. Mimpen

7
งานต้นฉบับ ( task) จะยังคงทำงานต่อไปในกรณีที่หมดเวลาหรือไม่
jag

6
โอกาสในการปรับปรุงเล็กน้อย: TimeoutExceptionมีข้อความเริ่มต้นที่เหมาะสม แทนที่ด้วย "การดำเนินการหมดเวลา" ไม่เพิ่มคุณค่าใด ๆ และทำให้เกิดความสับสนโดยการอ้างว่ามีเหตุผลที่จะแทนที่มัน
Edward Brey

49

คุณสามารถใช้Task.WaitAnyเพื่อรองานแรกจากหลายงาน

คุณสามารถสร้างสองงานเพิ่มเติม (เสร็จสมบูรณ์หลังจากหมดเวลาที่กำหนด) จากนั้นใช้WaitAnyเพื่อรอว่างานใดเสร็จก่อน หากงานที่ทำเสร็จก่อนคืองาน "งาน" ของคุณแสดงว่าคุณทำเสร็จแล้ว หากงานที่เสร็จสิ้นก่อนเป็นงานหมดเวลาคุณสามารถตอบกลับการหมดเวลาได้ (เช่นการยกเลิกคำขอ)


1
ฉันเห็นเทคนิคนี้ที่ใช้โดย MVP ที่ฉันเคารพจริงๆมันดูดีกว่าคำตอบที่ฉันยอมรับ บางทีตัวอย่างจะช่วยให้ได้รับคะแนนมากขึ้น! ฉันต้องการเป็นอาสาสมัครที่จะทำมันยกเว้นผมไม่เคยมีประสบการณ์ในงานพอที่จะมั่นใจว่ามันจะเป็นประโยชน์ :)
GrahamMc

3
หนึ่งเธรดจะถูกบล็อก - แต่ถ้าคุณตกลงกับสิ่งนั้นก็ไม่มีปัญหา วิธีการแก้ปัญหาที่ฉันทำคือด้านล่างเนื่องจากไม่มีการบล็อกเธรด ฉันอ่านโพสต์บล็อกซึ่งดีจริงๆ
JJschk

@JJschk คุณพูดถึงว่าคุณใช้วิธีแก้ปัญหาbelow.... นั่นคืออะไร? ขึ้นอยู่กับการสั่งซื้อดังนั้น?
BozoJoe

และถ้าฉันไม่ต้องการให้งานช้าลงจะถูกยกเลิก? ฉันต้องการที่จะจัดการกับมันเมื่อมันเสร็จสิ้น แต่กลับมาจากวิธีการปัจจุบัน ..
Akmal Salikhov

18

แล้วเรื่องแบบนี้ล่ะ?

    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 โดยไม่ปิดกั้นเธรดหลักโดยใช้งานอื่น


ในความเป็นจริงในตัวอย่างนี้คุณไม่ได้รออยู่ข้างใน t1 แต่อยู่ที่งานด้านบน ฉันจะลองทำตัวอย่างที่ละเอียดกว่านี้
as-cii

14

นี่คือตัวอย่างการทำงานอย่างสมบูรณ์ตามคำตอบที่ได้รับการโหวตสูงสุดซึ่งคือ:

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 วินาที) จากนั้นแอปจะแสดงข้อผิดพลาดและดำเนินการต่อไปอย่างร่าเริงหรือลองอีกครั้ง


11

การใช้ห้องสมุดAsyncEx ที่ยอดเยี่ยมของ Stephen Cleary คุณสามารถทำได้:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException จะถูกโยนทิ้งในกรณีที่หมดเวลา


10

นี่เป็นคำตอบก่อนหน้านี้ที่ปรับปรุงเล็กน้อย

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, timeoutCancellation.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();
        }
    }
}

การใช้

InnerCallAsyncอาจใช้เวลานานกว่าจะเสร็จสมบูรณ์ CallAsyncล้อมรอบด้วยการหมดเวลา

async Task<int> CallAsync(CancellationToken cancellationToken)
{
    var timeout = TimeSpan.FromMinutes(1);
    int result = await CancelAfterAsync(ct => InnerCallAsync(ct), timeout,
        cancellationToken);
    return result;
}

async Task<int> InnerCallAsync(CancellationToken cancellationToken)
{
    return 42;
}

1
ขอบคุณสำหรับการแก้ปัญหา! ดูเหมือนว่าคุณควรผ่านtimeoutCancellationเข้าdelayTaskมา ขณะนี้หากคุณยกเลิกการยิงCancelAfterAsyncอาจโยนTimeoutExceptionแทนTaskCanceledExceptionสาเหตุdelayTaskอาจเสร็จสิ้นก่อน
AxelUser

@AxelUser คุณพูดถูก ฉันใช้เวลาชั่วโมงกับการทดสอบหน่วยเพื่อทำความเข้าใจกับสิ่งที่เกิดขึ้น :) ฉันคิดว่าเมื่องานทั้งสองWhenAnyถูกยกเลิกโดยโทเค็นเดียวกันWhenAnyจะส่งคืนงานแรก ข้อสันนิษฐานนั้นผิด ฉันแก้ไขคำตอบแล้ว ขอบคุณ!
Josef Bláha

ฉันมีเวลายากที่จะหาวิธีเรียกสิ่งนี้ด้วยฟังก์ชั่น Task <SomeResult> ที่กำหนดไว้ โอกาสใด ๆ ที่คุณสามารถตะปูบนตัวอย่างของวิธีการเรียกมันได้หรือไม่
jhaagsma

1
@jhaagsma ตัวอย่างเพิ่ม!
Josef Bláha

@ JosefBláhaขอบคุณมาก! ฉันยังคงล้อมรอบหัวของฉันรอบ ๆ ไวยากรณ์ของแลมบ์ดาซึ่งจะไม่เกิดขึ้นกับฉัน - ที่โทเค็นถูกส่งไปยังงานในร่างกายของ CancelAfterAsync โดยผ่านฟังก์ชั่นแลมบ์ดา Nifty!
jhaagsma

8

ใช้ตัวจับเวลาเพื่อจัดการข้อความและการยกเลิกอัตโนมัติ เมื่องานเสร็จสมบูรณ์ให้โทรเลิกใช้งานตัวจับเวลาเพื่อที่จะไม่เริ่มทำงาน นี่คือตัวอย่าง; เปลี่ยน 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;
}

เขาไม่ต้องการเธรดปัจจุบันจะถูกบล็อก, ที่อยู่, task.Wait()ไม่มี
เฉินเฉิน

@ แดนนี่: นั่นเป็นเพียงการทำให้ตัวอย่างสมบูรณ์ หลังจาก ContinueWith คุณสามารถส่งคืนและปล่อยให้งานรัน ฉันจะอัปเดตคำตอบเพื่อให้ชัดเจนยิ่งขึ้น
Quartermeister

2
@dtb: จะเกิดอะไรขึ้นถ้าคุณสร้างงาน t1 <Task <Result>> จากนั้นเรียก TaskExtensions.Unwrap? คุณสามารถส่งคืน t2 จากแลมบ์ดาภายในของคุณและคุณสามารถเพิ่มการดำเนินการต่อไปยังภารกิจที่ยังไม่ได้เปิดในภายหลัง
Quartermeister

! น่ากลัว นั่นแก้ปัญหาของฉันได้อย่างสมบูรณ์แบบ ขอบคุณ! ฉันคิดว่าฉันจะไปกับวิธีแก้ปัญหาที่เสนอโดย @ AS-CII แม้ว่าฉันต้องการฉันสามารถยอมรับคำตอบของคุณได้เป็นอย่างดีสำหรับการแนะนำ
dtb

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;

4

รุ่นทั่วไปของคำตอบของ @ Kevan ด้านบนโดยใช้ Reactive Extensions

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

ด้วย Scheduler เพิ่มเติม:

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

BTW: เมื่อเกิดการหมดเวลาข้อยกเว้นการหมดเวลาจะถูกส่งออกไป


0

หากคุณใช้ BlockingCollection เพื่อกำหนดเวลางานผู้ผลิตสามารถเรียกใช้งานที่อาจใช้เวลานานและผู้บริโภคสามารถใช้วิธีการทดลองใช้ซึ่งมีโทเค็นการหมดเวลาและการยกเลิก


ฉันจะต้องเขียนบางอย่าง (ไม่ต้องการใส่รหัสกรรมสิทธิ์ที่นี่) แต่สถานการณ์เป็นเช่นนี้ ผู้สร้างจะเป็นรหัสที่ประมวลผลวิธีการที่อาจหมดเวลาและจะใส่ผลลัพธ์ลงในคิวเมื่อเสร็จสิ้น ผู้บริโภคจะโทรหา trytake () พร้อมไทม์เอาต์และจะได้รับโทเค็นเมื่อหมดเวลา ทั้งผู้ผลิตและผู้บริโภคจะเป็นงานแบ็คกราวน์และแสดงข้อความถึงผู้ใช้โดยใช้โปรแกรมเลือกจ่ายงานเธรด UI หากจำเป็น
kns98

0

ฉันรู้สึกว่าTask.Delay()งานและCancellationTokenSourceในคำตอบอื่น ๆ เล็กน้อยสำหรับกรณีการใช้งานของฉันในวงระบบเครือข่ายที่แน่น

และถึงแม้ว่าCrafting ของ Joe Hoag จะมีการสร้าง Task.TimeoutAfter Method บนบล็อก 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;

0

คำตอบบางส่วนของคำตอบของ Andrew Arnott:

  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


0

ฉันกำลังทบทวนแนวคิดของคำตอบอื่น ๆ ที่นี่และคำตอบนี้ในเธรดอื่นเป็นวิธีส่วนขยายแบบลอง สิ่งนี้มีประโยชน์ถ้าคุณต้องการวิธีการขยาย แต่หลีกเลี่ยงข้อยกเว้นเมื่อหมดเวลา

public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
    TimeSpan timeout, Action<TResult> successor)
{

    using var timeoutCancellationTokenSource = new CancellationTokenSource();
    var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                  .ConfigureAwait(continueOnCapturedContext: false);

    if (completedTask == task)
    {
        timeoutCancellationTokenSource.Cancel();

        // propagate exception rather than AggregateException, if calling task.Result.
        var result = await task.ConfigureAwait(continueOnCapturedContext: false);
        successor(result);
        return true;
    }
    else return false;        
}     

async Task Example(Task<string> task)
{
    string result = null;
    if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
    {
        Console.WriteLine(result);
    }
}    

-3

ไม่ทำเช่นนี้ แต่เป็นตัวเลือกถ้า ... ฉันไม่สามารถคิดเหตุผลที่ถูกต้องได้

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.