Terapkan C # Generic Timeout

157

Saya mencari ide bagus untuk menerapkan cara umum untuk memiliki satu baris (atau delegasi anonim) dari kode dieksekusi dengan batas waktu.

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

Saya mencari solusi yang dapat diimplementasikan secara elegan di banyak tempat di mana kode saya berinteraksi dengan kode temperamental (yang tidak dapat saya ubah).

Selain itu, saya ingin kode "habis waktu" yang menyinggung berhenti dari mengeksekusi lebih lanjut jika memungkinkan.

chilltemp
sumber
46
Sekadar pengingat kepada siapa pun yang melihat jawaban di bawah ini: Banyak dari mereka menggunakan Thread.Abort yang bisa sangat buruk. Silakan baca berbagai komentar tentang ini sebelum menerapkan Abort dalam kode Anda. Ini bisa sesuai pada kesempatan, tetapi itu jarang terjadi. Jika Anda tidak mengerti apa yang Abort lakukan atau tidak perlukan, harap terapkan salah satu solusi di bawah ini yang tidak menggunakannya. Mereka adalah solusi yang tidak memiliki banyak suara karena mereka tidak sesuai dengan kebutuhan pertanyaan saya.
chilltemp
Terima kasih atas nasihatnya. Memberi suara +1.
QueueHammer
7
Untuk rincian tentang bahaya Thread.Abort, membaca artikel ini dari Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/22/...
JohnW

Jawaban:

95

Bagian yang benar-benar rumit di sini adalah membunuh tugas yang sudah berjalan lama melalui melewati utas pelaksana dari Aksi kembali ke tempat di mana ia bisa dibatalkan. Saya menyelesaikan ini dengan menggunakan delegasi terbungkus yang membagikan utas untuk membunuh ke variabel lokal dalam metode yang menciptakan lambda.

Saya kirimkan contoh ini, untuk kesenangan Anda. Metode yang sangat menarik bagi Anda adalah CallWithTimeout. Ini akan membatalkan utas jangka panjang dengan membatalkannya, dan menelan ThreadAbortException :

Pemakaian:

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

Metode statis melakukan pekerjaan:

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

}
Perangkat LunakJedi
sumber
3
Mengapa menangkap (ThreadAbortException)? AFAIK Anda tidak dapat benar-benar menangkap ThreadAbortException (itu akan dipasang kembali setelah ketika blok tangkap dibiarkan).
csgero
12
Thread.Abort () sangat berbahaya untuk digunakan, tidak boleh digunakan dengan kode biasa, hanya kode yang dijamin aman harus dibatalkan, seperti kode yang Cer.Safe, menggunakan wilayah eksekusi terbatas dan pegangan aman. Seharusnya tidak dilakukan untuk kode apa pun.
Pop Catalin
12
Meskipun Thread.Abort () buruk, tidak ada yang sedekat proses kehabisan kontrol dan menggunakan setiap siklus CPU & byte memori yang dimiliki PC. Tetapi Anda benar untuk menunjukkan potensi masalah kepada orang lain yang mungkin menganggap kode ini berguna.
chilltemp
24
Saya tidak percaya ini adalah jawaban yang diterima, seseorang tidak boleh membaca komentar di sini, atau jawabannya diterima sebelum komentar dan orang itu tidak memeriksa halaman balasannya. Thread.Abort bukan solusi, itu hanya masalah lain yang perlu Anda pecahkan!
Lasse V. Karlsen
18
Kaulah yang tidak membaca komentar. Seperti yang dikatakan chilltemp di atas, dia memanggil kode yang TIDAK dia kendalikan - dan ingin itu dibatalkan. Dia tidak memiliki pilihan selain Thread. Amort () jika dia ingin ini berjalan dalam prosesnya. Anda benar bahwa Thread.Bort buruk - tetapi seperti kata chilltemp, hal lain lebih buruk!
TheSoftwareJedi
73

Kami banyak menggunakan kode seperti ini di productio n:

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

Implementasi bersumber terbuka, bekerja secara efisien bahkan dalam skenario komputasi paralel dan tersedia sebagai bagian dari 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);
    }
}

Kode ini masih bermasalah, Anda dapat mencoba dengan program uji kecil ini:

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

Ada kondisi balapan. Sangat mungkin bahwa ThreadAbortException dinaikkan setelah metode WaitFor<int>.Run()dipanggil. Saya tidak menemukan cara yang dapat diandalkan untuk memperbaikinya, namun dengan tes yang sama saya tidak dapat melaporkan masalah dengan jawaban yang diterima TheSoftwareJedi .

masukkan deskripsi gambar di sini

Rinat Abdullin
sumber
3
Ini yang saya implementasikan, dapat menangani parameter dan mengembalikan nilai, yang saya sukai dan butuhkan. Terima kasih Rinat
Gabriel Mongeon
7
Apa itu [Kekal]?
raklos
2
Hanya atribut yang kami gunakan untuk menandai kelas yang tidak dapat diubah (ketidakmampuan diverifikasi oleh Mono Cecil dalam unit test)
Rinat Abdullin
9
Ini adalah jalan buntu yang menunggu untuk terjadi (saya terkejut Anda belum mengamatinya). Panggilan Anda untuk menontonThread.Abort () ada di dalam kunci, yang juga perlu diperoleh di blok akhirnya. Ini berarti ketika blok terakhir sedang menunggu kunci (karena menontonThread memiliki itu antara Tunggu () kembali dan Thread.Abort ()), panggilan menontonThread.Abort () juga akan memblokir tanpa batas menunggu akhirnya selesai (yang tidak akan pernah). Therad.Abort () dapat memblokir jika wilayah kode yang dilindungi dijalankan - menyebabkan kebuntuan, lihat - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev
1
trickdev, terima kasih banyak. Untuk beberapa alasan, kemacetan tampaknya sangat jarang terjadi, tetapi kami tetap memperbaiki kode :-)
Joannes Vermorel
15

Nah, Anda bisa melakukan hal-hal dengan delegasi (BeginInvoke, dengan panggilan balik yang mengatur bendera - dan kode asli menunggu bendera atau batas waktu itu) - tetapi masalahnya adalah sangat sulit untuk mematikan kode yang sedang berjalan. Misalnya, membunuh (atau menjeda) utas berbahaya ... jadi saya tidak berpikir ada cara mudah untuk melakukan ini dengan kuat.

Saya akan memposting ini, tetapi perhatikan ini tidak ideal - tidak menghentikan tugas yang sudah berjalan lama, dan tidak membersihkan dengan benar pada kegagalan.

    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();
        }
    }
Marc Gravell
sumber
2
Saya sangat senang membunuh sesuatu yang hilang pada saya. Masih lebih baik daripada membiarkannya memakan siklus CPU sampai reboot berikutnya (ini adalah bagian dari layanan windows).
chilltemp
@ Markc: Saya penggemar berat Anda. Tapi, kali ini, saya bertanya-tanya, mengapa Anda tidak menggunakan result.AsyncWaitHandle seperti yang disebutkan oleh TheSoftwareJedi. Adakah manfaat menggunakan ManualResetEvent dibandingkan AsyncWaitHandle?
Anand Patel
1
@ Dan baiklah, ini beberapa tahun yang lalu jadi saya tidak bisa menjawab dari memori - tetapi "mudah dimengerti" sangat penting dalam kode ulir
Marc Gravell
13

Beberapa perubahan kecil pada jawaban hebat Pop Catalin:

  • Berfungsi sebagai ganti Aksi
  • Lempar pengecualian pada nilai batas waktu buruk
  • Memanggil EndInvoke jika ada batas waktu

Kelebihan telah ditambahkan untuk mendukung pekerja pemberi sinyal untuk membatalkan eksekusi:

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
}
George Tsiokos
sumber
Aktifkan (e => {// ... if (error) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos
1
Perlu ditunjukkan bahwa dalam jawaban ini metode 'habis waktu' dibiarkan berjalan kecuali dapat dimodifikasi untuk dengan sopan memilih untuk keluar ketika ditandai dengan 'batal'.
David Eison
David, itulah yang tipe PembatalanToken (.NET 4.0) secara khusus dibuat untuk mengatasi. Dalam jawaban ini, saya menggunakan CancelEventArgs sehingga pekerja dapat polling args.Cancel untuk melihat apakah harus keluar, meskipun ini harus diimplementasikan kembali dengan CancellingToken for .NET 4.0.
George Tsiokos
Catatan penggunaan tentang hal ini yang membingungkan saya untuk sementara waktu: Anda perlu dua blok coba / tangkap jika kode Fungsi / Tindakan Anda dapat mengeluarkan pengecualian setelah batas waktu. Anda perlu satu mencoba / menangkap panggilan untuk Invoke untuk menangkap TimeoutException. Anda memerlukan sedetik di dalam Fungsi / Tindakan Anda untuk menangkap dan menelan / mencatat pengecualian yang mungkin terjadi setelah lemparan waktu habis Anda. Kalau tidak, aplikasi akan berakhir dengan pengecualian yang tidak ditangani (kasus penggunaan saya adalah ping menguji koneksi WCF pada batas waktu yang lebih ketat dari yang ditentukan dalam app.config)
fiat
Tentu saja - karena kode di dalam fungsi / tindakan dapat melempar, itu harus di dalam coba / tangkap. Per konvensi, metode ini tidak mencoba untuk mencoba / menangkap fungsi / tindakan. Ini desain yang buruk untuk menangkap dan membuang pengecualian. Seperti halnya semua kode asinkron, terserah kepada pengguna metode untuk mencoba / menangkap.
George Tsiokos
10

Beginilah cara saya melakukannya:

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!");
    }
}
Pop Catalin
sumber
3
ini tidak menghentikan tugas eksekusi
TheSoftwareJedi
2
Tidak semua tugas aman untuk dihentikan, semua jenis masalah dapat tiba, kebuntuan, kebocoran sumber daya, korupsi negara ... Seharusnya tidak dilakukan dalam kasus umum.
Pop Catalin
7

Saya baru saja menghilangkan ini sekarang sehingga mungkin perlu beberapa perbaikan, tetapi akan melakukan apa yang Anda inginkan. Ini adalah aplikasi konsol sederhana, tetapi menunjukkan prinsip-prinsip yang diperlukan.

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

}
Jason Jackson
sumber
1
Bagus. Satu-satunya hal yang akan saya tambahkan adalah bahwa ia mungkin lebih suka membuang System.TimeoutException daripada hanya System.Exception
Joel Coehoorn
Oh, ya: dan saya akan membungkusnya di kelasnya sendiri juga.
Joel Coehoorn
2

Bagaimana dengan menggunakan Thread.Join (int timeout)?

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

sumber
1
Itu akan memberitahukan metode panggilan masalah, tetapi tidak membatalkan utas yang menyinggung.
chilltemp
1
Saya tidak yakin itu benar. Tidak jelas dari dokumentasi apa yang terjadi pada utas pekerja ketika batas waktu Gabung berlalu.
Matthew Lowe