C # 4.0'da ateş edip yöntemi unutmanın en basit yolu


106

Bu soruyu gerçekten beğendim:

C # ile ateş et ve unut yöntemini yapmanın en basit yolu?

Artık C # 4.0'da Paralel uzantılarımız olduğuna göre, Paralel linq ile Fire & Forget yapmanın daha temiz bir yolu var mı?


1
Bu sorunun cevabı hala .NET 4.0 için geçerlidir. Ateş et ve unut, QueueUserWorkItem'den daha basit olamaz.
Brian Rasmussen

Yanıtlar:


116

4.0 için bir yanıt değil, ancak .Net 4.5'te bunu aşağıdakilerle daha da basitleştirebileceğinizi belirtmek gerekir:

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

Pragma, size bu Görevi ateş ve unut olarak çalıştırdığınızı söyleyen uyarıyı devre dışı bırakmaktır.

Küme parantezlerinin içindeki yöntem bir Görev döndürürse:

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

Bunu parçalayalım:

Task.Run, bu kodun arka planda çalıştırılacağını belirten bir derleyici uyarısı (CS4014 uyarısı) oluşturan bir Task döndürür - tam olarak istediğiniz buydu, bu yüzden 4014 uyarısını devre dışı bırakıyoruz.

Varsayılan olarak, Görevler "Orijinal İş Parçacığına geri dönmeye" çalışır, bu da bu Görevin arka planda çalışacağı ve ardından onu başlatan İş Parçacığına geri dönmeye çalışacağı anlamına gelir. Orijinal İş Parçacığı tamamlandıktan sonra genellikle ateşleyin ve Görevler biter. Bu bir ThreadAbortException oluşturulmasına neden olur. Çoğu durumda bu zararsızdır - sadece size söylüyorum, yeniden katılmaya çalıştım, başarısız oldum, ama yine de umursamıyorsun. Ancak ThreadAbortExceptions'ın Üretimdeki günlüklerinizde veya yerel geliştirmedeki hata ayıklayıcınızda olması hala biraz gürültülü. .ConfigureAwait(false)bu sadece düzenli kalmanın bir yoludur ve açıkça şunu söyleyin, bunu arka planda çalıştırın ve hepsi bu.

Bu çok sözlü olduğu için, özellikle çirkin pragma, bunun için bir kütüphane yöntemi kullanıyorum:

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

Kullanım:

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

7
@ksm Yaklaşımınız maalesef bazı sorunlar için - test ettiniz mi? Bu yaklaşım tam olarak Warning 4014'ün var olmasının nedenidir. Beklemeden ve Task.Run ... yardımı olmadan zaman uyumsuz bir yöntemi çağırmak, bu yöntemin çalışmasına neden olur, evet, ancak tamamlandığında, ateşlendiği orijinal iş parçacığına geri dönmeye çalışacaktır. Genellikle bu iş parçacığı yürütmeyi zaten tamamlamış olacak ve kodunuz kafa karıştırıcı, belirsiz şekillerde patlayacaktır. Yapmayın! Task.Run çağrısı, "Bunu genel bağlamda çalıştır" demenin uygun bir yoludur ve bunu sıralamak için hiçbir şey bırakmaz.
Chris Moschini

1
@ChrisMoschini yardımcı olmaktan mutluluk duyar ve güncelleme için teşekkürler! Öte yandan, yukarıdaki yorumda "onu eşzamansız anonim bir işlevle çağırıyorsunuz" yazdığım yerde, bu gerçekten benim için gerçekten kafa karıştırıcı. Tek bildiğim, kodun, eşzamansız (anonim işlev) arama koduna dahil edilmediğinde çalıştığıdır, ancak bu, çağıran kodun eşzamansız bir şekilde çalıştırılmayacağı anlamına mı gelir (kötü, çok kötü bir durum)? Ben am Yani değil zaman uyumsuz olmadan tavsiye, o kadar bu durumda, her iki çalışma sadece garip.
Nicholas Petersen

8
Ben noktası görmüyorum ConfigureAwait(false)üzerinde Task.Rundeğil mi zaman awaitgörev. Fonksiyonun amacı kendi adındadır: "configure await ". Bunu yapmazsanız awaitgörevi, bir devamı kayıt yoktur ve dediğiniz gibi "orijinal iplik üzerine Mareşal arkasına" için görev için hiçbir kod yoktur. Daha büyük risk, gözlemlenmemiş bir istisnanın sonlandırıcı iş parçacığında yeniden ortaya çıkmasıdır, ki bu yanıt bu yanıtın bile değinmiyor.
Mike Strobel

3
@stricq Burada eşzamansız geçersiz kullanım yoktur. Async () => ... ifadesine atıfta bulunuyorsanız, imza void değil, Görev döndüren bir Func'dur.
Chris Moschini

2
VB.net'te uyarıyı bastırmak için:#Disable Warning BC42358
Altiano Gerung

86

İle Tasksınıf evet, ama PLINQ koleksiyonları üzerinde sorgulamak için gerçekten.

Aşağıdakine benzer bir şey bunu Görev ile yapacaktır.

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

Ya da...

Task.Factory.StartNew(FireAway);

Veya...

new Task(FireAway).Start();

nerede FireAwayolduğunu

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

Dolayısıyla, sınıf ve yöntem adı kısalığı sayesinde bu, iş parçacığı sürümünü, seçtiğiniz karaktere bağlı olarak altı ila on dokuz karakter arasında yener :)

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

Elbette işlevsellik eşdeğeri değiller mi?
Jonathon Kresner

6
StartNew ve new Task.Start arasında ince bir anlamsal fark vardır, ancak aksi halde evet. Hepsi iş parçacığı havuzundaki bir iş parçacığı üzerinde çalışmak için FireAway'i kuyruğa alır.
Ade Miller

Bu durum için çalışma: fire and forget in ASP.NET WebForms and windows.close()?
PreguntonCojoneroCabrón

34

Bu sorunun ana cevabıyla ilgili birkaç sorunum var.

Birincisi, gerçek bir ateş ve unut durumunda, muhtemelen awaitgörevi yapmayacaksınız , bu yüzden eklemek işe yaramaz ConfigureAwait(false). awaitTarafından döndürülen değeri almazsanız ConfigureAwait, muhtemelen herhangi bir etkisi olamaz.

İkinci olarak, görev bir istisna ile tamamlandığında ne olacağını bilmeniz gerekir. @ Ade-miller'in önerdiği basit çözümü düşünün:

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

Bu bir tehlike oluşturur: eğer işlenmemiş bir istisna kaçarsa SomeMethod(), bu istisna asla gözlenmez ve sonlandırıcı iş parçacığına yeniden atılarak uygulamanızın kilitlenmesi 1 olabilir. Bu nedenle, ortaya çıkan istisnaların gözlemlenmesini sağlamak için yardımcı bir yöntem kullanmanızı tavsiye ederim.

Bunun gibi bir şey yazabilirsiniz:

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

Bu uygulama minimum ek yüke sahip olmalıdır: Devam etme yalnızca görev başarıyla tamamlanmazsa başlatılır ve eşzamanlı olarak çağrılmalıdır (orijinal görevden ayrı olarak planlanmasının aksine). "Tembel" durumda, devam temsilcisi için bir tahsisat bile yapmazsınız.

Zaman uyumsuz bir işlemi başlatmak daha sonra önemsiz hale gelir:

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

1. Bu, .NET 4.0'daki varsayılan davranıştır. .NET 4.5, varsayılan davranış gözlemlenmemiş istisnalar olur, öyle ki değiştirildi değil (hala TaskScheduler üzerinde UnobservedTaskException olayı aracılığıyla onları gözlemlemek olabilir) sonlandırıcıyı iş parçacığı üzerinde rethrown olun. Ancak, varsayılan yapılandırma geçersiz kılınabilir ve uygulamanız .NET 4.5 gerektirse bile, gözlenmeyen görev istisnalarının zararsız olacağını varsaymamalısınız.


1
Bir işleyici aktarılırsa ve ContinueWithiptal nedeniyle çağrılırsa, öncülün istisna özelliği Null olur ve bir boş referans istisnası atılır. Görev devam ettirme seçeneğini olarak ayarlamak, OnlyOnFaultedboş kontrol ihtiyacını ortadan kaldırır veya kullanmadan önce istisnanın boş olup olmadığını kontrol eder.
sanmcp

Run () yönteminin farklı yöntem imzaları için geçersiz kılınması gerekir. Bunu Task'in bir uzatma yöntemi olarak yapmak daha iyidir.
sıkı

1
@stricq "Ateş et ve unut" işlemleri hakkında sorulan soru (yani, durum hiç kontrol edilmedi ve sonuç hiç gözlenmedi), bu yüzden odaklandım. Soru kendini ayağından en temiz şekilde nasıl vuracağı olunca, tasarım açısından hangi çözümün "daha iyi" olduğunu tartışmak oldukça tartışmalı hale geliyor :). En iyi cevap muhtemelen "yapma" dır, ancak bu minimum cevap uzunluğunun altında kaldı, bu yüzden diğer cevaplarda mevcut olan sorunları önleyen kısa bir çözüm sağlamaya odaklandım. Bağımsız değişkenler bir kapanışa kolayca sarılır ve hiçbir dönüş değeri olmadan kullanımı Taskbir uygulama ayrıntısı haline gelir.
Mike Strobel

@stricq Bu, sana katılmıyorum dedi. Önerdiğiniz alternatif, farklı nedenlerle de olsa geçmişte yaptığım şeydi. "Bir geliştirici hatalı bir durumu gözlemleyemediğinde uygulamanın çökmesini önlemek istiyorum Task" ile aynı şey değildir "Arka plan işlemini başlatmak için basit bir yol istiyorum, sonucunu asla gözlemlemiyorum ve hangi mekanizmanın kullanıldığı umurumda değil yapmak için." Bu temelde, sorulan soruya verdiğim cevabın arkasında duruyorum .
Mike Strobel

Bu durum için çalışma: fire and forgetin ASP.NET WebForms ve windows.close()?
PreguntonCojoneroCabrón

7

Mike Strobel'in cevabıyla ortaya çıkacak bazı sorunları düzeltmek için:

Bu var task = Task.Run(action)göreve bir devam ettirme kullanırsanız ve sonra atarsanız, o zaman Taskbir istisna işleyicisi devamı atamadan önce bazı istisna atma riskiyle karşılaşırsınız Task. Dolayısıyla, aşağıdaki sınıf bu riskten arındırılmış olmalıdır:

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

Burada, taskdoğrudan çalıştırılmaz, bunun yerine yaratılır, bir devam atanır ve ancak o zaman görev, bir devam atamadan önce görevin yürütmeyi tamamlama (veya bazı istisna atma) riskini ortadan kaldırmak için çalıştırılır.

Buradaki Runyöntem devamı döndürür, Taskböylece yürütmenin tamamlandığından emin olarak birim testleri yazabilirim. Yine de kullanımınızda güvenle görmezden gelebilirsiniz.


Bunu statik bir sınıf yapmamanız da kasıtlı mı?
Deantwo

Bu sınıf IAsyncManager arabirimini uygular, bu nedenle statik bir sınıf olamaz.
Mert Akçakaya

Bunun ele aldığı durumun (devam eklenmeden önceki görev hataları) önemli olduğunu sanmıyorum. Bu olursa, hatalı durum ve istisna görevde depolanır ve eklendiğinde devam tarafından ele alınır. Anlaşılması gereken önemli nokta, istisnanın bir görev / görev bekleyene kadar atılmamasıdır.Wait () / task.Result / task.GetAwaiter (). GetResult () - bkz. Docs.microsoft.com/en-us/dotnet/ standart / paralel programlama /… .
Rhys Jones
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.