Najprostszy sposób na wykonanie metody odpal i zapomnij w C #?

150

Widziałem w WCF, że mają [OperationContract(IsOneWay = true)]atrybut. Jednak funkcja WCF wydaje się być powolna i ciężka, aby utworzyć funkcję nieblokującą. Idealnie byłoby coś takiego jak nieblokująca się statyczna pustkaMethodFoo(){} , ale nie sądzę, że istnieje.

Jaki jest najszybszy sposób utworzenia wywołania metody nieblokującej w języku C #?

Na przykład

class Foo
{
    static void Main()
    {
        FireAway(); //No callback, just go away
        Console.WriteLine("Happens immediately");
    }

    static void FireAway()
    {
        System.Threading.Thread.Sleep(5000);
        Console.WriteLine("5 seconds later");
    }
}

NB : Każdy, kto to czyta, powinien pomyśleć o tym, czy rzeczywiście chce, aby metoda została zakończona. (Zobacz pierwszą odpowiedź nr 2) Jeśli metoda musi się zakończyć, w niektórych miejscach, takich jak aplikacja ASP.NET, będziesz musiał zrobić coś, aby zablokować i utrzymać wątek przy życiu. W przeciwnym razie mogłoby to doprowadzić do „odpalenia-zapomnienia-ale-nigdy-nie-wykonania-wykonania”, w którym to przypadku oczywiście łatwiej byłoby napisać w ogóle żaden kod. ( Dobry opis tego, jak to działa w ASP.NET )

MatthewMartin
źródło

Odpowiedzi:

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

(pięć lat później...)

Task.Run(() => FireAway());

jak wskazał luisperezphd .


źródło
Wygrałeś. Nie, naprawdę wydaje się, że jest to najkrótsza wersja, chyba że umieścisz ją w innej funkcji.
OregonGhost
13
Myśląc o tym ... w większości aplikacji jest to w porządku, ale w twoim aplikacja konsoli zostanie zamknięta, zanim FireAway wyśle ​​cokolwiek do konsoli. Wątki ThreadPool są wątkami w tle i umierają, gdy aplikacja umiera. Z drugiej strony użycie wątku również nie zadziała, ponieważ okno konsoli zniknie, zanim FireAway spróbuje napisać do okna.
3
Nie ma sposobu, aby wywołanie metody nieblokującej było gwarantowane, więc jest to w rzeczywistości najdokładniejsza odpowiedź na pytanie IMO. Jeśli chcesz zagwarantować wykonanie, to potencjalne blokowanie należy wprowadzić za pomocą struktury kontrolnej, takiej jak AutoResetEvent (jak wspomniał Kev)
Guvante
1
Aby uruchomić zadanie w wątku niezależnym od puli wątków, myślę, że możesz również przejść(new Action(FireAway)).BeginInvoke()
Sam Harwell
2
A co powiesz Task.Factory.StartNew(() => myevent());na odpowiedź stackoverflow.com/questions/14858261/ ...
Luis Perez
39

W przypadku języka C # 4.0 i nowszych wydaje mi się, że najlepszą odpowiedzią jest teraz Ade Miller: Najprostszy sposób wykonania polecenia odpal i zapomnij w języku c # 4.0

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

Lub nawet...

Task.Factory.StartNew(FireAway);

Lub...

new Task(FireAway).Start();

Gdzie FireAway jest

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

Tak więc dzięki zwięzłości nazwy klasy i metody przewyższa wersję puli wątków o od sześciu do dziewiętnastu znaków w zależności od tego, który wybierzesz :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Patrick Szalapski
źródło
11
Najbardziej zależy mi na tym, aby najlepsze odpowiedzi na dobre pytania były łatwo dostępne. Zdecydowanie zachęcałbym innych do tego samego.
Patrick Szalapski
3
Według stackoverflow.com/help/referencing , musisz używać cytatów blokowych, aby wskazać, że cytujesz z innego źródła. Ponieważ to twoja odpowiedź wydaje się być twoją własną pracą. Twój zamiar ponownego opublikowania dokładnej kopii odpowiedzi mógł zostać osiągnięty za pomocą linku w komentarzu.
tom redfern
1
jak przekazać parametry do FireAway?
GuidoG
Uważam, że trzeba użyć pierwszego rozwiązania: Task.Factory.StartNew(() => FireAway(foo));.
Patrick Szalapski
1
zachowaj ostrożność podczas używania Task.Factory.StartNew(() => FireAway(foo));foo nie można modyfikować w pętli, jak wyjaśniono tutaj i tutaj
AaA
22

W przypadku platformy .NET 4.5:

Task.Run(() => FireAway());
David Murdoch
źródło
15
Fyi, to jest głównie skrót od Task.Factory.StartNew(() => FireAway(), CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);według blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Jim Geurts
2
Dobrze wiedzieć, @JimGeurts!
David Murdoch
W interesie potomnych artykuł, do którego link ma być @JimGeurts w ich komentarzu, jest teraz 404'd (dzięki, MS!). Biorąc pod uwagę sygnaturę daty w tym adresie
Dan Atkinson
1
@DanAtkinson masz rację. Oto oryginał na archive.org, na wszelki wypadek, gdyby MS ponownie go przeniósł: web.archive.org/web/20160226022029/http://blogs.msdn.com/b/…
David Murdoch
18

Aby dodać do odpowiedzi Willa , jeśli jest to aplikacja konsolowa, po prostu wrzuć an AutoResetEventi a, WaitHandleaby zapobiec zamknięciu go przed zakończeniem wątku roboczego:

Using System;
Using System.Threading;

class Foo
{
    static AutoResetEvent autoEvent = new AutoResetEvent(false);

    static void Main()
    {
        ThreadPoolQueueUserWorkItem(new WaitCallback(FireAway), autoEvent);
        autoEvent.WaitOne(); // Will wait for thread to complete
    }

    static void FireAway(object stateInfo)
    {
        System.Threading.Thread.Sleep(5000);
        Console.WriteLine("5 seconds later");
        ((AutoResetEvent)stateInfo).Set();
    }
}
Kev
źródło
Jeśli to nie jest aplikacja konsolowa, a moja klasa C # jest widoczna w modelu COM, czy Twoje zdarzenie AutoEvent będzie działać? Czy autoEvent.WaitOne () blokuje?
dan_l
5
@dan_l - Nie mam pojęcia, dlaczego nie zadać tego jako nowego pytania i nie odnieść się do tego.
Kev
@dan_l, tak, WaitOnezawsze blokuje i AutoResetEventzawsze będzie działać
AaA
AutoResetEvent naprawdę działa tutaj tylko wtedy, gdy istnieje pojedynczy wątek w tle. Zastanawiam się, czy Barrier byłby lepszy, gdy używanych jest wiele wątków. docs.microsoft.com/en-us/dotnet/standard/threading/barrier
Martin Brown
@MartinBrown - nie jestem pewien, czy to prawda. Możesz mieć tablicę WaitHandles, każda z własną AutoResetEventi odpowiadającą jej ThreadPool.QueueUserWorkItem. Po prostu WaitHandle.WaitAll(arrayOfWaitHandles).
Kev
15

Prostym sposobem jest utworzenie i uruchomienie wątku z bezparametrową lambdą:

(new Thread(() => { 
    FireAway(); 
    MessageBox.Show("FireAway Finished!"); 
}) { 
    Name = "Long Running Work Thread (FireAway Call)",
    Priority = ThreadPriority.BelowNormal 
}).Start();

Używając tej metody w ThreadPool.QueueUserWorkItem, możesz nazwać nowy wątek, aby ułatwić debugowanie. Nie zapomnij także o zastosowaniu obszernej obsługi błędów w swojej procedurze, ponieważ wszelkie nieobsłużone wyjątki poza debugerem nagle spowodują awarię aplikacji:

wprowadź opis obrazu tutaj

Robert Venables
źródło
+1, gdy potrzebujesz dedykowanego wątku z powodu długotrwałego lub blokującego procesu.
Paul Turner,
12

Zalecanym sposobem zrobienia tego, gdy używasz Asp.Net i .Net 4.5.2, jest użycie QueueBackgroundWorkItem. Oto klasa pomocnicza:

public static class BackgroundTaskRunner
{     
    public static void FireAndForgetTask(Action action)
    {
        HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => // .Net 4.5.2 required
        {
            try
            {
                action();
            }
            catch (Exception e)
            {
                // TODO: handle exception
            }
        });
    }

    /// <summary>
    /// Using async
    /// </summary>
    public static void FireAndForgetTask(Func<Task> action)
    {
        HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => // .Net 4.5.2 required
        {
            try
            {
                await action();
            }
            catch (Exception e)
            {
                // TODO: handle exception
            }
        });
    }
}

Przykład użycia:

BackgroundTaskRunner.FireAndForgetTask(() =>
{
    FireAway();
});

lub używając async:

BackgroundTaskRunner.FireAndForgetTask(async () =>
{
    await FireAway();
});

Działa to świetnie w witrynach sieci Web platformy Azure.

Dokumentacja: Używanie QueueBackgroundWorkItem do planowania zadań w tle z aplikacji ASP.NET w .NET 4.5.2

Augusto Barreto
źródło
7

Wywołanie funkcji beginInvoke bez przechwytywania EndInvoke nie jest dobrym podejściem. Odpowiedź jest prosta: Powodem, dla którego powinieneś wywołać EndInvoke, jest to, że wyniki wywołania (nawet jeśli nie ma wartości zwracanej) muszą być buforowane przez .NET do momentu wywołania EndInvoke. Na przykład, jeśli wywołany kod zgłasza wyjątek, wyjątek jest buforowany w danych wywołania. Dopóki nie wywołasz EndInvoke, pozostanie w pamięci. Po wywołaniu EndInvoke pamięć może zostać zwolniona. W tym konkretnym przypadku możliwe jest, że pamięć pozostanie do zakończenia procesu, ponieważ dane są utrzymywane wewnętrznie przez kod wywołania. Wydaje mi się, że GC może w końcu je zebrać, ale nie wiem, skąd GC wiedziałby, że porzuciłeś dane, a ich odzyskanie zajmuje naprawdę dużo czasu. Wątpię, że tak. Dlatego może wystąpić wyciek pamięci.

Więcej można znaleźć na http://haacked.com/archive/2009/01/09/asynchronous-fire-and-forget-with-lambdas.aspx

Manoj Aggarwal
źródło
3
GC może stwierdzić, kiedy rzeczy są porzucane, jeśli nie ma do nich odniesienia. Jeśli BeginInvokezwróci obiekt opakowania, który zawiera odniesienie do obiektu, który przechowuje rzeczywiste dane wynikowe, ale do niego się nie odwołuje, obiekt opakowania będzie kwalifikował się do finalizacji, gdy wszystkie odniesienia do niego zostaną porzucone.
supercat
Wątpię, czy to „nie jest dobre podejście”; przetestuj go, biorąc pod uwagę stany błędów, które Cię niepokoją, i sprawdź, czy masz problemy. Nawet jeśli czyszczenie pamięci nie jest doskonałe, prawdopodobnie nie wpłynie to zauważalnie na większość aplikacji. Według firmy Microsoft: „Możesz wywołać EndInvoke w celu pobrania wartości zwracanej przez delegata, jeśli jest to konieczne, ale nie jest to wymagane. EndInvoke będzie blokować, dopóki nie będzie można pobrać wartości zwracanej”. od: msdn.microsoft.com/en-us/library/0b1bf3y3(v=vs.90).aspx
Abacus
5

Prawie 10 lat później:

Task.Run(FireAway);

Dodałbym obsługę wyjątków i logowanie w FireAway

Oscar Fraxedas
źródło
3

Najprostszym podejściem .NET 2.0 i nowszym jest użycie Asynchnonous Programming Model (tj. BeginInvoke na delegacie):

static void Main(string[] args)
{
      new MethodInvoker(FireAway).BeginInvoke(null, null);

      Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);

      Thread.Sleep(5000);
}

private static void FireAway()
{
    Thread.Sleep(2000);

    Console.WriteLine("FireAway: " + Thread.CurrentThread.ManagedThreadId );  
}
Popiół
źródło
Wcześniej Task.Run()był to prawdopodobnie najbardziej zwięzły sposób na zrobienie tego.
binki