Cara termudah untuk melakukan metode api dan lupakan di c # 4.0


101

Saya sangat suka pertanyaan ini:

Cara termudah untuk melakukan metode api dan lupa di C #?

Saya hanya ingin tahu bahwa sekarang kita memiliki ekstensi Paralel di C # 4.0, adakah cara yang lebih bersih untuk melakukan Fire & Forget dengan Parallel linq?


1
Jawaban atas pertanyaan itu masih berlaku untuk .NET 4.0. Aktifkan dan lupakan tidak jauh lebih sederhana dari QueueUserWorkItem.
Brian Rasmussen

Jawaban:


111

Bukan jawaban untuk 4.0, tetapi perlu dicatat bahwa di .Net 4.5 Anda dapat membuatnya lebih sederhana dengan:

#pragma warning disable 4014
Task.Run(() =>
{
    MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Pragma adalah menonaktifkan peringatan yang memberi tahu Anda bahwa Anda menjalankan Tugas ini sebagai api dan lupa.

Jika metode di dalam kurung kurawal mengembalikan Tugas:

#pragma warning disable 4014
Task.Run(async () =>
{
    await MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Mari kita uraikan:

Task.Run mengembalikan sebuah Tugas, yang menghasilkan peringatan kompilator (peringatan CS4014) yang mencatat bahwa kode ini akan dijalankan di latar belakang - itulah yang Anda inginkan, jadi kami menonaktifkan peringatan 4014.

Secara default, Tasks mencoba untuk "mengirim kembali ke Thread asli," yang berarti bahwa Tugas ini akan berjalan di latar belakang, kemudian mencoba untuk kembali ke Thread yang memulainya. Seringkali aktifkan dan lupakan Tugas selesai setelah Thread asli selesai. Itu akan menyebabkan ThreadAbortException dilempar. Dalam kebanyakan kasus, ini tidak berbahaya - hanya memberi tahu Anda, saya mencoba bergabung kembali, saya gagal, tetapi Anda toh tidak peduli. Tapi masih agak berisik untuk memiliki ThreadAbortExceptions baik di log Anda di Produksi, atau di debugger Anda di dev lokal..ConfigureAwait(false)hanyalah cara untuk tetap rapi dan secara eksplisit mengatakan, menjalankan ini di latar belakang, dan hanya itu.

Karena ini bertele-tele, terutama pragma jelek, saya menggunakan metode perpustakaan untuk ini:

public static class TaskHelper
{
    /// <summary>
    /// Runs a TPL Task fire-and-forget style, the right way - in the
    /// background, separate from the current thread, with no risk
    /// of it trying to rejoin the current thread.
    /// </summary>
    public static void RunBg(Func<Task> fn)
    {
        Task.Run(fn).ConfigureAwait(false);
    }

    /// <summary>
    /// Runs a task fire-and-forget style and notifies the TPL that this
    /// will not need a Thread to resume on for a long time, or that there
    /// are multiple gaps in thread use that may be long.
    /// Use for example when talking to a slow webservice.
    /// </summary>
    public static void RunBgLong(Func<Task> fn)
    {
        Task.Factory.StartNew(fn, TaskCreationOptions.LongRunning)
            .ConfigureAwait(false);
    }
}

Pemakaian:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}

7
@ksm Sayangnya pendekatan Anda berada dalam beberapa masalah - apakah Anda sudah mengujinya? Pendekatan itulah tepatnya mengapa Peringatan 4014 ada. Memanggil metode async tanpa menunggu, dan tanpa bantuan Task.Run ... akan menyebabkan metode tersebut berjalan, ya, tetapi setelah selesai, ia akan mencoba untuk kembali ke utas asli tempat ia ditembakkan. Seringkali utas itu telah menyelesaikan eksekusi dan kode Anda akan meledak dengan cara yang membingungkan dan tidak pasti. Jangan lakukan itu! Panggilan ke Task.Run adalah cara yang nyaman untuk mengatakan "Jalankan ini dalam konteks global," tanpa menyisakan apa pun untuk dicoba untuk marshal.
Chris Moschini

1
@ChrisMoschini senang membantu, dan terima kasih atas pembaruannya! Pada masalah lain, di mana saya menulis "Anda memanggilnya dengan fungsi anonim async" dalam komentar di atas, itu benar-benar membingungkan saya yang mana yang sebenarnya. Yang saya tahu adalah kode berfungsi ketika async tidak disertakan dalam kode pemanggil (fungsi anonim), tetapi apakah itu berarti kode panggilan tidak akan dijalankan dengan cara asinkron (yang buruk, gotcha yang sangat buruk)? Jadi saya tidak merekomendasikan tanpa async, hanya aneh bahwa dalam kasus ini, keduanya berfungsi.
Nicholas Petersen

8
Saya tidak melihat titik ConfigureAwait(false)pada Task.Runsaat Anda tidak awaittugas. Tujuan dari fungsi ini ada dalam namanya: "konfigurasi await ". Jika Anda tidak melakukan awaittugas tersebut, Anda tidak mendaftarkan kelanjutan, dan tidak ada kode untuk tugas "marshal kembali ke utas asli", seperti yang Anda katakan. Risiko yang lebih besar adalah pengecualian yang tidak teramati ditampilkan kembali pada utas finalizer, yang bahkan tidak ditangani oleh jawaban ini.
Mike Strobel

3
@stricq Tidak ada penggunaan ruang kosong asinkron di sini. Jika Anda merujuk ke async () => ... tandatangannya adalah Func yang mengembalikan Tugas, bukan batal.
Chris Moschini

2
Di VB.net untuk menekan peringatan:#Disable Warning BC42358
Altiano Gerung

85

Dengan Taskkelas ya, tetapi PLINQ benar-benar untuk menanyakan koleksi.

Sesuatu seperti berikut ini akan melakukannya dengan Tugas.

Task.Factory.StartNew(() => FireAway());

Atau bahkan...

Task.Factory.StartNew(FireAway);

Atau...

new Task(FireAway).Start();

dimana FireAwayadalah

public static void FireAway()
{
    // Blah...
}

Jadi berdasarkan ketegasan nama kelas dan metode ini mengalahkan versi threadpool dengan antara enam dan sembilan belas karakter tergantung pada yang Anda pilih :)

ThreadPool.QueueUserWorkItem(o => FireAway());

Tentunya mereka tidak setara dengan fungsionalitas?
Jonathon Kresner

6
Ada perbedaan semantik halus antara StartNew dan new Task. Start tapi sebaliknya, ya. Mereka semua mengantrekan FireAway untuk berjalan di thread di threadpool.
Ade Miller

Bekerja untuk kasus ini: fire and forget in ASP.NET WebForms and windows.close()?
PreguntonCojoneroCabrón

32

Saya memiliki beberapa masalah dengan jawaban utama untuk pertanyaan ini.

Pertama, dalam situasi api-dan-lupakan yang sebenarnya , Anda mungkin tidak akan awaitmelakukan tugas itu, jadi tidak ada gunanya menambahkan ConfigureAwait(false). Jika Anda tidak awaitmengembalikan nilai ConfigureAwait, maka itu tidak mungkin berpengaruh.

Kedua, Anda perlu menyadari apa yang terjadi saat tugas selesai dengan pengecualian. Pertimbangkan solusi sederhana yang disarankan @ ade-miller:

Task.Factory.StartNew(SomeMethod);  // .NET 4.0
Task.Run(SomeMethod);               // .NET 4.5

Ini menimbulkan bahaya: jika pengecualian yang tidak tertangani lolos dari SomeMethod(), pengecualian itu tidak akan pernah diamati, dan mungkin 1 dicabut kembali pada utas finalizer, membuat aplikasi Anda mogok. Oleh karena itu, saya akan merekomendasikan menggunakan metode helper untuk memastikan bahwa pengecualian yang dihasilkan diamati.

Anda bisa menulis sesuatu seperti ini:

public static class Blindly
{
    private static readonly Action<Task> DefaultErrorContinuation =
        t =>
        {
            try { t.Wait(); }
            catch {}
        };

    public static void Run(Action action, Action<Exception> handler = null)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        var task = Task.Run(action);  // Adapt as necessary for .NET 4.0.

        if (handler == null)
        {
            task.ContinueWith(
                DefaultErrorContinuation,
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
        else
        {
            task.ContinueWith(
                t => handler(t.Exception.GetBaseException()),
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
    }
}

Implementasi ini harus memiliki overhead minimal: kelanjutan hanya dipanggil jika tugas tidak berhasil diselesaikan, dan harus dijalankan secara sinkron (bukan dijadwalkan secara terpisah dari tugas asli). Dalam kasus "malas", Anda bahkan tidak akan dikenai alokasi untuk delegasi kelanjutan.

Memulai operasi asinkron kemudian menjadi sepele:

Blindly.Run(SomeMethod);                              // Ignore error
Blindly.Run(SomeMethod, e => Log.Warn("Whoops", e));  // Log error

1. Ini adalah perilaku default di .NET 4.0. Dalam .NET 4.5, perilaku default diubah sedemikian rupa sehingga pengecualian yang tidak teramati tidak akan ditampilkan kembali pada utas finalizer (meskipun Anda masih dapat mengamati mereka melalui acara UnobservedTaskException di TaskScheduler). Namun, konfigurasi default dapat diganti, dan meskipun aplikasi Anda memerlukan .NET 4.5, Anda tidak boleh berasumsi bahwa pengecualian tugas yang tidak teramati tidak akan berbahaya.


1
Jika penangan diteruskan dan ContinueWithdipanggil karena pembatalan, properti pengecualian anteseden akan menjadi Null dan pengecualian referensi null akan dilemparkan. Menetapkan opsi kelanjutan tugas ke OnlyOnFaultedakan menghilangkan kebutuhan akan pemeriksaan nol atau memeriksa apakah pengecualiannya null sebelum menggunakannya.
sanmcp

Metode Run () perlu diganti untuk tanda tangan metode yang berbeda. Lebih baik melakukan ini sebagai metode perpanjangan Tugas.
stricq

1
@stricq Pertanyaan yang diajukan tentang operasi "tembak dan lupakan" (yaitu, status tidak pernah diperiksa dan hasil tidak pernah diamati), jadi itulah yang saya fokuskan. Ketika pertanyaannya adalah bagaimana cara paling tepat untuk menembak diri sendiri, memperdebatkan solusi mana yang "lebih baik" dari perspektif desain menjadi agak diperdebatkan :). The terbaik Jawabannya adalah bisa dibilang "tidak", tapi itu jatuh pendek dari panjang minimum jawaban, jadi saya fokus pada penyediaan solusi singkat yang menghindari masalah hadir dalam jawaban lainnya. Argumen dengan mudah digabungkan dalam closure, dan tanpa nilai kembalian, menggunakan Taskmenjadi detail implementasi.
Mike Strobel

@stricq Yang mengatakan, saya tidak setuju dengan Anda. Alternatif yang Anda usulkan persis seperti yang telah saya lakukan di masa lalu, meskipun untuk alasan yang berbeda. "Saya ingin menghindari aplikasi mogok saat pengembang gagal mengamati kesalahan Task" tidak sama dengan "Saya ingin cara sederhana untuk memulai operasi latar belakang, tidak pernah mengamati hasilnya, dan saya tidak peduli mekanisme apa yang digunakan untuk melakukannya. " Atas dasar itu, saya mendukung jawaban saya atas pertanyaan yang diajukan .
Mike Strobel

Bekerja untuk kasus ini: fire and forgetdi ASP.NET WebForms dan windows.close()?
PreguntonCojoneroCabrón

7

Hanya untuk memperbaiki beberapa masalah yang akan terjadi dengan jawaban Mike Strobel:

Jika Anda menggunakan var task = Task.Run(action)dan setelah menetapkan kelanjutan untuk tugas itu, maka Anda akan menghadapi risiko Taskmembuat beberapa pengecualian sebelum Anda menetapkan kelanjutan penangan pengecualian ke Task. Jadi, kelas di bawah ini harus bebas dari risiko ini:

using System;
using System.Threading.Tasks;

namespace MyNameSpace
{
    public sealed class AsyncManager : IAsyncManager
    {
        private Action<Task> DefaultExeptionHandler = t =>
        {
            try { t.Wait(); }
            catch { /* Swallow the exception */ }
        };

        public Task Run(Action action, Action<Exception> exceptionHandler = null)
        {
            if (action == null) { throw new ArgumentNullException(nameof(action)); }

            var task = new Task(action);

            Action<Task> handler = exceptionHandler != null ?
                new Action<Task>(t => exceptionHandler(t.Exception.GetBaseException())) :
                DefaultExeptionHandler;

            var continuation = task.ContinueWith(handler,
                TaskContinuationOptions.ExecuteSynchronously
                | TaskContinuationOptions.OnlyOnFaulted);
            task.Start();

            return continuation;
        }
    }
}

Di sini, tasktidak dijalankan secara langsung, melainkan dibuat, kelanjutan ditetapkan, dan baru kemudian tugas dijalankan untuk menghilangkan risiko tugas menyelesaikan eksekusi (atau memunculkan beberapa pengecualian) sebelum menetapkan kelanjutan.

The RunMetode sini kembali kelanjutan Tasksehingga saya bisa menulis unit test memastikan eksekusi selesai. Anda dapat dengan aman mengabaikannya dalam penggunaan Anda.


Apakah itu juga disengaja agar Anda tidak menjadikannya kelas statis?
Deantwo

Kelas ini mengimplementasikan antarmuka IAsyncManager, sehingga tidak bisa menjadi kelas statis.
Mert Akcakaya
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.