Dispose na třídu s více vlákny   zodpovězená otázka

C#, Threading

Dobrý den,

jak správně volat dispose na třídu, která má v jiných vláknech otevřená TCP spojení?

Díky

Zdraví

Jakub

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To by chtělo trochu rozvést. Jaká třída? jaká vlákna? Jaký problém přesně potřebujete řešit?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dobrý den,

díky za dotaz, zkusím trochu detailněji, snad pomůže :)

Třída si v konstruktoru mimojiné vytvoří 1 nebo více UDPsocketů. A dále dvě vlákna, které se starají čistě o komunikaci.

Hlavní vlákno se stará o zpracování fronty příkazů, které se mají odeslat v tom jiném vlákně a zpracovává příchozí informace z jiného vlákna.

Problém (nejasnost) u mne nastává ve chvíli, kdy chci zavolat metodu dispose. (Teď píšu z hlavy, tak se omluvám za případné nepřesnosti). Pokud bych chtěl v disposu zlikvidovat ty UDPsockety pomocí close, tak ostatní vlákna mohou pokusit číst nebo psát a bude mi to házet výjimky.

Pokud ty vlákna killnu, tak se zase nemusí uzavřít

- join bude čekat, dokud se nedokončí kód, což pokud bude čekat na read bez timeoutu může nastat za velmi dlouho

- abort a interrupt se podle mě také vlákno neukončí hned a na něco čekají

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Proč máš ty UDP sockety v hlavním vlákně? Pokud tomu správně rozumím, tak ty vlákna jen naslouchají na síti a posílají to dál, bylo by tedy lepší kdyby si ty vlákna vytvořily / ukončily vlákna samy ne?

Jinak k tvému problému.. Disposeovat by měl ideálně ten komu to náleží, v tvém případě se ty sockety nacházejí tedy v hlavním vlákně, kde bys to měl disposeovat.. Ty ale musíš nějak dát vědět těm ostatním vláknum, že si "skončil" a ty vlákna tedy nebudou ten socket využívat a skončí, po skončení to můžeš disposenout.. Tedy implementačně to bude nějak takto:

Hlavní vlákno:

CreateSockets(); // vytvoření socketu

while(Running) { 

}

CloseThreads = true; // Volatile 
WaitTillThreadsEnd(); // čekat na ukončení threadu
DisposeSockets();  // dispose socketu

Conneciton vlákna:

while (!CloseThreads) {
  // neco delej.. 
}

Takhle tedy hlavni vlakno povi threadum "ukoncete se" a pocka, nez ty thready skonci, pak disposne sockety. Ale chtelo by to videt kod.. todle je obecny postup.. Nezapomen uvezt ze se jedna o volatile (hodnota muze byt menena z jineho threadu)..

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Pokud tento váš objekt vlastní jak ty UDP sokety i ty pomocná vlákna (tj. tyto věci jsou interní/private implementační záležitostí této třídy) tak, je správně, že by i tato třída měla být zodpovědná za jejich rušení (dispose). Protože ale tato třída řeší nějakou komunikaci - odesílání zpráv ve frontě a přijímaní a zpracovávání příchozích zpráv - bude v důsledku toho platit, že po volání Dispose na tomto objektu:

1) Již nepůjde vložit další zprávu pro odeslání do fronty - příslušná metoda by měla vyhodit výjimku. Volání Dispose tedy musí být voláno až v momentě, kdy již bylo např. voláno Dispose na všech producentech zpráv pro odeslání.

2) V dispose je buď počká na odesílání aktuálně odesílané zprávy nebo bude odesílání aktuální zprávy nějakým způsobem ukončeno, buď násilně (nedefinovaně) nebo lépe v definovaných místech).

3) Již se nebudou přijímat (a zpracovávat další zprávy).

4) Při Dispose se počká na dokončení zpracování aktuálně přijaté zprávy nebo bude zpracování aktuálně přijaté zprávy nějakým způsobem ukončeno, buď násilně (nedefinovaně) nebo lépe v definovaných místech).

Všechny tyto body musí implementace metody dispose zajistit.

U bodů 2 a 4 v případě počkání to lze řešit nastavením nějakého příznaku (Disposing = true) a voláním Join na threadu (nebo voláním Wait na objektu Task). Pomocný tread musí na příznak Disposing mezi zpracováváním jednotlivých zpráv koukat. A samozřejmě je potřeba přístup na příznak Disposing ošetřit pomoci dalšího objektu a konstrukce lock. Klíčové slovo volatile nepoužívejte, protože existují případy, kdy nefunguje, a proto si to může dovolit jen opravdu velmi dobrý expert na "lock-free" multitreading. Lock je bezpečný.

V případě přerušení operace je jediná čistá možnost použít Task a Cancellation pattern tj. na definovaných místech se testuje IsCancellationRequested na příslušném objektu CancellationToken, na jehož source se v hlavním treadu volalo Cancel.

Ostatní metody jako Tread.Abort nebo Tread.Interrupt jsou obsolete.

Po ukončení dané operace pomocné vlákno skončí a již se nebude koukat na frontu nebo používat socket. Metoda Dispose pak může bezpečně ostatní objekty (Tread/Task, Socket) také Disposnout.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Volatile funguje vždy a všude. Musí se ale správně pochopit, volatile nezaručuje atomicitu operací, ale brání určitým optimalizacím. Tedy v běžném případě pokud budu mít:

bool doSomething = false;
if (doSomething) Foo();

Tak i přesto, že se tato hodnota doSomething změní "dříve" v čase na true z pohledu kódu, tak se Foo() ani nikdy nevykoná, jelikož to překladač zoptimalizoval.. proto je tam volatile, které poví, že proměnná doSomething může být změněna v jiném threadu.

lockovat v tomto případně možná ani nebude třeba, protože read/write operace u těchto primitivních typů je atomická (vlastnost sbernice ) a ty pomocné thready tu proměnnou nenastavují, takže pokud by to vypadalo jak jsem popsal výše, může dojít maximálně k tomu, že ten thread vykoná o jednu iteraci navíc, což ale vůbec ničemu nevadí, protože hlavní thread disposne connection až po ukončení pomocných threadu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

O atomicitu nejde, i když C# dle specifikace naopak vždy garantuje, že každé čtení nebo zápis volatile fieldu bude atomické, obecně jde ale o to, že i na volatile read může být proveden reordering.

viz.

http://blog.coverity.com/2014/03/26/reor...

http://ericlippert.com/2011/06/16/atomic...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Nejsem si teď moc jistý, jestli jsme se pochopili.. mně je jasné, co dělá volatile a především, co mi nedělá - defakto mi nezaručuje vůbec nic, spíše je to pro prekladac, kteremu poví "nenene tytyty tady dochazi ke zmene, zadnou optimalizaci nedelej".

V tomto případě ale je úplně jedno, že tam dojde k časově závislé chybě, jelikož dojde k iteraci while() navíc, která ničemu nevadí..

Tedy i přesto že v čase "kodu" budu mít toto takto za sebou:

stop = true;
...
while(!stop) {
...
}

a dojde mi tam k race condition, tak mi to tady nevadí, prostě se vykoná o iteraci navíc a v další se to zařízne.. A jelikož ty další vlákna poté už nejsou nikterak závislé na hlavním, tak se prostě v další iteraci ukončí.. hlavní vlákno tedy Nasetuje KONEC=true a pak jen počká, až se ukončí ty ostatní vlákna a Disposne zdroje. Stále ale mi přijde zvláštní, že vlastníkem connectionu je to hlavní vlákno.. Já bych to a základě toho popisu dal tak, že si každé vlákno vytvoří vlastní socket (případně si o něj poví, pokud tam ma byt nejaky "connection pool") a pak ho i uvolní.

Jak jsem psal hned na začátku, záleží na příkladu, ale zde bych si dovedl představit, že lock nebude potřeba.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Já souhlasím, že v tomto konkrétním případě to bude fungovat i bez locku. Jen jsem psal, že se obecně volatile nedoporučuje používat, právě z toho důvodu, že hodně vývojářů (a to i ti co o sobě tvrdí, že ví jak to funguje) nevědí přesně jak funguje a to pak vede k chybám.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Naprostý souhlas. Je potřeba vědět vždy jak to přesně funguje. Žádná technologie, ani klíčové slovo ho neochrání, pokud nemá znalosti. Je nejsmutnější pokud zná někdo i ten lock() a pak se diví, že mu to někde deadlockuje, ale to už je na jinou diskuzi :-)

Jsem rád, že jsme si nakonec vše vyjasnili a Jakub se snad ozve s podrobnostmi.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dobrý den,

omlouvám se za pozdní reakci a děkuji za vyčerpávající odpověď.

Se všemi body souhlasím a dává mi to takto smysl, jen se ještě zeptám:

Řekněme, že v nějakém z těch vláken, je zavolána metoda udp.client.receive() - předtím jsem již zkontroloval, zda mateřské vlákno není disposing - a ta nemá nastavený timeout, takže může čekat do nekonečna, dokud nebude moci zkontrolovat znovu, zda mateřské vlákno není disposing, zároveň nemůže dojít ani k provedení Joinu na těch threadech, pokud by v průběhu toho client.receive() dolšo k disposování.

Uvažuji takto správně?

Díky J

nahlásit spamnahlásit spam 0 odpovědětodpovědět

nebo se zeptám ještě jinak:

Pokud nastavím tomu udp.client timeout např na 5 sekund, bude to "normální" řešení? V tom smyslu, že při volání dispose na tu třídu se může stát, že to bude trvat 5 sekund?

Díky

J

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Já si stále stojím (možná bohužel) za svým řešením a sice netahat do toho nijak implementaci Dispose, ale proste jednoduse synchronizovat vlakna a po jejich ukonceni zavolat standardni dispose.

Takže budeš muset počkat na Tomáše, až ti to objasní, já tomu moc nechápu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Já nejsem natolik programátorsky zdatný, takže Vašemu řešení nerozumím, tak se zeptám možná hloupě:

Njedřív jen pro kontrolu:

1) Dispose bych podle měl volat na třídu, která jakkoli používá socket?

2) Ten socket bych měl ukončit pomocí metody close atp?

3) Pokud přestávám používat třídu, která má více vláken, měl bych se postarat o to, abych ty vlákna ukončil?

Vaše řešení z Vašeho prvního příspěvku toto asi dělá, ale nerozumím následujícímu:

1) WaitTillThreadsEnd(); jak toto naimplementovat? Jak se dozvím, kdy ty thready skončili? Pomocí Threadstate?

A Ukončí se opravdu ihned po tom, co vyskočí s cyklu while nebo to řeší něco, co já nemohu ovlivnit (ála garbage collector?)

Pokud se ukončí opravdu hned a já se to dozvím pomocí threadstate, pak je to za mne OK :)

2) Viz můj poslední dotaz - jak se vypořádat s tím, že pokud už jsem uvnitř toho cyklu "while (!CloseThreads){}" - a dojde ke změně v hlavním vláknu na "closeThreads = True," tak mohu čekat:

za A: Neomezeně dlouho, pokud není nastaven timeout - to je rozhodně špatně, že ano?

za B: Např 5 sekund, pokud je nastaven timeout 5 sec - toto právě nevím jestli je korektní chování nebo ne...

Na toto se ptám, protože zkrátka neznám běžné konvence a když dělám něco poprvé, tak nechci, aby to byla (naprostá :) ) prasárna.

Na bod 1 se ptám, protože neznám přesně toto chování a než něco naimplementuji, tak bych tomu rád i rozuměl.

Ještě jednou díky za Váš čas a snahu!

nahlásit spamnahlásit spam 0 odpovědětodpovědět

1] Ptáte se k čemu slouží Dispose? K uvolnění unmanaged resourcu dříve, než to udělá Garbage Collector finalizerem. Jinak vzniká Memory leak - tedy zbytečně využitá paměť a další resourcy, přesto že nemusí, že už je nepotřebujete, proto je Disposable interface, aby si programátor mohl říct "už to nechci" a tam to uvolnit.

2) Socket.Close() by měl vše odpojit a uvolnit unmanaged zdroje

3) Měl by je uvolnit ten, kdo je vytvořil,, tedy pokud ta třída používá více vláken (VYTVARI JE), tak by je ona sama mela i ukoncovat - ne ale stylem Abort Interrupt atp, ale proste jim říci "hele skonči" a pak počkat až skončí.

K mému řešení:

1) Thread.Join

https://msdn.microsoft.com/cs-cz/library...

2)Ukončí se ihned. Na socketech jsou ty metody blokující většinou, takže

while (!end) {

var a = udp.neco()

...

11: }

by už na řádce 11 neměl nic dělat se socketem - je bezpečné ho uzavřít.

3) ano nastavit timeout je používané řešení, pokud to timeoutne a není konec programu , nic se neděje v další iteraci to sebere.. Pokud to timeoutne a je konec programu, další iterace threadu nenastane, ukončí se a na to pak zareaguje hlavní vlákno které je na něm .Join()

Todle je podle mého názoru zcela správné řešení, které se používá prostě všude.. Hlavní vlákno kde se setuje zda program má běžet dále a pak workeři, které v iteraci checkuji, zda mají makat dál nebo skončit...

Funguje takto každý server (Který nepoužívá select() single.thread model).

Pokud to nechceš udělat správně a jde ti o to to udělat co nejrychleji, tak vždycky to můžeš "na hulváta" Closenout() z hlavího vlákna a pak být připravený odchytávat tuny ObjectDisposedExceptionu.

Pokud opravdu nevis stale jak to udelat, muzu ti tu napsat přímo kód tak jak bys to měl použít, ale musíš mi nějak poslat jak to nyní máš.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ano, u toho Receive je potřeba použít timeout. Při jeho vypršení vždy otestovat, zda nebyl požadavek na ukončení. Pokud ne tak pokračovat v další iteraci novým voláním Receive.

Při ukončování je pak potřeba nastavit příznak, že se má skončit a volat Join. A ano, v nejhorším případě to bude trvat celou délku timeoutu, proto ho zvolte menší než těch 5s (např. 500ms).

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.
  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback