Thực hiện H # Chung Hết giờ


157

Tôi đang tìm kiếm những ý tưởng tốt để thực hiện một cách chung để có một dòng mã (hoặc đại biểu ẩn danh) thực thi mã với thời gian chờ.

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

Tôi đang tìm kiếm một giải pháp có thể được thực hiện một cách tao nhã ở nhiều nơi mà mã của tôi tương tác với mã khí chất (mà tôi không thể thay đổi).

Ngoài ra, tôi muốn có mã "hết thời gian" vi phạm đã ngừng thực thi thêm nếu có thể.


46
Chỉ cần một lời nhắc nhở cho bất cứ ai nhìn vào các câu trả lời dưới đây: Nhiều người trong số họ sử dụng Thread.Abort có thể rất tệ. Vui lòng đọc các ý kiến ​​khác nhau về điều này trước khi thực hiện Abort trong mã của bạn. Nó có thể thích hợp trong các dịp, nhưng những điều đó là hiếm. Nếu bạn không hiểu chính xác những gì Abort làm hoặc không cần nó, vui lòng thực hiện một trong những giải pháp dưới đây không sử dụng nó. Chúng là những giải pháp không có nhiều phiếu bầu vì chúng không phù hợp với nhu cầu của tôi.
chilltemp

Cảm ơn đã tư vấn. +1 phiếu bầu.
Queue Hammer

7
Để biết chi tiết về sự nguy hiểm của thread.Abort, hãy đọc bài viết này từ Eric Lippert: blog.msdn.com/b/ericlippert/archive/2010/02/22/ trên
JohnW

Câu trả lời:


95

Phần thực sự khó khăn ở đây là giết chết nhiệm vụ chạy dài thông qua việc chuyển luồng thực thi từ Hành động trở lại nơi có thể bị hủy bỏ. Tôi đã thực hiện điều này bằng cách sử dụng một đại biểu được bọc mà đưa ra luồng để giết thành một biến cục bộ trong phương thức tạo ra lambda.

Tôi gửi ví dụ này, để bạn thưởng thức. Phương pháp bạn thực sự quan tâm là CallWithTimeout. Điều này sẽ hủy chuỗi chạy dài bằng cách hủy bỏ nó và nuốt ThreadAdortException :

Sử dụng:

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

Phương thức tĩnh thực hiện công việc:

    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
Tại sao bắt (ThreadAdortException)? AFAIK bạn không thể thực sự bắt được một ThreadAdortException (nó sẽ được truy xuất lại sau khi khối bắt còn lại).
csgero

12
Thread.Abort () rất nguy hiểm khi sử dụng, Nó không nên được sử dụng với mã thông thường, chỉ có mã được đảm bảo an toàn mới bị hủy bỏ, chẳng hạn như mã đó là Cer.Safe, sử dụng các vùng thực thi bị ràng buộc và xử lý an toàn. Nó không nên được thực hiện cho bất kỳ mã.
Pop Catalin

12
Mặc dù Thread.Abort () rất tệ, nhưng nó không tệ bằng một quá trình mất kiểm soát và sử dụng mọi chu kỳ CPU & byte bộ nhớ mà PC có. Nhưng bạn có quyền chỉ ra những vấn đề tiềm ẩn cho bất kỳ ai khác có thể nghĩ rằng mã này hữu ích.
chilltemp

24
Tôi không thể tin rằng đây là câu trả lời được chấp nhận, ai đó không được đọc các bình luận ở đây hoặc câu trả lời đã được chấp nhận trước các bình luận và người đó không kiểm tra trang trả lời của anh ta. Thread.Abort không phải là một giải pháp, nó chỉ là một vấn đề khác bạn cần giải quyết!
Lasse V. Karlsen

18
Bạn là người không đọc các bình luận. Như chilltemp đã nói ở trên, anh ta gọi mã rằng anh ta KHÔNG có quyền kiểm soát - và muốn hủy bỏ nó. Anh ta không có lựa chọn nào khác ngoài Thread.Abort () nếu anh ta muốn điều này chạy trong quy trình của mình. Bạn nói đúng rằng Thread.Abort rất tệ - nhưng như chilltemp nói, những thứ khác còn tệ hơn!
TheSoftwareJedi

73

Chúng tôi đang sử dụng mã như thế này rất nhiều trong sản phẩm n:

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

Việc triển khai có nguồn mở, hoạt động hiệu quả ngay cả trong các tình huống tính toán song song và có sẵn như là một phần của Thư viện chia sẻ của Lokad

/// <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);
    }
}

Mã này vẫn còn lỗi, bạn có thể thử với chương trình thử nghiệm nhỏ này:

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

Có một điều kiện chủng tộc. Rõ ràng là có thể một ThreadAdortException được nêu ra sau khi phương thức WaitFor<int>.Run()được gọi. Tôi đã không tìm thấy một cách đáng tin cậy để khắc phục điều này, tuy nhiên với cùng một bài kiểm tra tôi không thể khắc phục bất kỳ vấn đề nào với câu trả lời được chấp nhận của TheSoftwareJedi .

nhập mô tả hình ảnh ở đây


3
Đây là những gì tôi đã thực hiện, Nó có thể xử lý các tham số và giá trị trả về, mà tôi thích và cần. Cảm ơn Rinat
Gabriel Mongeon

7
[Bất biến] là gì?
raklos

2
Chỉ là một thuộc tính chúng tôi sử dụng để đánh dấu các lớp bất biến (tính bất biến được xác minh bởi Mono Cecil trong các bài kiểm tra đơn vị)
Rinat Abdullin

9
Đây là một bế tắc đang chờ để xảy ra (Tôi ngạc nhiên khi bạn chưa quan sát thấy nó). Cuộc gọi của bạn đến xemThread.Abort () nằm trong một khóa, cũng cần phải có được trong khối cuối cùng. Điều này có nghĩa là trong khi khối cuối cùng đang chờ khóa (vì đã xemThread có nó giữa Wait () trở lại và Thread.Abort ()), cuộc gọi đã theo dõiThread.Abort () cũng sẽ chặn vô thời hạn chờ cuối cùng kết thúc sẽ không bao giờ). Therad.Abort () có thể chặn nếu một vùng mã được bảo vệ đang chạy - gây ra bế tắc, hãy xem - msdn.microsoft.com/en-us/l
Library / yd8d3wta.aspx

1
trickdev, cảm ơn rất nhiều. Vì một số lý do, sự xuất hiện bế tắc dường như rất không thường xuyên, nhưng chúng tôi đã sửa mã dù sao :-)
Joannes Vermorel

15

Chà, bạn có thể làm mọi việc với các đại biểu (BeginInvoke, với một cuộc gọi lại đặt cờ - và mã gốc đang chờ cờ hoặc thời gian chờ đó) - nhưng vấn đề là rất khó để tắt mã đang chạy. Ví dụ, giết (hoặc tạm dừng) một chuỗi là nguy hiểm ... vì vậy tôi không nghĩ có một cách dễ dàng để làm điều này một cách mạnh mẽ.

Tôi sẽ đăng bài này, nhưng lưu ý rằng nó không lý tưởng - nó không dừng nhiệm vụ dài hạn và nó không dọn dẹp đúng cách khi thất bại.

    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
Tôi hoàn toàn hạnh phúc khi giết một thứ gì đó đã làm tôi lo lắng. Nó vẫn tốt hơn là để nó ăn chu kỳ CPU cho đến lần khởi động lại tiếp theo (đây là một phần của dịch vụ windows).
chilltemp

@Marc: Tôi là fan hâm mộ tuyệt vời của bạn. Nhưng, lần này, tôi tự hỏi, tại sao bạn không sử dụng result.AsyncWaitHandle như được đề cập bởi TheSoftwareJedi. Bất kỳ lợi ích nào của việc sử dụng ManualResetEvent so với AsyncWaitHandle?
Anand Patel

1
@Anand tốt, đây là một vài năm trước vì vậy tôi không thể trả lời từ bộ nhớ - nhưng "dễ hiểu" được tính rất nhiều trong mã luồng
Marc Gravell

13

Một số thay đổi nhỏ đối với câu trả lời tuyệt vời của Pop Catalin:

  • Thay vì hành động
  • Ném ngoại lệ vào giá trị thời gian chờ xấu
  • Gọi EndInvoke trong trường hợp hết thời gian

Quá tải đã được thêm vào để hỗ trợ nhân viên báo hiệu để hủy thực thi:

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
}

Gọi (e => {// ... if (error) e.Cattery = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos

1
Thật đáng để chỉ ra rằng trong câu trả lời này, phương thức 'hết thời gian' vẫn còn hoạt động trừ khi có thể sửa đổi để lịch sự chọn thoát khi được gắn cờ 'hủy'.
David Eison

David, đó là những gì loại CancellingToken (.NET 4.0) được tạo riêng để giải quyết. Trong câu trả lời này, tôi đã sử dụng CancEventArss để nhân viên có thể thăm dò ý kiến ​​args.Cattery để xem liệu nó có nên thoát hay không, mặc dù điều này nên được thực hiện lại với CancellingToken cho .NET 4.0.
George Tsiokos

Một lưu ý sử dụng về điều này làm tôi bối rối trong một thời gian ngắn: Bạn cần hai khối thử / bắt nếu mã Chức năng / Hành động của bạn có thể ném ngoại lệ sau khi hết thời gian. Bạn cần một lần thử / bắt xung quanh lệnh gọi Gọi để bắt TimeoutException. Bạn cần một giây bên trong Chức năng / Hành động của mình để nắm bắt và nuốt / ghi lại bất kỳ ngoại lệ nào có thể xảy ra sau khi bạn hết thời gian chờ. Nếu không, ứng dụng sẽ chấm dứt với một ngoại lệ chưa được xử lý (trường hợp sử dụng của tôi là ping kiểm tra kết nối WCF trong thời gian chờ chặt hơn so với quy định trong app.config)
fiat

Hoàn toàn - vì mã bên trong chức năng / hành động có thể ném, nên nó phải nằm trong một lần thử / bắt. Theo quy ước, các phương thức này không cố gắng thử / bắt hàm / hành động. Đó là một thiết kế tồi để bắt và loại bỏ ngoại lệ. Như với tất cả các mã không đồng bộ, tùy thuộc vào người dùng phương thức thử / bắt.
George Tsiokos

10

Đây là cách tôi sẽ làm:

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
việc này không dừng nhiệm vụ thực thi
TheSoftwareJedi

2
Không phải tất cả các nhiệm vụ đều an toàn để dừng lại, tất cả các loại vấn đề có thể đến, bế tắc, rò rỉ tài nguyên, tham nhũng của nhà nước ... Không nên thực hiện trong trường hợp chung.
Pop Catalin

7

Tôi vừa loại bỏ điều này ngay bây giờ để nó có thể cần một số cải tiến, nhưng sẽ làm những gì bạn muốn. Nó là một ứng dụng giao diện điều khiển đơn giản, nhưng thể hiện các nguyên tắc cần thiết.

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
Đẹp. Điều duy nhất tôi muốn nói thêm là anh ta có thể thích ném System.TimeoutException hơn là chỉ System.Exception
Joel Coehoorn

Ồ, vâng: và tôi cũng bao bọc nó trong lớp học của riêng mình.
Joel Coehoorn

2

Còn việc sử dụng Thread.Join (int timeout) thì sao?

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
Điều đó sẽ thông báo cho phương thức gọi của một vấn đề, nhưng không hủy bỏ chuỗi vi phạm.
chilltemp

1
Tôi không chắc điều đó đúng. Không rõ tài liệu nào xảy ra với luồng công nhân khi hết thời gian Tham gia.
Matthew Lowe
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.