Šta je novo?

Kylix & multiprocessing/multithreading

silverglider

Administrator
Administrator
Učlanjen(a)
30.07.2000
Poruke
5,577
Poena
770
Teme sam se posredno dotakao u threadu o CppBuilderu za linux, pa bolje da se tema prenese na novi thread, nego da se nastavi na "civilno" pitanje :D
Elem, iako je ovo namenjeno pocetnicima u kylixu, ne zelim da objasnjavam sta je multiprocessing ili multithreading (ako vam je vec zatrebalo, onda znate sta je to), nego samo da skrenem paznju na par manjih zamki.

Najvecu zamku predstavlja X server. Dakle, mnoge od ovih stvari se ne bi desile ukoliko se radi o nekoj konzolnoj aplikaciji (recimo daemonu), ali za desktop ...


1. multithreading
Vrlo je diskutabilno koje komponente/klase su thread-safe, a koje ne (TTimer, na primer, to definitivno nije). TThread klasa ima isti interfejs kao i pandan u Delphiju, ali metod Synchronize() jednostavno ne zavrsava uvek posao. Kada se poveca broj komponenti, kao da se refresh ne odvija dovoljno brzo/sigurno i aplikacija puca uz poruku X servera da "nije mogao da sinhronizuje ...". Bilo koji linux programer ce zajedljivo primetiti da vam to nije windows, te da ne koristite multithreading, nego multiprocessing. Medjutim, resenje moze biti tu negde izmedju. Uzmimo kao primer taj kilavi TTimer, te da treba voditi racuna o nekoliko razlicitih eventova (na 100ms, na 500ms i na 1000ms) u relativno zahtevnijem programu. U tom slucaju, mozda je najbolje TTimer skroz izdvojiti iz glavnog programa, u sasvim poseban izvrsni fajl, koji nema "u sebi" multithreading. Takav programcic se napravi kao konzolni, bez ikakvog GUI-ja i jedini zadatak mu je da sadrzi timer, te da na definisani interval (recimo 100ms) "tikne", tj saopsti glavnom progamu da je otkucao jedan tick. Kako ce ta dva procesa komunicirati, zavisi od vaseg umea u IPC-u; bilo preko sherovanog dela memorije, bilo preko signala, bilo preko socketa, ... vas izbor. Recimo da ste se opredelili za socket komunikaciju, posto vam semafori nisu jaca strana. U tom slucaju, mali "ticker" treba da sadrzi samo TTimer i neki UDP klijent objekat, tako da na definisani interval posalje telegram (record definisan prema vasim potrebama) na odredjeni port. I to je sve sta se njega tice.
Glavni program treba da sadrzi klasu koja potice od TThreada, te sadrzi UDPserver klasu (recimo Indy paket), koja ima mogucnost da "osluskuje" na doticnom portu. Ukoliko je telegram malo komplexniji, nije lose da se polja mapiraju na propertyje tog threada. Nadalje, treba vam jedna globalna (da mogu svi threadovi da joj pristupaju) varijabla tipa TSimpleEvent
koja ce obelezavati kada je telegram stigao (nazovimo je GUIevent). Treba definisati OnUDPread event-handler tog UDPservera, gde se primi stream preko socketa, izdvoji telegram i raspodeli polja, te okine taj GUIevent (sa GUIevent.SetEvent). E sad, da ne bi bilo sve tako lepo i jednostavno, treba vam jos jedan thread, koji ce sve to da sinhronizuje. Dakle, spoljni proces hvata tajmer, salje telegram glavnom programu. Glavni program ima thread u kojem cuci UDPserver i hvata te telegrame, te okida event "telegramStigao". Zasto praviti sad jos jedan dodatni, "singhronizacioni" thread ? Zbog nekoliko razloga; jedan ozbiljniji program koji bi trebao da radi "glatko", treba da reaguje odmah. Prilikom primanja streama sa socketa, moguci su razni oblici obrade tog telegrama. Ostatak programa mozda ne moze da ceka da se to zavrsi, nego te stvari mogu paralelno da se izvrsavaju; dakle, primi telegram i signaliziraj "tick". Dok se vrsi analiza telegrama (to se vec nalazi u posebnom threadu), neka se paralelno izvrsava sinhronizacija aktivnosti s obzirom na pristigli tajmer. Da bi to stvarno bilo paralelno, treba nam jos jedan thread (paznja, to su sve male i jednostavne klase, treba ih samo pazljivo uvezati u "mrezu"). Drugi veliki razlog koriscenja dodatnog sync threda je u cpu overheadu. Dakle, moze i bez njega, ali cete verovatno imati zauzece procesora 80-90% iako program stoji idle. Dakle, sinhronizaciju povlaci samo kada je potrebno - a upravo to radi ovaj sync thread. On samo ceka da GUIevent bude okinut i nista vise; npr:

[code:1]
procedure TsyncThread.Execute;
begin
while not terminated do
if GUIevent.WaitFor($ffffffff) = wrSignaled then
begin
Synchronize(Main.OnSimulTimer);
GUIevent.ResetEvent;
end;
end;
[/code:1]

OnSimulTimer je procedura koja radi ono sto bi inace radio OnTimer event-handler, da ste kojim slucajem ostavili TTimer u glavnom programu, npr, proverava config fajl, updatuje label koji sadrzi aktuelno vreme, itd. Posto se salju uvek kratki telegrami iste strukture u istim vremenskim intervalima, budite sigurni da je deltaT uvek isto. Ukoliko treba da se odreaguje na razlicite tajminge, onda moze da se postavi neki flag tipa SyncCounter, pa se kod citanja telegrama flag inkrementuje, pa tek ona okine GUIevent. A ovde u sinhro-threadu proverava da li je svaki peti event ili sta vam vec treba (if (SyncCounter mod 5)=0 then Synchronize(Main.SvakiPeti); )
 
2. multiprocessing

Posto sam najavio da se problem moze resiti kombinacijom MT i MP, da vidimo gde se MP tu uplice u pricu. Njegova uloga dolazi na videlo u kontroli onog malog ticker programa. Treba se osigurati da je programcic startovan kada se pokrene glavni program, te da se gasi kada prekine izvrsavanje glavnog programa. Startovati ga mozete, doduse, na staru (seljacku) dos/windows foru (okini i boli te dupe vise za njega - execute), ali, kao pravi programeri, zelimo da taj programcic i gasimo;
1. jer ne treba da ostavljamo djubre po memoriji
2. jer nas program dolazi u opasnost ako ima dva (ili vise) aktivna tickera u memoriji - program ce reagovati onoliko puta koliko dobije telegram.

Dakle, treba da zapamtimo PID (process id) tog programcica, da bi mogli preko njega da ga gasimo. Zato je najbolje kreirati poseban proces i tu ga startovati. Za kreiranje novog procesa (od postojeceg) se obicno koristi fork ili clone (ima jos podvarijanti, pominjem samo najvaznije). Recimo da koristimo fork. Nakon poziva ove funkcije (nije objekat) sve sta se poziva se odvija u posebnom prostoru, kao novi proces. Medjutim, za novi proces su iskopirani i svi atributi postojeceg procesa (glavnog programa). Ovo opet nije problem ukoliko se radi o konzolnoj aplikaciji, ali posto se radi o desktop programu koji radi pod Xom, X ce se pobuniti (i neretko proizvesti lepe i zanimljive efekte :D). Zato se ti atributi moraju "ocistiti". To izgleda otprilike ovako (iz glavnog programa):

[code:1]
procedure StartSyncTicker;
var Max, I: Integer;
begin
Main.TickerPID := fork;
if Main.TickerPID = 0 then
begin
// zatvaranje svih "nasledjenih" fajl deskriptora
Max := sysconf(_SC_OPEN_MAX);
for i := (STDERR_FILENO+1) to Max do
fcntl(i, F_SETFD, FD_CLOEXEC);

// startuj ticker
if libc.execv( PChar(ExtractFilePath(ParamStr(0))+'ticker'), nil) = -1 then
ShowMessage(sTickerError);
end;
end;
[/code:1]

Napomene:
- prema primeru, 'ticker' je naziv programcica i nalazi se u istom diru kao i glavni program
- sTickerError je string koji sadrzi tekst greske
- kada se pozove fork, najbolje da se PID sacuva kao globalna varijabla (ovde Main.TickerPID). Ovo stoga sto se u trenutku okidanja forka kreira novi prostor za proces i sa stanovista tog novog procesa (sve iza forka), PID je NULA. Sa stanovista glavnog programa, PID tog podprocesa je ono sto bi vam linux prijavio (870, 1254, itd). Ako je PID nula, znaci da se korektno kreirao i onda mozemo da startujemo ticker.

Kod izlaska iz programa mozete da "ubijete" programcic naredbom kill(Main.TickerPID, SIGTERM). Ovo je moglo i malo drugacije da se uradi, bez snimanja PIDa. Generalno, moze prilikom gasenja da se sazna PID cele grupe procesa (dakle, vaseg glavnog programa i svih podprocesa koje je on kreirao) i da se onda sve zajedno ubije kao grupa:

[code:1]
...
PGroup_PID := getpgrp;
kill(PGroup_PID, SIGTERM);
...
[/code:1]

No ovo je prilicno brutalno i morate biti sigurni da ste pre ovog poziva uradili cleanup svog memorijskog prostora koji ste rezervisali za svoje objekte, pointere, itd.

No, neko ce primetiti da se ticker nece ocistiti iz memorije ukoliko program krahira ili ga neko ubije na nelegalan nacin (dakle, ne na quit/exit). Zato je najbolje napraviti kratku skripticu kojom se startuje program - dakle, ne pozivati program direktno:

[code:1]
#!/bin/bash
cd /opt/mojprogram/
/opt/mojprogram/glavni
killall ticker
[/code:1]

Skripta nije, naravno, vrhunsko umece, ali je ovde samo kao ilustracija. Dakle, posto je shell roditeljski proces vaseg glavnog procesa, cak i ako glavni krahira, killall ce se izvrsiti u svakom slucaju.
 
3. komentar2

Sad ovo se tice konkretno jave, medjutim mislim da je isa situacija kog svakog jezika koji koristi automatski GC. Naime ubijanje Threada je jako *****ata stvar i zbog toga je metod thread.stop(), koji je momentalno terminisao thread, deprecated u javi. To je zato sto kada mi ubijemo neki thread ne znamo da li je on u tom trenutku napravio neke izmene na objektu na kom je recimo trebao nesto da radi, pa zbog togamoze doci do raznoraznih exceptiona(meni se recimo desilo da kad sam na par mesta koristio .stop() u nekom chat appletu da su se desavale neverovatne stvari, posle pola sata, sat , uopste nemogu da odredim neku pravilnost, javljale su se BrokenPipe greske i server bi padao). Tako da uvek se treba koristiti nekim interupt metodom koji ce osigurati da je thread oslobodio sve resurse pre nogo sto je terminisan
Eto...
 
1. pitanje

Eto kad si se dotako moje omiljene teme imam za tebe jedno pitanje. Uzmi sledeci primer: treba ti aplikacija koja ce korisnika da obavestava da je mu je stigao novi mail. Znaci imamo bazu, u njoj flag koji je ako je postoji neprocitani mail setovan na true, a ako nije na false i tebi server treba da pita bazu da li je flag true, ako jeste da obavesti korisnika. Caka je u sledecem:
1. bez timera, znaci necu da se taj proces startuje svakih 10, 15 115 ili sta ti ja znam koliko sekundi vec da se korisnik odmah obavesti;
2. my favorite part: znaci ako nema timera, onda taj proces treba stalno da pita bazu sto uzrokuje veliki traffic ako imamo veliki broj korisnika, e necu ni tako! Treba da bude na neki nacin da se uspostavi komunikacija izmedju servera i klijenta tako da klijent osluskuje na odredjenom portu, a server da odgovara SAMO kad se nesto promeni u bazi. Znaci na neki nacin server treba da zna da je u bazi doslo do promene bez upitkivanja svaki cas.

Ovo sto pitam je moj prvi zadatak za koji sam trebo da budem placen:( , a nisam znao da ga uradim tada, a iskreno nisam siguran da bi i sada znao(mada nisam nesto ni razmisljo o tome, ali si me podsetio ovim primerom).
Eto...

PS. super ti je ovo oko threadova
 
2. komentar

Evo da ja samo nesto dodam sto se tice generalno problema sa threadovima sto bi na prvom mestu trebalo da pomogne neiskusnijim programerima, mada svako moze da naleti na isti. Stvar se zove deadlock. Uopste deadlock oznacava situaciju kada se thread (ili ti popularna "nit" kako je zovu nasi prevodioci u knjigama i uvek se zbunim kad naletim na termin) blokira zato sto ocekuje odredjeni uslov da se ispuni, a nesto drugo u aplikaciji cini nemogucim taj uslova da se ispuni. Na primer recimo da imamo thread koji hoce da se koristi sa dve "brave" (a pricam nesto za prevodioce:)) koje su enkapsulirane u dva objekta Koka i Zeka. Prvo dolazi do one u objektu Koka, a onda pokusava da dodje do one u objektu Zeka. Recimo isto da imoamo jos jedan thread koji drzi bravu na objektu Zeka. E sad, problem je u tome sta ako ovaj drugi thread treba da koristi bravu u objektu Koka koju vec drzi prvi thread, a on ne moze da je oslobodi sve dok ne dodje do one u objektu Zeka koju vec drzi drugi thread. To je deadlock.
primer:

[code:1]
public class Deadlock implements Runnable{
public static void main(String args[]){
Object Koka = "Resurs 1";
Object Zeka = "Resurs 2";
Thread t1 = new Thread(new Deadlock(Koka, Zeka));
Thread t2 = new Thread(new Deadlock(Koka, Zeka));
t1.start();
t2.start();
}

private Object prviResursi;
private Object drugiResursi;

public Deadlock(Object prvi, Object drugi){
prviResursi = prvi;
drugiResursi = drugi;
}

for(;;){
System.out.println(Thread.currentThread().getName() + "Trazi bravu na " +prviResurs);
synchronized(prviResurs){
System.out.println(Thread.currentThread().getName() + " zakljucao bravu na " +prviResurs);
}
System.out.println(Thread.currentThread().getName() + "Trazi bravu na " +drugiResurs);
synchronized(drugiResurs){
System.out.println(Thread.currentThread().getName() + " zakljucao bravu na " +drugiResurs);
try{Thread.sleep(100);}
catch(InteruptedException ie){}
}

}
}

[/code:1]

ako neko nije lenj pa kopajluje videce rezultat iz koga moze da se zakljuci da Thread -0 drzi bravu na prvom i pokusava da dodje do bravena drugom objektu. Za to vreme Thread-1 drzi bravu na drugom i pokusava da dodje do brave na prvom objektu, sto sprecava prvi thread da se izvrsi, a takodje ni on se ne izvrsava zato sto nemoze da dodje do prve brave. Resenje je naizgled prosto. Ako bi threadovi trazili bravi istim redom do ovoga ne bi ni doslo. Medjutim ovo je veliki problem zog toga sto threadovi koji su u deadlocku mogu biti na totalno razlicitim mestima u programu pa je sam problem tesko indentifikovati.
Eto...
 
Re: 1. pitanje

range je napisao(la):
Eto kad si se dotako moje omiljene teme imam za tebe jedno pitanje. Uzmi sledeci primer: treba ti aplikacija koja ce korisnika da obavestava da je mu je stigao novi mail. Znaci imamo bazu, u njoj flag koji je ako je postoji neprocitani mail setovan na true, a ako nije na false i tebi server treba da pita bazu da li je flag true, ako jeste da obavesti korisnika. Caka je u sledecem:
1. bez timera, znaci necu da se taj proces startuje svakih 10, 15 115 ili sta ti ja znam koliko sekundi vec da se korisnik odmah obavesti;
2. my favorite part: znaci ako nema timera, onda taj proces treba stalno da pita bazu sto uzrokuje veliki traffic ako imamo veliki broj korisnika, e necu ni tako! Treba da bude na neki nacin da se uspostavi komunikacija izmedju servera i klijenta tako da klijent osluskuje na odredjenom portu, a server da odgovara SAMO kad se nesto promeni u bazi. Znaci na neki nacin server treba da zna da je u bazi doslo do promene bez upitkivanja svaki cas.

Ovo sto pitam je moj prvi zadatak za koji sam trebo da budem placen:( , a nisam znao da ga uradim tada, a iskreno nisam siguran da bi i sada znao(mada nisam nesto ni razmisljo o tome, ali si me podsetio ovim primerom).
Eto...

Pa to se ni ne radi sa tajmerom ili periodicnom proverom - ukoliko bi se interval stavio na veci period, reakcija ne bi bila pravovremena; ukoliko bi stavio kraci period, to bi radilo poprilican CPU overhead (a to niko ne voli :))

Problem se resava DB eventingom. Svaka modernija baza podrzava eventing (sem valjda shugavog MySQLa). Posto izgleda koristis samo Javu, a ja sa JDBC i nisam bas jaci, evo generalnog hinta (u dve varijante):

1. uzmes lepo klasu koja moze da se predstavi kao table i povezes je sa tabelom u bazi. Ili uzmes neku query klasu koja moze da primi SQL sekvencu (makar SELECT) i dovlaci set recorda iz tabele. Uglavnom, da imas live result iz te tabele. Takve klase/komponente uglavnom imaju definisani event tipa "onDataChange". Prototip event-handlera koji obradjuje ovo obicno salje pointer na field ili record koji se promenio, odnosno null ako ih se vise promenilo - tada ionako moras da prodjes celu tabelu da bi video sta se promenilo (vazno da dobijes signal da je doslo do promene). Sam event handler treba samo da popuni struct (ili listu, ukoliko ima mogucnosti da ih se vise promeni u istom momentu) i okine thread-event. Naravno, pod pretpostavkom da si definisao thread, koji cuci u pozadini i sadrzi samo tcp/ip klijent objekat, na primer, koji (kada je okinut) prolazi kroz tu zajednicku listu i salje notification useru. Ovde je bolje drzati to u threadu, jer se obrada liste i slanje obavestenja odvija paralelno sa glavnim programom i on ne mora da ceka da se lista obradi.

2. manji problem je kada nemas lepu klasu koja ima definisan event tipa "onDataChange". Onda uzmes i napises svoju :D
No, za to je vazno da su ispunjeni neki uslovi:
- da baza podrzava DB eventing (dakle EventManager, triggere, itd)
- da imas API za tu vrstu baze (vecina ih ima) da mozes da pristupas lepo iz nekog viseg jezika

a) prvo SQLom definises lepo trigger koji se okida na neku od akcija (insert, update, ...) nad tabelom gde drzis polje "new_mails", kao npr:
[code:1]
SET TERM !! ;
CREATE TRIGGER trg_new_mail FOR users
AFTER UPDATE AS
BEGIN
post_event "new_mail";
END !! ;
SET TERM ; !!
[/code:1]

b) Iz samog programa (recimo da se radi o C++u) prvo kroz bazni API treba da registrujes sve eventove koje si definisao kroz triggere u bazi (moze i kroz embedded sql); primer sa Interbaseom:
[code:1]
EXEC SQL
EVENT INIT db_responder ("new_mail", "new_sms", [...]);
EXEC SQL
EVENT WAIT db_responder;
[/code:1]

i u programu koristis API event array isc_event[] da proveris koji od njih se okinuo. Koliko si ih registrovao, toliko ti je velik array. Dakle, ako si registovao njih 5, onda :

[code:1]
for (i=0; i<5; i++)
{
if (isc_$event > 0)
{
switch (i)
{
case 1: // "new_mail
...

}
}
}
[/code:1]



PS. super ti je ovo oko threadova


Ma nisam imao nameru da razjasnjavam sta su threadovi, nego da skrenem paznju ljudima koji su programirali za windows i probaju nesto da urade pod linuxom, da se tu stvari malo drugacije odvijaju. I sam sam proveo ne-znam-koliko dana dok nisam resio gorenavedeni problem sa threadovima. Ostale kombinacije su na prvi pogled radile, ali ako ostavis program da radi sat-dva, ubice ga X garantovano.
 
Re: 3. komentar2

range je napisao(la):
Sad ovo se tice konkretno jave, medjutim mislim da je isa situacija kog svakog jezika koji koristi automatski GC. Naime ubijanje Threada je jako *****ata stvar i zbog toga je metod thread.stop(), koji je momentalno terminisao thread, deprecated u javi. To je zato sto kada mi ubijemo neki thread ne znamo da li je on u tom trenutku napravio neke izmene na objektu na kom je recimo trebao nesto da radi, pa zbog togamoze doci do raznoraznih exceptiona(meni se recimo desilo da kad sam na par mesta koristio .stop() u nekom chat appletu da su se desavale neverovatne stvari, posle pola sata, sat , uopste nemogu da odredim neku pravilnost, javljale su se BrokenPipe greske i server bi padao). Tako da uvek se treba koristiti nekim interupt metodom koji ce osigurati da je thread oslobodio sve resurse pre nogo sto je terminisan
Eto...

Phu, gasenje threadova zavisi od vise stvari. U prvom redu od toga kako je klasa za thread definisana u odnosu na API tog OS-a. Dakle, trebala bi da ima minimum opciju da thread bude pauziran i "otpauziran" i da moze da se reaguje na ove dve akcije (tu vec moze da se radi priprema ili ciscenje djubreta) - dakle, ne samo da thread "krene" kada se izvrsi konstruktor i slicno.

Zavisi, zatim, da li su threadovi objekti iste ili razlicitih thread klasa, te da li zavise medjusobno jedni od drugih. Ukoliko se radi o visestrukim objektima jedne te iste thread klase, neka signalizacija moze da se izvrsi ukoliko sam programski jezik podrzava staticke atribute (svi objekti te klase dele medjusobno taj jedan zajednicki atribut), na primer.

Mada je nasigurnije imati jedan dodatni thread-klasu koja sluzi samo da sinhronizuje ove ostale (pogotovo ako su threadovi razliciti objekti i razlicite namene). Nekada su definisani vec razni ThreadPoolovi, ThreadManageri i slicno.

Generalno, vazno je voditi racuna o dve stvari:
- koji thread zavisi od kojeg (ukoliko su medjuzavisni) i gasiti/pauzirati ih u odgovarajucem redosledu
- u svakom threadu uraditi cleanup posla koji thread inace obavlja; dakle, ako radi nesto sa fajlovima, prvo ih pozatvarati; sockete takodje, i slicno. Onda uraditi nullovanje event handlera - kada se otkace te procedure, cak i ako se trigeruje neki event, od tog momenta se nece na njega reagovati. U nekim slucajevima je mozda i nizbezno nakon toga staviti neku Sleep(500) komandu, ukoliko treba sacekati da OS isprazni kes ili tako nesto (zavisi sta thread radi).
 
silverglider je napisao(la):
...
Najvecu zamku predstavlja X server. Dakle, mnoge od ovih stvari se ne bi desile ukoliko se radi o nekoj konzolnoj aplikaciji (recimo daemonu), ali za desktop ...

Zaboravih da ostavim tacnu poruku greske koju X javi oko problema sa threadovima:

"Xlib: unexpected async reply(0xXXXX)"

Gde je 0xXXXX hex adresa (najverovatnije threada iz kojega se pokusava izvrsiti sinhronizacija).
 
Nazad
Vrh Dno