ใช้ C # Generic Timeout


157

ฉันกำลังมองหาความคิดที่ดีสำหรับการนำวิธีการทั่วไปไปใช้ในการเรียกใช้โค้ด (หรือผู้รับมอบสิทธิ์นิรนาม) บรรทัดเดียวด้วยการหมดเวลา

TemperamentalClass tc = new TemperamentalClass();
tc.DoSomething();  // normally runs in 30 sec.  Want to error at 1 min

ฉันกำลังมองหาโซลูชันที่สามารถนำไปใช้อย่างหรูหราในหลาย ๆ ที่ที่โค้ดของฉันโต้ตอบกับโค้ดเจ้าอารมณ์ (ที่ฉันไม่สามารถเปลี่ยนแปลงได้)

นอกจากนี้ฉันต้องการให้โค้ด "หมดเวลา" ที่ละเมิดมีการหยุดการทำงานเพิ่มเติมหากเป็นไปได้


46
เพียงเตือนความทรงจำให้กับทุกคนที่ดูคำตอบด้านล่าง: หลายคนใช้ Thread.Abort ซึ่งอาจแย่มาก โปรดอ่านความคิดเห็นต่าง ๆ เกี่ยวกับเรื่องนี้ก่อนที่จะใช้ Abort ในรหัสของคุณ อาจเหมาะสมในบางโอกาส แต่สิ่งเหล่านี้หายาก หากคุณไม่เข้าใจว่า Abort ทำอะไรหรือไม่ต้องการมันโปรดใช้วิธีการแก้ปัญหาด้านล่างที่ไม่ได้ใช้ พวกเขาคือคำตอบที่ไม่ได้รับคะแนนมากเพราะพวกเขาไม่ตรงกับความต้องการของคำถามของฉัน
chilltemp

ขอบคุณสำหรับคำแนะนำ +1 คะแนน
QueueHammer

7
สำหรับรายละเอียดเกี่ยวกับอันตรายของเธรดขอให้อ่านบทความนี้จาก Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/22/ …
JohnW

คำตอบ:


95

ส่วนที่ยุ่งยากจริงๆที่นี่คือการฆ่างานที่ใช้เวลานานผ่านการส่งเธรดผู้ปฏิบัติการจาก Action กลับไปยังที่ที่สามารถยกเลิกได้ ฉันทำสิ่งนี้สำเร็จด้วยการใช้ตัวแทนที่ถูกห่อหุ้มซึ่งส่งผ่านเธรดออกไปเพื่อฆ่าเข้าสู่ตัวแปรท้องถิ่นในวิธีการที่สร้างแลมบ์ดา

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

การใช้งาน:

class Program
{

    static void Main(string[] args)
    {
        //try the five second method with a 6 second timeout
        CallWithTimeout(FiveSecondMethod, 6000);

        //try the five second method with a 4 second timeout
        //this will throw a timeout exception
        CallWithTimeout(FiveSecondMethod, 4000);
    }

    static void FiveSecondMethod()
    {
        Thread.Sleep(5000);
    }

วิธีการคงที่ทำงาน:

    static void CallWithTimeout(Action action, int timeoutMilliseconds)
    {
        Thread threadToKill = null;
        Action wrappedAction = () =>
        {
            threadToKill = Thread.CurrentThread;
            try
            {
                action();
            }
            catch(ThreadAbortException ex){
               Thread.ResetAbort();// cancel hard aborting, lets to finish it nicely.
            }
        };

        IAsyncResult result = wrappedAction.BeginInvoke(null, null);
        if (result.AsyncWaitHandle.WaitOne(timeoutMilliseconds))
        {
            wrappedAction.EndInvoke(result);
        }
        else
        {
            threadToKill.Abort();
            throw new TimeoutException();
        }
    }

}

3
ทำไมถึงต้องจับ (ThreadAbortException)? AFAIK คุณไม่สามารถจับ ThreadAbortException ได้จริง ๆ (มันจะถูกโยนอีกครั้งหลังจากที่บล็อก catch ถูกทิ้งไว้)
csgero

12
Thread.Abort () อันตรายมากที่จะใช้มันไม่ควรใช้กับรหัสปกติเฉพาะรหัสที่รับประกันว่าปลอดภัยควรถูกยกเลิกเช่นรหัสที่ Cer.Safe ใช้ขอบเขตการดำเนินการที่ จำกัด และที่จับที่ปลอดภัย ไม่ควรใช้รหัสใด ๆ
Pop Catalin

12
ในขณะที่ Thread.Abort () ไม่ดีจะไม่มีที่ไหนที่ใกล้จะแย่เท่ากับกระบวนการที่ไม่สามารถควบคุมได้และใช้ซีพียูรอบ & ไบต์ทุกหน่วยความจำที่พีซีมี แต่คุณมีสิทธิ์ที่จะชี้ให้เห็นปัญหาที่อาจเกิดขึ้นกับคนอื่นที่อาจคิดว่ารหัสนี้มีประโยชน์
chilltemp

24
ฉันไม่อยากเชื่อเลยว่านี่เป็นคำตอบที่ได้รับการยอมรับบางคนต้องไม่อ่านความคิดเห็นที่นี่หรือคำตอบนั้นได้รับการยอมรับก่อนความคิดเห็นและบุคคลนั้นไม่ได้ตรวจสอบหน้าคำตอบของเขา Thread.Abort ไม่ใช่วิธีแก้ปัญหามันเป็นอีกปัญหาที่คุณต้องแก้ไข!
Lasse V. Karlsen

18
คุณเป็นคนที่ไม่ได้อ่านความคิดเห็น ตามที่ chilltemp บอกไว้ข้างต้นเขาเรียกรหัสว่าเขาไม่สามารถควบคุมได้ - และต้องการให้ยกเลิก เขาไม่มีตัวเลือกอื่นนอกจาก Thread.Abort () ถ้าเขาต้องการให้มันทำงานภายในกระบวนการของเขา คุณพูดถูกที่ Thread.Abort ไม่ดี - แต่อย่างที่ chilltemp พูดสิ่งอื่น ๆ แย่ลง!
TheSoftwareJedi

73

เรากำลังใช้รหัสอย่างนี้ใน productio n:

var result = WaitFor<Result>.Run(1.Minutes(), () => service.GetSomeFragileResult());

การใช้งานนั้นมาจากแหล่งเปิดใช้งานได้อย่างมีประสิทธิภาพแม้ในสถานการณ์การคำนวณแบบขนานและมีอยู่ในส่วนหนึ่งของLokad Shared Libraries

/// <summary>
/// Helper class for invoking tasks with timeout. Overhead is 0,005 ms.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
[Immutable]
public sealed class WaitFor<TResult>
{
    readonly TimeSpan _timeout;

    /// <summary>
    /// Initializes a new instance of the <see cref="WaitFor{T}"/> class, 
    /// using the specified timeout for all operations.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    public WaitFor(TimeSpan timeout)
    {
        _timeout = timeout;
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval. 
    /// </summary>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public TResult Run(Func<TResult> function)
    {
        if (function == null) throw new ArgumentNullException("function");

        var sync = new object();
        var isCompleted = false;

        WaitCallback watcher = obj =>
            {
                var watchedThread = obj as Thread;

                lock (sync)
                {
                    if (!isCompleted)
                    {
                        Monitor.Wait(sync, _timeout);
                    }
                }
                   // CAUTION: the call to Abort() can be blocking in rare situations
                    // http://msdn.microsoft.com/en-us/library/ty8d3wta.aspx
                    // Hence, it should not be called with the 'lock' as it could deadlock
                    // with the 'finally' block below.

                    if (!isCompleted)
                    {
                        watchedThread.Abort();
                    }
        };

        try
        {
            ThreadPool.QueueUserWorkItem(watcher, Thread.CurrentThread);
            return function();
        }
        catch (ThreadAbortException)
        {
            // This is our own exception.
            Thread.ResetAbort();

            throw new TimeoutException(string.Format("The operation has timed out after {0}.", _timeout));
        }
        finally
        {
            lock (sync)
            {
                isCompleted = true;
                Monitor.Pulse(sync);
            }
        }
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public static TResult Run(TimeSpan timeout, Func<TResult> function)
    {
        return new WaitFor<TResult>(timeout).Run(function);
    }
}

รหัสนี้ยังคงเป็นรถคุณสามารถลองกับโปรแกรมทดสอบขนาดเล็กนี้:

      static void Main(string[] args) {

         // Use a sb instead of Console.WriteLine() that is modifying how synchronous object are working
         var sb = new StringBuilder();

         for (var j = 1; j < 10; j++) // do the experiment 10 times to have chances to see the ThreadAbortException
         for (var ii = 8; ii < 15; ii++) {
            int i = ii;
            try {

               Debug.WriteLine(i);
               try {
                  WaitFor<int>.Run(TimeSpan.FromMilliseconds(10), () => {
                     Thread.Sleep(i);
                     sb.Append("Processed " + i + "\r\n");
                     return i;
                  });
               }
               catch (TimeoutException) {
                  sb.Append("Time out for " + i + "\r\n");
               }

               Thread.Sleep(10);  // Here to wait until we get the abort procedure
            }
            catch (ThreadAbortException) {
               Thread.ResetAbort();
               sb.Append(" *** ThreadAbortException on " + i + " *** \r\n");
            }
         }

         Console.WriteLine(sb.ToString());
      }
   }

มีสภาพการแข่งขัน เป็นไปได้อย่างชัดเจนว่า ThreadAbortException จะเพิ่มขึ้นหลังจากที่WaitFor<int>.Run()มีการเรียกเมธอด ฉันไม่พบวิธีที่เชื่อถือได้ในการแก้ไขปัญหานี้อย่างไรก็ตามด้วยการทดสอบเดียวกันฉันไม่สามารถทำซ้ำปัญหาใด ๆ กับคำตอบที่ได้รับการยอมรับTheSoftwareJedi

ป้อนคำอธิบายรูปภาพที่นี่


3
นี่คือสิ่งที่ฉันใช้มันสามารถจัดการพารามิเตอร์และค่าส่งคืนซึ่งฉันต้องการและจำเป็น ขอบคุณ Rinat
Gabriel Mongeon

7
[ไม่เปลี่ยนรูป] คืออะไร
raklos

2
เพียงแค่แอตทริบิวต์ที่เราใช้เพื่อทำเครื่องหมายคลาสที่ไม่เปลี่ยนรูป (การเปลี่ยนไม่ได้ถูกตรวจสอบโดยโมโนเซซิลในการทดสอบหน่วย)
Rinat Abdullin

9
นี่คือการหยุดชะงักที่รอให้เกิดขึ้น (ฉันประหลาดใจที่คุณยังไม่ได้สังเกต) การโทรไปดู Thread.Abort () ของคุณอยู่ในล็อคซึ่งจำเป็นต้องได้รับในบล็อกสุดท้าย ซึ่งหมายความว่าในขณะที่บล็อกสุดท้ายกำลังรอการล็อก (เนื่องจาก watchedThread มีไว้ระหว่างการรอ () ส่งคืนและ Thread.Abort ()) การเรียกใช้ watchedThread.Abort () จะบล็อกการรอจนกว่าจะเสร็จสิ้น (ซึ่งจะเป็นการสิ้นสุด ไม่เคยจะ) Therad.Abort () สามารถบล็อกได้ว่าพื้นที่ที่มีการป้องกันของรหัสกำลังทำงานอยู่หรือไม่ - ทำให้เกิดการหยุดชะงักโปรดดูที่ - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
trickdev ขอบคุณมาก ด้วยเหตุผลบางอย่างการหยุดชะงักเกิดขึ้นไม่บ่อยนัก แต่เราได้แก้ไขรหัสอย่างไรก็ตาม :-)
Joannes Vermorel

15

คุณสามารถทำสิ่งต่างๆกับผู้รับมอบสิทธิ์ (BeginInvoke ด้วยการตั้งค่าการเรียกกลับ - และรหัสต้นฉบับที่รอการตั้งค่าสถานะหรือการหมดเวลา) - แต่ปัญหาคือยากที่จะปิดการเรียกใช้รหัส ตัวอย่างเช่นการฆ่า (หรือหยุดชั่วคราว) เธรดเป็นอันตราย ... ดังนั้นฉันไม่คิดว่าจะมีวิธีที่ง่ายในการทำสิ่งนี้อย่างสมบูรณ์

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

    static void Main()
    {
        DoWork(OK, 5000);
        DoWork(Nasty, 5000);
    }
    static void OK()
    {
        Thread.Sleep(1000);
    }
    static void Nasty()
    {
        Thread.Sleep(10000);
    }
    static void DoWork(Action action, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate {evt.Set();};
        IAsyncResult result = action.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            action.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }
    static T DoWork<T>(Func<T> func, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate { evt.Set(); };
        IAsyncResult result = func.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            return func.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }

2
ฉันมีความสุขอย่างสมบูรณ์แบบที่จะฆ่าบางสิ่งที่ไปตกบนฉัน มันยังดีกว่าปล่อยให้มันกินซีพียูรอบจนกว่าจะรีบูตครั้งต่อไป (นี่เป็นส่วนหนึ่งของบริการ windows)
chilltemp

@ Marc: ฉันเป็นแฟนตัวยงของคุณ แต่คราวนี้ฉันสงสัยว่าทำไมคุณไม่ใช้ผลลัพธ์ AsyncWaitHandle ตามที่ TheSoftwareJedi กล่าวถึง ประโยชน์ของการใช้ ManualResetEvent ผ่าน AsyncWaitHandle?
Anand Patel

1
@ และนี่ก็เป็นเวลาหลายปีที่ผ่านมาดังนั้นฉันไม่สามารถตอบจากหน่วยความจำ - แต่ "ง่ายต่อการเข้าใจ" นับเป็นจำนวนมากในรหัสเธรด
Marc Gravell

13

การเปลี่ยนแปลงเล็กน้อยสำหรับคำตอบที่ยอดเยี่ยมของ Pop Catalin:

  • Func แทน Action
  • โยนข้อยกเว้นเกี่ยวกับค่าการหมดเวลาที่ไม่ดี
  • โทร EndInvoke ในกรณีที่หมดเวลา

มีการเพิ่มเกินพิกัดเพื่อสนับสนุนผู้ปฏิบัติงานการส่งสัญญาณเพื่อยกเลิกการดำเนินการ:

public static T Invoke<T> (Func<CancelEventArgs, T> function, TimeSpan timeout) {
    if (timeout.TotalMilliseconds <= 0)
        throw new ArgumentOutOfRangeException ("timeout");

    CancelEventArgs args = new CancelEventArgs (false);
    IAsyncResult functionResult = function.BeginInvoke (args, null, null);
    WaitHandle waitHandle = functionResult.AsyncWaitHandle;
    if (!waitHandle.WaitOne (timeout)) {
        args.Cancel = true; // flag to worker that it should cancel!
        /* •————————————————————————————————————————————————————————————————————————•
           | IMPORTANT: Always call EndInvoke to complete your asynchronous call.   |
           | http://msdn.microsoft.com/en-us/library/2e08f6yc(VS.80).aspx           |
           | (even though we arn't interested in the result)                        |
           •————————————————————————————————————————————————————————————————————————• */
        ThreadPool.UnsafeRegisterWaitForSingleObject (waitHandle,
            (state, timedOut) => function.EndInvoke (functionResult),
            null, -1, true);
        throw new TimeoutException ();
    }
    else
        return function.EndInvoke (functionResult);
}

public static T Invoke<T> (Func<T> function, TimeSpan timeout) {
    return Invoke (args => function (), timeout); // ignore CancelEventArgs
}

public static void Invoke (Action<CancelEventArgs> action, TimeSpan timeout) {
    Invoke<int> (args => { // pass a function that returns 0 & ignore result
        action (args);
        return 0;
    }, timeout);
}

public static void TryInvoke (Action action, TimeSpan timeout) {
    Invoke (args => action (), timeout); // ignore CancelEventArgs
}

เรียกใช้ (e => {// ... if (ข้อผิดพลาด) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos

1
เป็นค่าที่ชี้ให้เห็นว่าในคำตอบนี้วิธีการ 'หมดเวลาใช้งาน' จะยังคงทำงานต่อไปจนกว่าจะสามารถแก้ไขได้เพื่อเลือกออกอย่างสุภาพเมื่อตั้งค่าสถานะด้วย 'ยกเลิก'
David Eison

เดวิดนั่นคือประเภท CancellationToken (.NET 4.0) ที่สร้างขึ้นเป็นพิเศษเพื่อจัดการ ในคำตอบนี้ฉันใช้ CancelEventArgs เพื่อให้ผู้ปฏิบัติงานสามารถสำรวจ args.Cancel เพื่อดูว่าควรจะออกหรือไม่แม้ว่าสิ่งนี้ควรนำไปใช้กับ CancellationToken for .NET 4.0 อีกครั้ง
George Tsiokos

บันทึกการใช้งานที่ทำให้ฉันสับสนอยู่ครู่หนึ่ง: คุณต้องลอง / จับบล็อกสองครั้งหากรหัสฟังก์ชั่น / การกระทำของคุณอาจเกิดข้อผิดพลาดหลังจากหมดเวลา คุณต้องลอง / รับสายเพื่อเรียกใช้เพื่อตรวจจับ TimeoutException คุณต้องมีวินาทีในฟังก์ชั่น / การกระทำของคุณในการจับและกลืน / บันทึกข้อยกเว้นใด ๆ ที่อาจเกิดขึ้นหลังจากหมดเวลาของคุณ มิฉะนั้นแอปจะยุติด้วยข้อยกเว้นที่ไม่สามารถจัดการได้ (กรณีการใช้งานของฉันคือ ping ทดสอบการเชื่อมต่อ WCF ในการหมดเวลาที่เข้มงวดกว่าที่ระบุไว้ใน app.config)
fiat

อย่างแน่นอน - เนื่องจากโค้ดในฟังก์ชั่น / แอ็คชั่นสามารถขว้างได้มันจะต้องอยู่ในการลอง / จับ ตามระเบียบวิธีการเหล่านี้ไม่ได้พยายามที่จะลอง / จับฟังก์ชั่น / การกระทำ มันเป็นการออกแบบที่ไม่ดีที่จะจับและทิ้งข้อยกเว้น เช่นเดียวกับโค้ดอะซิงโครนัสทั้งหมดขึ้นอยู่กับผู้ใช้วิธีการลอง / จับ
George Tsiokos

10

นี่คือวิธีที่ฉันทำ:

public static class Runner
{
    public static void Run(Action action, TimeSpan timeout)
    {
        IAsyncResult ar = action.BeginInvoke(null, null);
        if (ar.AsyncWaitHandle.WaitOne(timeout))
            action.EndInvoke(ar); // This is necesary so that any exceptions thrown by action delegate is rethrown on completion
        else
            throw new TimeoutException("Action failed to complete using the given timeout!");
    }
}

3
สิ่งนี้จะไม่หยุดงานที่กำลังดำเนินการ
TheSoftwareJedi

2
ไม่ใช่ทุกงานที่ปลอดภัยที่จะหยุดปัญหาทุกชนิดสามารถมาถึงการหยุดชะงักการรั่วไหลของทรัพยากรการทุจริตของรัฐ ... ไม่ควรทำในกรณีทั่วไป
Pop Catalin

7

ฉันเพิ่งเคาะออกตอนนี้ดังนั้นมันอาจต้องปรับปรุงบางอย่าง แต่จะทำสิ่งที่คุณต้องการ มันเป็นแอพคอนโซลที่เรียบง่าย แต่แสดงให้เห็นถึงหลักการที่จำเป็น

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;


namespace TemporalThingy
{
    class Program
    {
        static void Main(string[] args)
        {
            Action action = () => Thread.Sleep(10000);
            DoSomething(action, 5000);
            Console.ReadKey();
        }

        static void DoSomething(Action action, int timeout)
        {
            EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
            AsyncCallback callback = ar => waitHandle.Set();
            action.BeginInvoke(callback, null);

            if (!waitHandle.WaitOne(timeout))
                throw new Exception("Failed to complete in the timeout specified.");
        }
    }

}

1
ดี สิ่งเดียวที่ฉันจะเพิ่มคือเขาอาจชอบที่จะโยน System.TimeoutException ไม่ใช่แค่ System.Exception
Joel Coehoorn

โอ้ใช่แล้วฉันจะห่อมันในชั้นเรียนของตัวเองด้วย
Joel Coehoorn

2

สิ่งที่เกี่ยวกับการใช้ Thread.Join (หมดเวลา int)?

public static void CallWithTimeout(Action act, int millisecondsTimeout)
{
    var thread = new Thread(new ThreadStart(act));
    thread.Start();
    if (!thread.Join(millisecondsTimeout))
        throw new Exception("Timed out");
}

1
ที่จะแจ้งวิธีการโทรของปัญหา แต่ไม่ยกเลิกกระทู้ที่กระทำผิด
chilltemp

1
ฉันไม่แน่ใจว่าถูกต้อง ไม่ชัดเจนจากเอกสารประกอบสิ่งที่เกิดขึ้นกับเธรดผู้ปฏิบัติงานเมื่อการหมดเวลาการเข้าร่วมหมดไป
Matthew Lowe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.