Pytanie o czyste zakończenie wątku in.NET

Rozumiem wątek.Abort() jest złem z mnóstwa artykułów, które przeczytałem na ten temat, więc jestem obecnie w trakcie wyrywania z mojego abort ' a, aby zastąpić go czystszym sposobem; i po porównaniu strategii użytkowników od ludzi tutaj na stackoverflow, a następnie po przeczytaniu "Jak: tworzyć i kończyć wątki (C# Programming Guide)" z MSDN oba, które określają podejście bardzo podobne -- czyli wykorzystanie volatile bool strategii sprawdzania podejścia, co jest miłe, ale mam jeszcze kilka pytań....

Od razu to, co wyróżnia mnie tutaj, to co, jeśli nie masz prostego procesu roboczego, który po prostu uruchamia pętlę chrupiącego kodu? Na przykład dla mnie, mój Proces jest procesem przesyłania plików w tle, robię w rzeczywistości pętlę przez każdy plik, więc to coś, i na pewno mogę dodać mój while (!_shouldStop) na górze, który obejmuje mnie każdą iterację pętli, ale mam wiele innych procesów biznesowych, które występują, zanim trafi to następna pętla iteracja, chcę, aby ta procedura anulowania była zgrabna; nie mów mi, że muszę posypywać te pętle, podczas gdy pętle co 4-5 linii w dół przez całą moją funkcję pracownika?!

Mam nadzieję, że jest lepszy sposób, czy ktoś mógłby mi doradzić, czy jest to w rzeczywistości poprawne [i tylko?] podejście do tego, lub strategie, których używali w przeszłości, aby osiągnąć to, czego szukam.

Dzięki.

Czytaj dalej: wszystkie te odpowiedzi zakładają, że wątek roboczy będzie pętla. To mi nie odpowiada. Co jeśli jest to liniowa, ale terminowa operacja tła?

Author: Community, 2010-09-03

8 answers

Niestety nie może być lepszej opcji. To naprawdę zależy od konkretnego scenariusza. Chodzi o to, aby zatrzymać wątek wdzięcznie w bezpiecznych punktach. To jest sedno powodu, dla którego Thread.Abort nie jest dobre; ponieważ nie jest gwarantowane, aby wystąpić w bezpiecznych punktach. Posypując kod mechanizmem zatrzymującym skutecznie ręcznie definiujesz bezpieczne punkty. To się nazywa spółdzielcze anulowanie. Zasadniczo istnieją 4 szerokie mechanizmy tego działania. Możesz wybrać ten, który najlepiej pasuje do twojej sytuacji.

Poll a stopping flag

Wspomniałeś już o tej metodzie. To dość powszechne. Dokonaj okresowych kontroli flagi w bezpiecznych punktach algorytmu i wycofaj się, gdy zostanie ona zasygnalizowana. Standardowym podejściem jest oznaczenie zmiennej volatile. Jeśli nie jest to możliwe lub niewygodne, możesz użyć lock. Pamiętaj, że nie możesz oznaczyć zmiennej lokalnej jako volatile, więc jeśli wyrażenie lambda przechwytuje ją przez zamknięcie, na przykład, wtedy będziesz musiał uciekać się do innej metody tworzenia bariery pamięci, która jest wymagana. Nie ma wiele więcej, co trzeba powiedzieć o tej metodzie.

Wykorzystanie nowych mechanizmów anulowania w TPL

Jest to podobne do przepytywania flagi zatrzymania, z tym że wykorzystuje ona nowe struktury danych anulowania w TPL. Jest on nadal oparty na wzorcach anulowania współpracy. Musisz uzyskać CancellationToken i okresowo sprawdzić IsCancellationRequested. Aby zażądać anulowanie można zadzwonić Cancel na CancellationTokenSource, który pierwotnie dostarczył token. Z nowymi mechanizmami anulowania można wiele zrobić. Możesz przeczytać więcej o tutaj .

Użyj uchwytów wait

Ta metoda może być przydatna, jeśli Twój wątek roboczy wymaga oczekiwania na określony interwał lub na sygnał podczas normalnej pracy. Możesz Set a ManualResetEvent, na przykład, aby poinformować wątek, że nadszedł czas, aby przestać. Możesz przetestować Zdarzenie za pomocą WaitOne funkcja zwracająca bool wskazująca, czy zdarzenie zostało zasygnalizowane. WaitOne pobiera parametr określający czas oczekiwania na powrót wywołania, jeśli zdarzenie nie zostało zasygnalizowane w tym czasie. Możesz użyć tej techniki zamiast Thread.Sleep i jednocześnie uzyskać wskazanie zatrzymania. Jest to również przydatne, jeśli istnieją inne instancje WaitHandle, na które wątek może czekać. Możesz wywołać WaitHandle.WaitAny, aby czekać na każde zdarzenie (łącznie ze zdarzeniem stop) w jednym wywołaniu. Za pomocą zdarzenie może być lepsze niż wywołanie Thread.Interrupt, ponieważ masz większą kontrolę nad przepływem programu (Thread.Interrupt rzuca wyjątek, więc musisz strategicznie umieścić try-catch bloki, aby wykonać niezbędne czyszczenie).

Scenariusze specjalistyczne

Istnieje kilka jednorazowych scenariuszy, które mają bardzo wyspecjalizowane mechanizmy zatrzymywania. Zdecydowanie poza zakresem tej odpowiedzi jest wyliczenie ich wszystkich (nieważne, że byłoby to prawie niemożliwe). Dobry przykładem tego, co mam na myśli jest klasa Socket. Jeśli wątek jest zablokowany podczas połączenia do Send lub Receive, to wywołanie Close spowoduje przerwanie gniazda w dowolnym wywołaniu blokującym, które zostało skutecznie odblokowane. Jestem pewien, że istnieje kilka innych obszarów w BCL, w których można użyć podobnych technik, aby odblokować wątek.

Przerwij wątek poprzez Thread.Interrupt

Zaletą jest to, że jest to proste i nie musisz skupiać się na posypywaniu kodu cokolwiek. Wadą jest to, że masz niewielką kontrolę nad tym, gdzie znajdują się bezpieczne punkty w algorytmie. Powodem jest to, że Thread.Interrupt działa poprzez wstrzyknięcie wyjątku wewnątrz jednego z canned BCL blocking calls. Należą do nich Thread.Sleep, WaitHandle.WaitOne, Thread.Join, itd. Więc musisz być mądry, gdzie je umieszczasz. Jednak większość czasu algorytm dyktuje, gdzie idą, a to jest zwykle w porządku, zwłaszcza jeśli algorytm spędza większość swojego czasu w jednym z tych połączeń blokujących. Jeśli ty algorytm nie używa jednego z blokowania wywołań w BCL wtedy ta metoda nie będzie działać dla Ciebie. Istnieje teoria, że ThreadInterruptException jest generowane tylko z połączenia oczekującego. NET, więc jest prawdopodobnie w bezpiecznym punkcie. Przynajmniej wiesz, że wątek nie może być w niezarządzanym kodzie lub wyjść z krytycznej sekcji, pozostawiając zwisający Zamek w stanie nabytym. Mimo że jest to mniej inwazyjne niż Thread.Abort nadal odradzam jego stosowanie, ponieważ nie jest oczywiste, które wywołania na niego reagują a wielu programistów będzie nieznanych z jego niuansami.

 93
Author: Brian Gideon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-28 19:17:48

[5]}cóż, niestety w wielowątkowości często trzeba iść na kompromis" snappiness " dla czystości... możesz natychmiast opuścić wątek, jeśli Interrupt go, ale nie będzie zbyt czysty. Więc nie, nie musisz posypywać sprawdzeń _shouldStop co 4-5 linii, ale jeśli przerwiesz wątek, powinieneś obsłużyć wyjątek i wyjść z pętli w czysty sposób.

Update

Nawet jeśli nie jest to wątek zapętlający (tzn. być może jest to wątek, który wykonuje jakiś długotrwały asynchroniczna operacja lub jakiś blok do operacji wejścia), możesz Interrupt to, ale nadal powinieneś złapać ThreadInterruptedException i zamknąć wątek czysto. Myślę, że przykłady, które czytasz, są bardzo odpowiednie.

Aktualizacja 2.0

Tak, mam przykład... Po prostu pokażę Ci przykład oparty na linku, który podałeś:
public class InterruptExample
{
    private Thread t;
    private volatile boolean alive;

    public InterruptExample()
    {
        alive = false;

        t = new Thread(()=>
        {
            try
            {
                while (alive)
                {
                    /* Do work. */
                }
            }
            catch (ThreadInterruptedException exception)
            {
                /* Clean up. */
            }
        });
        t.IsBackground = true;
    }

    public void Start()
    {
        alive = true;
        t.Start();
    }


    public void Kill(int timeout = 0)
    {
        // somebody tells you to stop the thread
        t.Interrupt();

        // Optionally you can block the caller
        // by making them wait until the thread exits.
        // If they leave the default timeout, 
        // then they will not wait at all
        t.Join(timeout);
    }
}
 10
Author: Kiril,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 02:35:42

Jeśli anulowanie jest wymogiem rzeczy, którą budujesz, to powinno być traktowane z takim samym szacunkiem, jak reszta kodu-może to być coś, dla czego musisz zaprojektować.

Załóżmy, że Twój wątek robi jedną z dwóch rzeczy przez cały czas.

  1. coś związanego z procesorem
  2. oczekiwanie na jądro

Jeśli jesteś związany w danym wątku, prawdopodobnie masz dobre miejsce, aby wstawić czek. Jeśli dzwonisz do czyjś kod aby wykonać jakieś długo działające zadanie związane z CPU, możesz potrzebować naprawić Zewnętrzny kod, przenieść go z procesu (przerywanie wątków jest złe, ale przerywanie procesów jest dobrze zdefiniowane i bezpieczne), itp.

Jeśli czekasz na jądro, to prawdopodobnie jest uchwyt (lub fd, lub mach port, ...) zaangażowany w oczekiwanie. Zazwyczaj, jeśli zniszczysz odpowiedni uchwyt, jądro natychmiast powróci z jakimś kodem błędu. Jeśli jesteś w. Net / java / etc. prawdopodobnie skończysz z wyjątek. W języku C, jakikolwiek kod, który już masz na miejscu, aby obsłużyć awarie wywołań systemowych, będzie propagować błąd aż do znaczącej części aplikacji. Tak czy inaczej, wyłamujesz się z niskopoziomowego miejsca dość czysto i w bardzo terminowy sposób, bez potrzeby stosowania nowego kodu rozsypanego wszędzie.

Taktyką, której często używam w tego rodzaju kodzie, jest śledzenie listy uchwytów, które muszą zostać zamknięte, a następnie ustawienie przez moją funkcję abort znacznika "anulowane", a następnie ich zamknięcie. Gdy funkcja awarie może sprawdzić flagę i zgłosić awarię z powodu anulowania, a nie z powodu określonego wyjątku / errno.

Wydaje się sugerować, że akceptowalna szczegółowość odwołania jest na poziomie połączenia serwisowego. To chyba nie jest dobre myślenie-o wiele lepiej jest anulować pracę w tle synchronicznie i połączyć Stary wątek tła z wątkiem pierwszoplanowym. Jest o wiele czystszy Bo:

  1. Unika klasy rasy Warunki, gdy stare wątki bgwork wracają do życia po nieoczekiwanych opóźnieniach.

  2. Pozwala to uniknąć potencjalnych ukrytych wycieków wątku / pamięci spowodowanych wiszącymi procesami tła, umożliwiając ukrycie efektów wiszącego wątku tła.

Istnieją dwa powody, aby bać się tego podejścia:

  1. Nie sądzisz, że możesz przerwać swój własny kod w odpowiednim czasie. Jeśli anulowanie jest wymogiem Twojej aplikacji, decyzja, której naprawdę potrzebujesz aby to zasób / decyzja biznesowa: zrobić hack, lub naprawić swój problem czysto.

  2. Nie ufasz kodowi, który dzwonisz, bo jest poza Twoją kontrolą. Jeśli naprawdę mu nie ufasz, rozważ przeniesienie go poza proces. Uzyskasz znacznie lepszą izolację od wielu rodzajów ryzyka, w tym tego, w ten sposób.

 8
Author: blucz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:37:16

Być może częścią problemu jest to, że masz tak długą pętlę metody / while. Niezależnie od tego, czy masz problemy z gwintowaniem, powinieneś podzielić je na mniejsze etapy przetwarzania. Załóżmy, że te kroki To Alpha (), Bravo (), Charlie () i Delta().

Możesz wtedy zrobić coś takiego:

    public void MyBigBackgroundTask()
    {
        Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
        int workStepSize = 0;
        while (!_shouldStop)
        {
            tasks[workStepSize++]();
            workStepSize %= tasks.Length;
        };
    }

Więc tak, pętle bez końca, ale sprawdza, czy nadszedł czas, aby zatrzymać się między każdym krokiem biznesowym.

 2
Author: Brent Arias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:12:54

Nie musisz posypywać, gdy pętle są wszędzie. Zewnętrzna pętla while sprawdza tylko, czy powiedziano jej, aby się zatrzymała, a jeśli tak, to nie robi kolejnej iteracji...

Jeśli masz prosty wątek "go do something and close out" (nie ma w nim pętli), po prostu sprawdź wartość logiczną _shouldstop przed lub po każdym głównym miejscu wewnątrz wątku. W ten sposób wiesz, czy powinno się kontynuować, czy wycofać się.

Na przykład:

public void DoWork() {

  RunSomeBigMethod();
  if (_shouldStop){ return; }
  RunSomeOtherBigMethod();
  if (_shouldStop){ return; }
  //....
}

 2
Author: NotMe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:36:04

Najlepsza odpowiedź w dużej mierze zależy od tego, co robisz w wątku.

  • Jak powiedziałeś, większość odpowiedzi obraca się wokół ankietowania wspólnego logicznego co kilka linijek. Chociaż może Ci się nie spodobać, często jest to najprostszy schemat. Jeśli chcesz ułatwić sobie życie, możesz napisać metodę taką jak Throwifcanceled (), która rzuca jakiś wyjątek, jeśli skończysz. Puryści powiedzą, że jest to (gasp) użycie wyjątków dla przepływu sterowania, ale potem znowu cacelling jest wyjątkowa imo.

  • Jeśli wykonujesz operacje We / Wy (np. sieciowe), możesz rozważyć wykonanie wszystkiego przy użyciu operacji asynchronicznych.

  • Jeśli robisz sekwencję kroków, możesz użyć mnogiej sztuczki, aby stworzyć maszynę stanową. Przykład:

abstract class StateMachine : IDisposable
{
    public abstract IEnumerable<object> Main();

    public virtual void Dispose()
    {
        /// ... override with free-ing code ...
   }

   bool wasCancelled;

   public bool Cancel()
   {
     // ... set wasCancelled using locking scheme of choice ...
    }

   public Thread Run()
   {
       var thread = new Thread(() =>
           {
              try
              {
                if(wasCancelled) return;
                foreach(var x in Main())
                {
                    if(wasCancelled) return;
                }
              }
              finally { Dispose(); }
           });
       thread.Start()
   }
}

class MyStateMachine : StateMachine
{
   public override IEnumerabl<object> Main()
   {
       DoSomething();
       yield return null;
       DoSomethingElse();
       yield return null;
   }
}

// then call new MyStateMachine().Run() to run.

>

Overengineering? To zależy, ile maszyn stanowych używasz. Jeśli masz tylko 1, tak. Jeśli masz 100, to może nie. Zbyt trudne? To zależy. Inny zaletą tego podejścia jest to, że pozwala (z niewielkimi modyfikacjami) przenieść operację do timera.zaznacz opcję callback i void threading, jeśli ma to sens. I rób wszystko, co mówi blucz.
 2
Author: Glenn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:46:43

Zamiast dodawania pętli while, do której pętla nie należy, dodaj coś w rodzaju if (_shouldStop) CleanupAndExit(); tam, gdzie ma to sens. Nie ma potrzeby sprawdzania po każdej operacji lub posypywania ich kodem. Zamiast tego pomyśl o każdej czeki jako o szansie na wyjście z wątku w tym momencie i dodaj je strategicznie, mając to na uwadze.

 2
Author: Adrian Lopez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:59:30

Wszystkie te odpowiedzi zakładają, że wątek roboczy zostanie zapętlony. That doesn ' t sit comfortably with me

Nie ma zbyt wielu sposobów, aby Kod trwał długo. Zapętlenie jest dość istotną konstrukcją programistyczną. Tworzenie kodu zajmuje dużo czasu bez zapętlania zajmuje ogromną Ilość instrukcji. Setki tysięcy.

Lub wywołanie innego kodu, który robi pętlę za Ciebie. Tak, trudno zatrzymać ten kod na żądanie. To po prostu nie praca.

 1
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-03 00:19:12