Dlaczego Thread.Sleep jest tak szkodliwy


128

Często widzę, że wspomniano, że Thread.Sleep();nie należy go używać, ale nie rozumiem, dlaczego tak jest. Jeśli Thread.Sleep();może powodować problemy, czy są jakieś alternatywne rozwiązania z takim samym skutkiem, które byłyby bezpieczne?

na przykład.

while(true)
{
    doSomework();
    i++;
    Thread.Sleep(5000);
}

inny to:

while (true)
{
    string[] images = Directory.GetFiles(@"C:\Dir", "*.png");

    foreach (string image in images)
    {
        this.Invoke(() => this.Enabled = true);
        pictureBox1.Image = new Bitmap(image);
        Thread.Sleep(1000);
    }
}

3
Podsumowanie bloga może brzmieć „nie nadużywaj Thread.sleep ()”.
Martin James,

5
Nie powiedziałbym, że to szkodliwe. Powiedziałbym raczej, że to jest tak, goto:że jest chyba lepsze rozwiązanie twoich problemów niż Sleep.
Domyślnie

9
To nie jest dokładnie to samo goto, co bardziej przypomina zapach kodu niż zapach projektu. Nie ma nic złego w tym, że kompilator wstawia gotos do twojego kodu: komputer się nie myli. Ale Thread.Sleepto nie jest dokładnie to samo; kompilatory nie wstawiają tego wywołania i ma to inne negatywne konsekwencje. Ale tak, ogólne przekonanie, że używanie go jest złe, ponieważ prawie zawsze istnieje lepsze rozwiązanie, jest z pewnością poprawne.
Cody Grey

32
Wszyscy opiniują, dlaczego powyższe przykłady są złe, ale nikt nie dostarczył przepisanej wersji, która nie używa Thread.Sleep (), która nadal realizuje cel podanych przykładów.
StingyJack

3
Za każdym razem, gdy wprowadzasz sleep()kod (lub testy), szczeniak umiera
Reza S

Odpowiedzi:


163

Problemy z wywołaniem Thread.Sleepwyjaśnione dość zwięźle tutaj :

Thread.Sleepma swoje zastosowanie: symulowanie długich operacji podczas testowania / debugowania w wątku MTA. W .NET nie ma innego powodu, aby go używać.

Thread.Sleep(n)oznacza blokowanie bieżącego wątku na co najmniej tyle razy, ile razy (lub ilość wątków) może wystąpić w ciągu n milisekund. Długość odcinka czasu jest różna w różnych wersjach / typach systemu Windows i różnych procesorach i zwykle wynosi od 15 do 30 milisekund. Oznacza to, że wątek prawie na pewno będzie blokował się przez ponad nmilisekundy. Prawdopodobieństwo, że Twój wątek przebudzi się ponownie dokładnie po upływie nmilisekund, jest prawie tak niemożliwe, jak niemożliwe. Nie Thread.Sleepma więc sensu mierzyć czasu .

Wątki to ograniczony zasób, ich utworzenie zajmuje około 200 000 cykli, a zniszczenie około 100 000 cykli. Domyślnie rezerwują 1 megabajt pamięci wirtualnej na swój stos i używają 2000-8000 cykli dla każdego przełącznika kontekstu. To sprawia, że ​​każdy czekający wątek jest ogromnym marnotrawstwem.

Preferowane rozwiązanie: WaitHandles

Najczęściej popełnianym błędem jest użycie Thread.Sleepz konstrukcją while ( demo i odpowiedź , fajny wpis na blogu )

EDYCJA:
Chciałbym poprawić swoją odpowiedź:

Mamy 2 różne przypadki użycia:

  1. Czekamy ponieważ wiemy konkretny żądany przedział czasu, kiedy powinniśmy kontynuować (użycie Thread.Sleep, System.Threading.Timerlub alikes)

  2. Czekamy, ponieważ jakiś warunek zmienia się z czasem ... słowo kluczowe to / są jakiś czas ! jeśli sprawdzanie warunku znajduje się w naszej domenie kodowej, powinniśmy użyć WaitHandles - w przeciwnym razie komponent zewnętrzny powinien zapewniać jakieś zaczepy ... jeśli nie, to jego projekt jest zły!

Moja odpowiedź dotyczy głównie przypadku użycia 2


30
Nie nazwałbym 1 MB pamięci ogromnym marnotrawstwem, biorąc pod uwagę dzisiejszy sprzęt
Domyślnie

14
@Domyślnie hej, przedyskutuj to z oryginalnym autorem :) i zawsze zależy to od twojego kodu - lub lepiej: czynnik ... a głównym problemem jest „Wątki są ograniczonym zasobem” - dzisiejsze dzieci niewiele wiedzą o skuteczności działania i kosztów niektórych implementacjach, ponieważ „sprzęt jest tani” ... ale czasami trzeba kodu bardzo optd
Andreas Niedermair

11
„To sprawia, że ​​każdy wątek oczekiwania jest ogromną stratą”, co? Jeśli jakaś specyfikacja protokołu wymaga jednosekundowej przerwy przed kontynuowaniem, co będzie czekać 1 sekundę? Jakiś wątek gdzieś będzie musiał poczekać! Narzut związany z tworzeniem / niszczeniem wątku jest często nieistotny, ponieważ wątek i tak musi zostać podniesiony z innych powodów i działa przez cały czas trwania procesu. Chciałbym zobaczyć jakikolwiek sposób na uniknięcie przełączania kontekstu, gdy specyfikacja mówi „po włączeniu pompy, poczekaj co najmniej dziesięć sekund, aż ciśnienie się ustabilizuje, zanim otworzysz zawór zasilający”.
Martin James,

9
@CodyGray - ponownie przeczytałem post. W moich komentarzach nie widzę żadnej ryby w jakimkolwiek kolorze. Andreas wyszedł z sieci: „Thread.Sleep ma swoje zastosowanie: symulowanie długich operacji podczas testowania / debugowania wątku MTA. W .NET nie ma innego powodu, aby go używać ”. Twierdzę, że istnieje wiele aplikacji, w których wywołanie sleep () jest właśnie tym, co jest wymagane. Jeśli leigoni deweloperów (bo jest ich wiele), nalegają na używanie pętli sleep () jako monitorów stanu, które powinny być zastąpione zdarzeniami / condvars / semas / cokolwiek, to nie jest uzasadnieniem dla stwierdzenia, że ​​`` nie ma innego powodu, aby go używać ”.
Martin James,

8
W ciągu 30 lat tworzenia aplikacji wielowątkowych (głównie C ++ / Delphi / Windows) nigdy nie widziałem potrzeby pętli uśpienia (0) lub uśpienia (1) w żadnym dostarczanym kodzie. Czasami umieszczałem taki kod w celu debugowania, ale nigdy nie trafił do klienta. „jeśli piszesz coś, co nie kontroluje całkowicie każdego wątku” - mikro-zarządzanie wątkami jest równie dużym błędem, jak mikro-zarządzanie personelem programistycznym. Zarządzanie wątkami jest tym, do czego służy system operacyjny - należy używać narzędzi, które zapewnia.
Martin James,

34

SCENARIUSZ 1 - oczekiwanie na zakończenie zadania asynchronicznego: Zgadzam się, że WaitHandle / Auto | ManualResetEvent powinno być używane w scenariuszu, w którym wątek oczekuje na zakończenie zadania w innym wątku.

SCENARIUSZ 2 - pętla czasowa while: Jednak jako prymitywny mechanizm czasowy (while + Thread.Sleep) jest całkowicie w porządku dla 99% aplikacji, które NIE wymagają dokładnej wiedzy , kiedy zablokowany wątek powinien się "obudzić *. 200k cykli do utworzenia wątku jest również nieprawidłowe - wątek pętli czasowej i tak musi zostać utworzony, a 200k cykli to kolejna duża liczba (powiedz mi, ile cykli otworzyć wywołania pliku / gniazda / db?).

Więc jeśli while + Thread.Sleep działa, po co komplikować sprawy? Tylko prawnicy składni byliby praktyczni !


Na szczęście mamy teraz TaskCompletionSource do efektywnego oczekiwania na wynik.
Austin Salgat

14

Chciałbym odpowiedzieć na to pytanie z perspektywy polityki kodowania, co może być pomocne lub nie dla nikogo. Ale szczególnie, gdy masz do czynienia z narzędziami przeznaczonymi dla programistów korporacyjnych od 9:00 do 17:00, osoby piszące dokumentację mają tendencję do używania takich słów, jak „nie powinien” i „nigdy” nie znaczy, nie rób tego, chyba że naprawdę wiesz, czego robisz i dlaczego ”.

Kilka innych moich ulubionych w świecie C # to to, że mówią ci, aby „nigdy nie wywoływać blokady (this)” lub „nigdy nie wywoływać GC.Collect ()”. Te dwa są silnie deklarowane na wielu blogach i oficjalnej dokumentacji, a IMO to kompletna dezinformacja. Na pewnym poziomie ta dezinformacja służy swojemu celowi, ponieważ powstrzymuje początkujących od robienia rzeczy, których nie rozumieją, zanim w pełni zbadają alternatywne rozwiązania, ale jednocześnie utrudnia znalezienie PRAWDZIWYCH informacji za pomocą wyszukiwarek, które to wszystko wydają się wskazywać na artykuły, w których mówi się, że nie należy robić czegoś, a nie dają odpowiedzi na pytanie „dlaczego nie?”

Politycznie sprowadza się to do tego, co ludzie uważają za „dobry projekt” lub „zły projekt”. Oficjalna dokumentacja nie powinna dyktować projektu mojej aplikacji. Jeśli naprawdę istnieje techniczny powód, dla którego nie powinieneś wywoływać funkcji sleep (), wówczas dokumentacja IMO powinna zawierać stwierdzenie, że można to wywołać w określonych scenariuszach, ale może zaoferować alternatywne rozwiązania, które są niezależne od scenariusza lub bardziej odpowiednie dla drugiego scenariusze.

Wyraźne wywołanie „sleep ()” jest przydatne w wielu sytuacjach, gdy terminy są jasno określone w kategoriach czasu rzeczywistego, jednak istnieją bardziej wyrafinowane systemy czekania i sygnalizowania wątków, które należy rozważyć i zrozumieć, zanim zaczniesz rzucać w sen ( ) do kodu, a wrzucanie do kodu niepotrzebnych instrukcji sleep () jest ogólnie uważane za taktykę początkujących.


5

Jest to 1) .spinning i 2) .polling loop twoich przykładów, przed którymi ludzie ostrzegają , a nie część Thread.Sleep (). Myślę, że Thread.Sleep () jest zwykle dodawany w celu łatwego ulepszenia kodu, który wiruje lub jest w pętli odpytywania, więc jest po prostu powiązany z „złym” kodem.

Ponadto ludzie robią takie rzeczy jak:

while(inWait)Thread.Sleep(5000); 

gdzie zmienna inWait nie jest dostępna w sposób bezpieczny dla wątków, co również powoduje problemy.

Programiści chcą zobaczyć wątki kontrolowane przez konstrukcje zdarzeń i sygnalizacji i blokowania, a kiedy to zrobisz, nie będziesz potrzebować Thread.Sleep (), a obawy dotyczące dostępu do zmiennych bezpiecznych dla wątków również zostaną wyeliminowane. Na przykład, czy możesz utworzyć procedurę obsługi zdarzeń skojarzoną z klasą FileSystemWatcher i użyć zdarzenia do wyzwolenia drugiego przykładu zamiast zapętlania?

Jak wspomniał Andreas N., przeczytaj artykuł Wątki w C # autorstwa Joe Albahari , jest naprawdę bardzo dobry.


Więc jaka jest alternatywa dla robienia tego, co jest w twoim przykładzie kodu?
shinzou

kuhaku - Tak, dobre pytanie. Oczywiście Thread.Sleep () to skrót, a czasem duży skrót. W powyższym przykładzie reszta kodu w funkcji poniżej pętli odpytywania „while (inWait) Thread.Sleep (5000);” jest nową funkcją. Ta nowa funkcja jest delegatem (funkcją zwrotną) i przekazujesz ją do wszystkiego, co ustawia flagę „inWait”, a zamiast zmieniać flagę „inWait”, wywoływane jest wywołanie zwrotne. To był najkrótszy przykład, jaki udało mi się znaleźć: myelin.co.nz/notes/callbacks/cs-delegates.html
mike

Żeby się upewnić, że mam, masz zamiar zawinąć Thread.Sleep()inną funkcję i wywołać ją w pętli while?
shinzou

5

Uśpienie jest używane w przypadkach, gdy niezależne programy, nad którymi nie masz kontroli, mogą czasami korzystać z powszechnie używanego zasobu (na przykład pliku), do którego program musi uzyskać dostęp, gdy działa, i gdy zasób jest przez nie używany inne programy Twój program nie może ich używać. W tym przypadku, gdy uzyskujesz dostęp do zasobu w swoim kodzie, umieszczasz swój dostęp do zasobu w try-catch (aby złapać wyjątek, gdy nie możesz uzyskać dostępu do zasobu) i umieszczasz to w pętli while. Jeśli zasób jest wolny, sen nigdy nie zostanie wywołany. Ale jeśli zasób jest zablokowany, śpisz przez odpowiedni czas i próbujesz ponownie uzyskać dostęp do zasobu (dlatego zapętlasz). Pamiętaj jednak, że musisz umieścić jakiś ogranicznik na pętli, więc nie jest to potencjalnie nieskończona pętla.


3

Mam przypadek użycia, którego tutaj nie do końca nie widzę, i będę argumentować, że jest to ważny powód, aby użyć Thread.Sleep ():

W aplikacji konsolowej wykonującej zadania czyszczenia muszę wykonać dużą liczbę dość kosztownych wywołań bazy danych do bazy danych współdzielonej przez tysiące jednoczesnych użytkowników. Aby nie uderzać DB i nie wykluczać innych godzinami, potrzebuję przerwy między połączeniami, rzędu 100 ms. Nie jest to związane z synchronizacją, a jedynie z udzielaniem dostępu do bazy danych innym wątkom.

Poświęcenie 2000-8000 cykli na przełączanie kontekstu między wywołaniami, których wykonanie może zająć 500 ms, jest nieszkodliwe, podobnie jak posiadanie 1 MB stosu na wątek, który działa jako pojedyncza instancja na serwerze.


Tak, używanie Thread.Sleepw jednowątkowej, jednofunkcyjnej aplikacji konsoli, takiej jak ta, którą opisujesz, jest całkowicie OK.
Theodor Zoulias

-7

Zgadzam się z wieloma tutaj, ale myślę, że to zależy.

Ostatnio zrobiłem ten kod:

private void animate(FlowLayoutPanel element, int start, int end)
{
    bool asc = end > start;
    element.Show();
    while (start != end) {
        start += asc ? 1 : -1;
        element.Height = start;
        Thread.Sleep(1);
    }
    if (!asc)
    {
        element.Hide();
    }
    element.Focus();
}

To była prosta funkcja animacji i użyłem Thread.Sleepna niej.

Mój wniosek, jeśli spełnia swoje zadanie, użyj go.


-8

Dla tych z Was, którzy nie widzieli jednego ważnego argumentu przeciwko używaniu Thread.Sleep w SCENARIUSZU 2, naprawdę jest jeden - wyjście aplikacji jest wstrzymywane przez pętlę while (SCENARIUSZ 1/3 jest po prostu głupi, więc nie jest wart więcej wspominając)

Wielu, którzy udają, że wiedzą, krzyczą Thread.Sleep is evil nie wymieniło ani jednego ważnego powodu dla tych z nas, którzy domagali się praktycznego powodu, aby go nie używać - ale oto on, dzięki Pete'owi - Thread. jest zły (można go łatwo uniknąć za pomocą timera / handlera)

    static void Main(string[] args)
    {
        Thread t = new Thread(new ThreadStart(ThreadFunc));
        t.Start();

        Console.WriteLine("Hit any key to exit.");
        Console.ReadLine();

        Console.WriteLine("App exiting");
        return;
    }

    static void ThreadFunc()
    {
        int i=0;
        try
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.ThreadState.ToString() + " " + i);

                Thread.Sleep(1000 * 10);
                i++;
            }
        }
        finally
        {
            Console.WriteLine("Exiting while loop");
        }
        return;
    }

9
- nie, Thread.Sleepnie jest tego powodem (jest to nowy wątek i ciągła pętla while)! możesz po prostu usunąć Thread.Sleep-line - et voila: program również nie wyjdzie ...
Andreas Niedermair,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.