Dlaczego wątki (nie) są trudne [ankieta]

0

Pozwoliłem sobie założyć nowy wątek, żeby offtopa nie robić odpowiadając na post Egona:

Niemniej wątki są naprawdę trudne i zrobienie poprawnego kodu, który robi coś nietrywialnego wymaga dużej wiedzy i ostrożności.

LOL. Watki byc moze sa trudne ale tylko dla osob ktore nie wiedza co robia. Potem wlasnie mamy sleepy w kodzie albo losowo powstawiane sekcje krytyczne.

Używanie wątków + monitorów / sekcji krytycznych prowadzi do wykładniczej eksplozji liczby możliwych ścieżek wykonania programu. Udowodnienie, że żadna z nich nie prowadzi do uszkodzenia danych lub deadlocku jest możliwe tylko w najprostszych sytuacjach (np. dajesz jedną wielką sekccję krytyczna na wszystko). Ale to jest ucieczka przed problemem - sztuką jest zrobić kod tak, aby synchronizować w jak najmniejszej liczbie miejsc, zachowując poprawność. Jeśli obejmiesz za dużo sekcjami krytycznymi, to masz kiepską skalowalność. Jeśli z kolei pozwalasz na duże zrównoleglanie kodu, to analiza tego staje się trudna, a udowodnienie poprawności praktycznie niemożliwe.

Na dodatek błędy związane z wielowątkowością są zwykle bardziej "niedebugowalne" niż błędy związane z ręcznym zarządzaniem pamięcią. Każdy z nas robi błędy. Błąd w programie wielowątkowym może się nie objawiać przez długie lata, a ujawnić dopiero jak obciażenie wzrośnie albo kod zostanie przeniesiony na inny system operacyjny. Na ręczne zarządzanie pamięcią mamy lekarstwa (GC, Valgrind, Electric fence itp.), na wątki - na razie nic równie skutecznego.

// edit: dodałem ankietę, bo temat ogólnie jest ciekawy. Może przenieść do Inżynierii Oprogramowania?

0

ano prawda, dlatego jestem za ciagla integracja na jaknajwiekszej liczbie maszyn naraz

0

Dlaczego wątki nie są trudne?
Może odpowiem tak - dawno temu kolega na studiach powiedział mi, że algorytmy nie są trudne. No cóż... nie zdał tego przedmiotu :P
Jak ktoś uważa, że programowanie wielowątkowe nie jest trudne to nawet nie powinien podchodzić do wątków.
Zwłaszcza pseudospecjalista egonek :P

0

Druga sprawa (dot. trudnosc) to, ze czesto pisane sa aplikacje metoda prob i bledow o ile w wiekszosci przypadkow taka aplikacje da sie utrzymac to w przypadku watkow juz nie za bardzo (ten kto ma tak dobra wyobraznie najczesciej robi projekt)

0

Jednak czasem bez wątków może być trudno, ale nie oznacza, że niemożliwie. Ja jednak jak już potrzebuję wątków, to staram się publiczne mieć immutable, a współdzielone są chronione muteksami i monitorami i całą masą innego szmatłastwa. Ogólnie teraz pracuję nad rozwojem D, gdzie mają to wszystko mocno nastawione na wątki (w szczególności w D2.0)

0
winerfresh napisał(a)

Jednak czasem bez wątków może być trudno, ale nie oznacza, że niemożliwie

nie ze trudno tylko wolno :P

0
endl napisał(a)

Jak ktoś uważa, że programowanie wielowątkowe nie jest trudne to nawet nie powinien podchodzić do wątków.
Zwłaszcza pseudospecjalista egonek :P

A jak ktos uwaza ze cos jest trudne to jest prawdziwym specjalista? Mam kolege ktory ma problemy z czytaniem, czy jest specjalista z literatury?
Poraza twoj tok rozumowania panie pseudointeligencie.
To ze ty czegos nie rozumiesz/nie pojmujesz to nie znaczy ze jest to trudne, jakos tysiace ludzi nie ma z tym problemow tylko ty.
PS. Rozumiem ze topic zalozyla urazona duma kolejnego pacjenta dla ktorego multithreading to wiedza tajemna.

0

To ze ty czegos nie rozumiesz/nie pojmujesz to nie znaczy ze jest to trudne

Z tym się zgadzam. Generalnie, że jakiś nieokreślony ktoś coś rozumie lub nie rozumie jeszcze nie świadczy o tym, że to jest trudne / łatwe. Ale jeśli tym kimś jest gość, który zaprojektował JavaScript, a przy tym jest CTO Mozilli, to już trochę daje do myślenia. Jeżeli goście w Ericssonie zaprojektowali własny język specjalnie do zastosowań współbieżnych i NIE zdecydowali się na wątki, to daje do myślenia trochę więcej...

Jakos tysiace ludzi nie ma z tym problemow tylko ty

Wątpię.

Większość programistów, których spotkałem, ma z tym problemy. Może obracam się wśród kiepskich programistów, ale chyba to nie to, bo firma, w której pracują radzi sobie dobrze i kolejne produkty wypuszcza w miarę na czas i w miarę z przyzwoitą jakością (patrz: link do gagabox w stopce). Nie sądzę, by nasi programiści byli gorsi niż są przeciętnie w firmach. Dodając do tego, że natrafialiśmy parokrotnie na błędy związane z wielowątkowością w otwartych bibliotekach, których używamy (m.in. JBoss), można mieć wrażenie, że wszędzie z wielowątkowowścią jest podobnie, tj. kiepsko.

A, mieliśmy takiego jednego na rozmowie kwalifikacyjnej, co na każde pytanie odpowiadał, że to jest "banalne". We wszystkich zrobił bugi, najtrudniejszego nie ruszył... :D

Zagadka specjalnie dla Egona:


private final int THREAD_COUNT = Runtime.getRuntime().availableProcessors();   
private final CyclicBarrier pauseBarrier = new CyclicBarrier(THREAD_COUNT + 1);
private boolean paused = false;
private boolean terminated = false;

public synchronized void start() {

   for (int i = 0; i < THREAD_COUNT; i++) {
        final int workerNo = i;
	new Thread(new Runnable() {
            @Override
	    public void run() {
	         while (!terminated) {

                    worker[workerNo].doWork();

	    	try {
			if (paused) {
                                // Proceed only when all threads are here, give signal that pause() can leave:
				pauseBarrier.await();                     

                                // Wait for nofityAll() called by resume():
                                synchronized(OuterClass.this) {      
                                     while (paused)
                                          OuterClass.this.wait();   
                                }
			}
		    } catch (InterruptedException e) {
			e.printStackTrace();                   
		    } catch (BrokenBarrierException e) {
			throw new RuntimeException(e);  // should never happen
		    }
	    }	
        }).start();
    }
}

/**  Pauses all thread in safe state, out of doWork(). Call to pause() leaves only AFTER all threads have been stopped. */
public synchronized void pause() {
     if (!paused) {
        paused = true;
        pauseBarrier.await();
    } 
}

/** Resumes all threads */
public synchronized void resume() {
     if (paused) {
         paused = false;
         notifyAll();
     } 
}

Ten kod ma na celu odpalenie tyle wątków, ile jest procesorów.
Wątki wykonują sobie równolegle doWork, które jest bezpieczne ze względu na wątki i nie stosuje żadnej synchronizacji. Metoda pause() ma na celu "zaparkowanie" wątków w bezpiecznym miejscu POZA procedurą doWork() w celu np. modyfikacji danych, na których te wątki operują przez jakiś inny niezależny wątek. Czyli wywołuje sobie pause() i mogę bezpiecznie grzebać w pamięci używanej przez doWork, a jak skończę, to wołam resume() i wszystkie wątki dalej pracują. doWork() wykonuje się b. krótko.

Teraz pytanie: czy ten kod jest dobry? Jeśli nie, to gdzie jest błąd?

0
Krolik napisał(a)

Zagadka specjalnie dla Egona:


private boolean paused = false;
private boolean terminated = false;

while (!terminated) {
if (paused) {

if (!paused) {

if (paused) {

Ty chcesz watkami sterowac za pomoca bool'i? Nie dziwie ci sie ze wydaje ci sie to trudne, skomplikowane i niewdzieczne...

PS. Kto puszcza latawce na polu niech lepiej nie probuje sterowac samolotami odrzutowymi.

0

Jest metoda doWork(), która nie może być synchronizowana z różnych powodów (np. z takiego, że jest częściowo pisana przez szefa działu marketingu oraz takiego, że skoro działa na rozłącznych danych, to nie ma co tam synchronizować). Klient zażyczył sobie jednak, że chce mieć możliwość niewielkiej zmiany tych danych w trakcie pracy całego procesu.

Zadanie: umożliwić podmianę danych w taki sposób, aby niczego nie zepsuć i nie przepisywać całego systemu. BEZ BOOLI [diabel] (sam chciałeś).

PS1. Nieznacznie zmieniłem kod, żeby było widać, że doWork pszczególnych wątków jest wołane na rzecz różnych obiektów. Zakładamy, ze nie dzielą one ze sobą żadnych danych.

PS2. Zmienne warunkowe są podstawowym narzędziem przy używaniu monitorów. Po co byłoby wtedy wait(), notify() i notifyAll()?

0
Krolik napisał(a)

Jest metoda doWork(), która nie może być synchronizowana z różnych powodów (np. z takiego, że jest częściowo pisana przez szefa działu marketingu oraz takiego, że skoro działa na rozłącznych danych, to nie ma co tam synchronizować). Klient zażyczył sobie jednak, że chce mieć możliwość niewielkiej zmiany tych danych w trakcie pracy całego procesu.

To tez ten kod wyglada jakby go pisala przekupka handlujaca kozim serem.
Nie bede ci nic poprawial zadam tylko pare pytan.

  1. dlaczego nigdzie nie jest zapamietany uchwyt na nowy thread?
  2. dlaczego nowe thready nie sa tworzone jako obiekty jakiejs klasy bazowej Thread?
  3. w zwiazu z 2 powyzszymi jak zamierzasz "posprzatac" po threadach jesli cos sie sypnie?

to tak na poczatek... doprawdy nie chce mi sie analizowac tego gowna.

Krolik napisał(a)

BEZ BOOLI [diabel] (sam chciałeś).

No BEZ BOOLI to bedzie imba hax co nie? Przynajmniej dla ciebie...

0
EgonOlsen napisał(a)

Nie bede ci nic poprawial zadam tylko pare pytan.

  1. dlaczego nigdzie nie jest zapamietany uchwyt na nowy thread?
  2. dlaczego nowe thready nie sa tworzone jako obiekty jakiejs klasy bazowej Thread?
  3. w zwiazu z 2 powyzszymi jak zamierzasz "posprzatac" po threadach jesli cos sie sypnie?
  1. A jest gdzieś potrzebny?
  2. Używanie dziedziczenia jak nie jest potrzebne to lamerstwo. Niemniej, w Javie są 2 sposoby tworzenia wątków: przez subclassing klasy Thread (obiektowo) i przez przekazanie Runnable() (funkcyjnie). Ja wolę podejście funkcyjne, bo jest czytelniejsze. Oba są jednak równoważne.
  3. Mój kod się nigdy nie sypie [diabel]. A tak serio, to pewne elementy tego kodu, jak np. obsługa wyjątków, które może rzucić doWork() pominąłem, żeby nie zaciemniać. Załóżmy, że doWork() sprząta po sobie jak się sypnie i łapie wszystkie wyjątki.

PS. A tak w ogóle to co to ma do rzeczy? Jak jesteś taki mądrala to opisz jak byś to lepiej zrobił (nawet bez kodu, podstawowe założenia rozwiązania) albo znajdź błąd w kodzie. Bo na razie unikasz odpowiedzi.

0

Oto wiec moja odpowiedz: g**no - to słowo pasuje do tego kawałka kodu.

0

@up, epitet to wyraz bezradności?

0

Aha. Czyli wątki jednak SĄ trudne. Nawet w przypadku kodu, który w całości mieści się na ekranie.
Strach myśleć, jak Wy programujecie coś większego :D Nie chcę Cię martwić Egon, ale używanie zmiennych warunkowych do kontroli wątków to też podstawy.

0

Jaki egonek jest łatwo przewidywalny. Oczywiście się nie myliłem :]

0
Krolik napisał(a)

Aha. Czyli wątki jednak SĄ trudne.

Dla ciebie tak...

Krolik napisał(a)

Nawet w przypadku kodu, który w całości mieści się na ekranie.

Co chcesz tu poprawiać w tym "kodzie"? Te luźno latające wskaźniki? Czy te booleany? (rozumiem że o eventach, mutexach i semaforach nie słyszałeś). Ten kawałek kodu bardziej nadaje się do dyskusji na temat odporności virtualnej maszyny na crashe i wydajności garbage collectora.

Krolik napisał(a)

Strach myśleć, jak Wy programujecie coś większego :D

Przejrzałeś mnie szybciej niż polaczek...

Krolik napisał(a)

Nie chcę Cię martwić Egon, ale używanie zmiennych warunkowych do kontroli wątków to też podstawy.

Może gdzieś w Mozambiku....

0

Egon, mam propozycję, kod jest prosty - napisz po swojemu. Któraś strona będzie musiała się zamknąć...

0

pause() i mogę bezpiecznie grzebać w pamięci używanej przez doWork

No chyba nie za bardzo możesz bo doWork może być właśnie wykonywane.

0

Niech więc będzie deusie.

Tak wiec mamy napisać program który odpala n wątków, w każdym ta sama metoda. Program ma pozwolić na zatrzymanie któregoś z wątków, wykonanie operacji i ponowne uruchomienie wątku. Czy dobrze zrozumiałem?

  1. Zakładamy że mamy w klasę bazową wątku CAbstractThread z virtualną metodą void Run(void). Metoda Run wykonuje się tylko raz w wątku po czym wątek jest zakończony.

  2. Potrzebujemy sygnalizacji stanu wątku, użyjemy eventów: HANDLE hNotTerminated, hNotSuspended, hNotTerminating, hFinished, hNotRunning

  3. implementujemy ciało Run

void doWorkThread::Run(void)
{
	ResetEvent(hFinished);
	SetEvent(hNotTerminating);
	while(WAIT_OBJECT_0  == WaitForSingleObject(hNotTerminating,0)) // zamiast tego nieszczesnego while(1)
	{
		WaitForSingleObject(hNotSuspended,INFINITE);
		ResetEvent(hNotRunning);
		if(WAIT_OBJECT_0 == WaitForSingleObject(hNotTerminating,0))
		{
			doWork();
		}
		SetEvent(hNotRunning);
	}
	SetEvent(hFinished);
}

// metody sterujace watkiem:

void doWorkThread::Resume() { SetEvent(hNotSuspended); };
void doWorkThread::Suspend() { ResetEvent(hNotSuspended); };
void doWorkThread::WaitPause() { WaitForSingleObject(hNotRunning,INFINITE) } // wywolulemy po Suspend zeby upewnic sie ze doWork sie skonczylo
void doWorkThread::WaitFinish() { WaitForSingleObject(hFinished,INFINITE); } // wywolujemy o void doWorkThread::Terminate zeby sie upewnic ze watek sie zakonczyl
void doWorkThread::Terminate() { ResetEvent(hNotTerminating); Resume(); }

Metody sterujace powinny byc w szablonie CAbstactThread ale podejrzewam ze wtedy moi dyskutanci oszaleliby od tego.

Watek startuje w trybie jako suspended, dlaczego? Dlatego że nie jest dobra praktyką tworzenie wątku i uruchamianie go bez upewnienia się, że wszystko zostało poprawnie zainicjalizowane.

  1. tworzymy n watkow.
doWorkThread *threads[n] = new doWorkThread();
  1. odpalamy sobie watki.
for(int i=0;i<n;i++) threads[i]->Resume();

  1. mozemy zatrzymac dowolny watek
threads[i]->Suspend();
threads[i]->WaitPause(); // upewniamy sie ze watek jest doprawdy zatrzymany a wlasciwie ze doWork nie jest wykonywane
  1. Jako ze metoda doWork jest w tym momencie metoda objektu doWorkThread mozemy sobie zaimplementowac metody dostepowe do danych na ktorych pracuje metoda doWork, wszak wszystko mamy w obiekcie.

Pisałem z głowy tak więc w tym kodzie mogą być jakieś bugi.

Mniej wiecej tym sposobem implementuję wątki od jakiegoś czasu. Zasada jest ogólnie taka sama jak przy TThread czy QThread, z tym że moim zdaniem obu tym klasom brakuje trochę funkcjonalności (np. WaitFor() z timeoutem, Terminate z timeoutem, ForceTerminate itd.).

Poważnie, jeśli ktoś używa do kontroli/synchronizacji multithreadingu booleanow i tym podobnych potworków to maxymalnie może się u mnie ubiegać o posadę stajennego (chociaż o czym on będzie rozmawiał z koniami skoro nie zna się nawet na multithreadingu?).

0

Program ma pozwolić na zatrzymanie któregoś z wątków, wykonanie operacji i ponowne uruchomienie wątku.

Program ma pozwolic na zatrzymanie wszystkich wątków jednocześnie.

0
this napisał(a)

Program ma pozwolić na zatrzymanie któregoś z wątków, wykonanie operacji i ponowne uruchomienie wątku.

Program ma pozwolic na zatrzymanie wszystkich wątków jednocześnie.


HANDLE hThreadSuspended[n];

for(int i=0;i<n;i++)
{
   hThreadSuspended[i] = threads[i]->hNotRunning; 
   threads[i]->Suspend(); 
}

WaitForMultipleObjects(hThreadSuspended,INFINITE);
// tutaj wszystkie watki juz sa suspended... 

poza tym, jesli ten doWork() to jest metoda ktora ma przetwarzac jakies dane na wielu rdzeniach jednoczesnie to prononuje sie zainteresowac http://www.threadingbuildingblocks.org/.

0

Większych różnic między Twoim kodem a moim nie widać. Podobna długość, podobny poziom złożoności. Różnica jest stylistyczna (napisałeś to w nieprzenośnym WinApi - funkcje WaitForSingleObject i SetEvent / ResetEvent są specyficzne TYLKO dla Windows, natomiast synchronize/notify/wait są dużo bardziej ustandaryzowane - np. poza Javą stosowane w POSIX threads, tylko pod innymi nazwami). Do tego użycie dziedziczenia i metod wirtualnych do realizacji wątków jest IMHO brzydkie, ale w WinApi się chyba nie da inaczej.

WaitForSingleObject jest RÓWNOWAŻNE z Javowym:

synchronized (x) {
   if (condition)
       x.wait();
}

WaitForMultipleObjects jest niemal równoważne z CyclicBarrier.await().
Więc nie chrzań, że nie używasz zmiennych warunkowych - te zmienne co zaczynają się na h. Dokładnie to samo co w moim paused i terminated.

rozumiem że o eventach, mutexach i semaforach nie słyszałeś

W Javie nie ma eventów ani mutexów. Semafory są, ale jest to konstrukcja niskopoziomowa. Mutexy i eventy można sobie zrobić, ale są niepotrzebne. Są monitory, bariery cykliczne, pule wątków i inne wyżejpoziomowe konstrukcje. Co nie oznacza, że nie można walnąć głupiego błędu przy ich stosowaniu.

A zresztą, przyczepiłeś się stylu, a błędu dalej nie znalazłeś :P

PS Ja wiem, że ten kod jest błędny. I wiem gdzie jest błąd. Chciałem tylko pokazać, że znalezienie błędu w wielowątkowym, nawet prostym kodzie JEST TRUDNE. Czasem łatwiej go napisać od początku. Używając przy tym innej techniki.

PS2: Poprawna wersja tego kodu w Javie, jest łatwiejsza w analizie niż Twoja (tzn. mniej kodu poświęconego synchronizacji oraz mniej zmiennych warunkowych). Jeszcze nie podaję rozwiązania, żeby nie psuć zabawy innym.

0

Chodziło mi tylko o pokazanie zasady jak należy pracować z threadami, jakich obiektow uzywac do sterowania i synchronizacji. To jest przyklad z winapi, to samo mozna napisac na mutexach, semaforach. Po to właśnie te obiekty zostały wymyślone.

No ale jeżli ty nie widzisz RÓŻNICY miedzy booleanem a handlem na event czy mutex no to ja już raczej zamilknę.

PS. Podtrzymuję to co napisałem wcześniej: g**no - to słowo pasuje do twojego kodu.

0

Jej, ślepy jesteś czy co? Gdzie widzisz w moim kodzie niesynchronizowany dostęp do boolean? W Javie NIE MA MUTEKSÓW i NIE MA EVENTÓW i NIE MA HANDLE'ów :P Są monitory i bariery cykliczne. Kod, w którym zapomniałbym któregoś z synchronized wokół wait lub notify się nie skompiluje.

Zauważ, że to tego kodu musiałeś użyć liczby handlów proporcjonalnej do liczby wątków, w tym 4 rodzaje różnych (hNotTerminating, hNotRunning, hFinished, hNotSuspended). I swoją drogą to zanegowanie trochę zaciemnia sprawę, ale nie będę się czepiał.

Mój kod używa dwóch + jednej bariery cyklicznej.
Więc który kod jest łatwiejszy w analizie i w którym łatwiej strzelić błąd?

BTW: Eventy są niskopoziomowe choćby w tym sensie, że na każdy warunek w programie, którego nie da się wyrazić wprost HANDLEM (np. czy dana kolekcja jest pusta) musisz użyć dodatkowego HANDLE'a powiązanego z tym warunkiem. Czyli mnożysz byty ponad potrzebę. Np. w przypadku monitora można napisać:

synchronized(this) {
   while (collection.isEmpty())
       wait();
   // tu kolekcja na pewno nie jest pusta
 }

Ty do tego kodu musisz dodać dodatkową zmienną na HANDLE i synchronizować ją w odpowiednim miejscu ze stanem kolekcji za pomocą setEvent i resetEvent:

Lock(hCollectionMutex);
WaitForSingleObject(hCollectionNotEmpty);  // pytanie czy WaitForSingleObject zwalnia mutex, domyślam się że nie, więc ten kod jest i tak do bani - trzeba zrobić pewnie coś jeszcze żeby to miało szanse działać...
// tu kolekcja CHYBA nie jest pusta, o ile ktoś dobrze ustawia hCollectionNotEmpty...
Unlock(hCollectionMutex);

Nieczytelne i dużo łatwiej o błąd.

PS. Nadal się od Ciebie nie dowiedziałem, czemu tak nie lubisz booleanów.

0

Ja bym w kodzie Krolika zmienną paused zmodyfikował na volatile. Zmienne trzymane w cache'u procesora są flushowane dopiero po zdjęciu locka więc paused = true nie musi być widoczne dla części wątków (co może skończyć się deadlockiem).
Zgadłem? ;)

0

Tak, zgadłeś. :) W ogólności CyclicBarrier.await nie zwalnia monitora, więc nie ma gwarancji, że zmienne są synchronizowane. Ale to nie jedyny błąd w tym kodzie. Jest jeszcze coś, co nieco trudniej zobaczyć. :)

0

Nie pokazałeś jak wygląda obsługa zmiennej terminate. Zakładam, że jest poprawna?

0

Olać terminate. Chodzi tylko o pause i resume.

0

Jak ktoś użyje pause przed start to sobie zrobi deadlocka. O to chodziło?

1 użytkowników online, w tym zalogowanych: 0, gości: 1