Životnost vlákna vícevláknových aplikací   zodpovězená otázka

VB.NET, Threading

Začínám se trochu vrtat v používání vícevláknových aplikací. Mám jeden konkrétní případ, kdy jsem si vytvořil třídu, která mi zapouzdřuje určitou funkčnost (konkrétně zvládá animovat přechod dvou obrázků na screenu).

Tuto třídu používám v hlavním těle programu a protože potřebuji, aby tato hlavní část byla k dispozici pro jiné úkoly i v průběhu konkrétní animace, pouštím animační metodu ze své třídy do nového vlákna, schematicky asi takto:

...
private withEvents pracovni as mojetrida
...
pracovni = new mojetrida(zakladni parametry)
...
...
private sub spustAnimaci() handles pracovni.animaceSkoncila

   pracovni.novyObrazek = nextImg

   dim thr as new Threading.Thread(AddressOf pracovni.animace)
   thr.Start()

end sub

Dokonce to hned napoprvé fungovalo. Mám ale problém, co se stane se spuštěným vláknem thr?

Ve třídě mám metodu "animace", která mi (cyklem) v průběhu nastaveného časového intervalu nějak přehodí "novyObrazek" za ten původní. Poté, co cyklus doběhne, před koncem metody vyvolá událost "animaceSkoncila".

Co se bude nyní dít s vláknem thr? Je vázáno na konkrétní v ten okamžik spuštěný proces "pracovni.animace" a spolu s jeho ukončením samo zanikne, nebo se mi v tomto případě budou "někde" hromadit odložená vlákna? Jak se s nimi správně vypořádat? (těch animací po sobě bude celkem dost!)

A při této příležitosti bych se ještě zeptal - má v tomto případě (kdy si animační procedura běží ve svém vlastním vláknu) ještě nějaký smysl do jejího pracovního cyklu zařazovat "Application.DoEvents()"? Omlouvám se, ptám-li se na úplné triviality, ale komu není shůry dáno...

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

Po ukončení dané procedury by se ta vlákna měla sama zrušit. Pokud si zapnete správce úloh a v menu Zobrazit\Vybrat sloupce zaškrtnete sloupec Počet podprocesů (podproces je vlastně vlákno), uvidíte, kolik vláken má aplikace. Já jich mám na testovací aplikaci 13 a když kliknu na tlačítko, tak se na chvíli objeví 14 a pak se to zase vrátí na 13.

Jinak pokud nepotřebujete příliš kontrolovat vlákna, jednoduššeji můžete zavolat asynchronně proceduru takto (tím se ovšem do aplikace natrvalo přidá jedno další vlákno, které se pak ale používá na všechna tato volání):

For i As Integer = 1 To 100
    System.Threading.ThreadPool.QueueUserWorkItem(AddressOf Procedura, Nothing)
Next i

...
Sub Procedura(ByVal state As Object)
...

Akorát procedura musí mít jediný parametr typu Object, jinak se to nezkompiluje. V tomto parametru ale můžete proceduře přidat argumenty, pokud to nepotřebujete, dejte Nothing.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Tisíceré díky, funkčnost, kterou popisujete při použití ThreadPoolu je přesně to, co by mi vyhovovalo - co nejdříve to vyzkouším.

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

Ještě pro doplnění. Zkoušel jsem zjistit počet vláken (o těch "skrytých" sloupcích ve správci jsem nevěděl, byl jsem zvyklý vybírat "windowsovsky" zobrazené sloupce klepnutím pravým tlačítkem myši na záhlaví - což tady nefunguje) a skutečně máte plnou pravdu - mám jich tam stabilně 7, pouze po dobu přechodu (tj. života procedury) to skočí na osmičku.

Takže v tom můj problém nebude. Můj dotaz byl totiž evokován tím, že využití paměti ve správci úloh u mé procedury postupně rostlo z nějakých 100 MB až na téměř 1,5 GB, pak po nějaké čtvrthodince krátce "zahrčí" HDD, využití paměti spadne na 60 MB a celý cyklus se opakuje. V používané třídě pracuji hodně s objekty Bitmap (v kostce - načtu z disku bitmapu v originální velikosti, abych mohl zjistit její parametry, a dle těchto parametrů načtu tuto bitmapu do nové proměnné s rozměry upravenými dle velikosti obrazovky - nejde to dělat v jednom kroku, protože dle poměru stran originálu vytvářím nový obrázek buď stejných rozměrů jako obrazovka, nebo stejné výšky (pro horizontální panoramování), či stejné šířky (pro vertikální panoramování)) No a s touto novou bitmapou pak provádím vlastní animaci.

Proto předpokládám, že problém s extrémními nároky na paměť souvisí s používanými objekty Bitmap. Proto se snažím (i přes skutečnost, že jsou vždy použity pouze lokálně, takže teoreticky by měly po ukončení procedury zaniknout) ve chvíli, kdy už je nepotřebuji, volat na ně dispose, ale nemá to žádný efekt.

Nebo toto chování aplikace je u "managed" aplikací normální a souvisí s vlastností gargabe collection (nebo jak se jmenuje ten neřád, co se stará o paměť), která si řekne, že když má k dispozici tolik paměti tak co by se honila a úklid provádí až ve větších celcích, a nemá tudíž cenu se tímto fenoménem zabývat?

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

Na tohle ten Garbage Collection je, jakmile drasticky dojde paměť, začne ji uvolňovat. Ale docházet by k tomu nemělo, tohle je až krizový případ. Pokud pracujete s bitmapami, není se čemu divit. Doporučuji vždy po ukončení práce s bitmapou zavolat na ni metodu Dispose. Speciálně u bitmap je to potřeba, protože berou opravdu dost paměti. Podívejte se také, zda-li načítání grafiky ze souboru neděláte při každém framu v animaci, protože 1,5GB paměti mi přijde až dost. Snažte se nevytvářet v každém snímku animace nové objekty, ale použít ty stávající.

Nevím, jak máte aplikaci stavěnou, takovýhle memory-leak se může hledat dost špatně, jsou na to asi nějaké speciální nástroje, ale nic takového jsem zatím nepotřeboval.

Jinak pokud děláte animací mnoho, doporučuji zvážit použití DirectX, GDI+ je na jednoduché věci a ne na animace desítek objektů.

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

Díky za rady.

Metodu dispose se snažím volat všude, kde dojdu k přesvědčení, že danou bitmapu už nebudu potřebovat, ale jak jsem napsal, vše je lokálně deklarované, a tím si vysvětluji, že se situace přidáním těch dispose volání nikterak nezměnila (jestli jsem dobře pochopil Vaše výklady v článcích pro nás (začátečníky) i ostatní literaturu, dělám to samé, co stejně systém při skončení procedury dělá sám - volá dispose, čímž ale neuvolní paměť, jenom řekne té paní Garbage, že až bude chtít, může se tohoto smetí zbavit.

Jinak při vlastní animaci již žádné bitmapy netvořím, už s nimi pouze manipuluju a "nějak" je vykresluju metodou drawimage do bufferu, který pak překlopím. Ta kvantita je dána hlavně tím (a průběh nárůstu použité paměti tomu odpovídá), že načítám bitmapu v prvním kroku v plném rozlišení (a velkou část fotek mám ze svého 7M foťáku) a teprve pak z ní vytvářím druhou bitmapu rozlišením odpovídající obrazovce (abych při vlastní manipulaci nemusel už pracovat s tak objemným souborem, což se dost projeví na rychlosti).

Ohledně DirextX - Ano, nejprve jsem pokukoval i po této technologii, pak jsem se ale umravnil, že toto není nic pro nás-úplný ucha v oboru. Leč pak jsem si přečetl Váš článek o DirectX a díky jeho srozumitelnosti jsem nabyl dojmu, že to zvládnu, leč dojel jsem na skutečnost, o které jsme již pod zmiňovaným článkem diskutovali. DirectDraw již není doporučené (což by nevadilo, jak jste mne také ujistil, po stránce provozní, ale problém je v tom, že jsem se nikde nedopídil žádné použitelné dokumentace k jednotlivým funkcím - všude se pouze dočtu proč by se to již nemělo používat a na konci celého odstavce povídání jednu větu, na co funkce kdysi sloužila - bez popisu parametrů, ...) no a v direct 3D je to úplně jiné a na to mé skromné schopnosti skutečně nestačí. A pokud k tomu přičtu skutečnost, že pokračování Vašeho seriálu jsem se prozatím nedočkal :-( a na můj dotaz ve fóru (ohledně řešení průhlednosti) se nikdo nechytil (pouze pan Renner se mi snažil pomoci - ale řešil to stejně jako já přes GDI+), tak mi z toho všeho vyšla jediná možnost, zůstat u GDI - a pro účely, pro jaké to zamýšlím, to nakonec vypadá, že by to výkonnostně mohlo nakonec dostačovat, navíc celou funkcionalitu animace mám hozenou v samostatné třídě takže nikde není řečeno, že to nebudu moci do budoucna do toho Directu překlopit (samozřejmě to předpokládá, že Vy máte v plánu s články o DirectX pokračovat "-)).

Ještě jednou díky za rady a omlouvám se za zdržování - vím, že smyslem tohoto webu není a ani nemůže být, abyste řešili konkrétní úkoly za nás neznalé, a ani bych to nechtěl, protože tak bych se nic nenaučil.

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

Ještě dovětek, aby to bylo zcela zřejmé - neanimuji desítky objektů, ale pouhopouhé dva, zato vytrvale. Smyslem celé aplikace ce prachsprostá fotoshow na fulscreenu, tzn. jedna fotka -> chvíli počkat -> nějakým přechodem (zatím prolínačka případně přesunutí jedné fotky před druhou, ale plánulu i jiné) druhá fotka -> chvíli počkat -> třetí fotka.... a tak to bude pracovat každou lichou hodinu cca 30-45 minut, 7 dní v týdnu, 356 dní v roce....

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

Někde na to Dispose zapomínáte. Dispose by totiž objekt z paměti mělo odstranit hned, zatímco přiřazení Nothing do objektu řekne, že až se GC uráčí, tak se to uvolní.

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

Tak jsem to procházel a stále se utvrzuji v domnění, že se jedná o vlastnost GC. Sledoval jsem využití paměti při krokování programem a chodí to asi takto:

Nejprve do své třídní proměnné předám odkaz na nový obrázek.

Jednodušší bude ukázka kódu:

''' <summary>
''' Nastavení obrázku, který se má použít pro příští animaci. Současně je provedena jeho transformace
''' do správných rozměrů a nastavena proměnná m_panorama
''' </summary>
''' <value>Úplná cesta k souboru s novým obrázkem pro příští animaci(ve formátu String)</value>
''' <remarks></remarks>
Public WriteOnly Property obrFromFile() As String
   Set(ByVal obrFile As String)
   Dim obr As New Bitmap(Image.FromFile(obrFile))
   m_obrazek.Dispose()

   Select Case obr.Width * m_Cntrl.Height / obr.Height / m_Cntrl.Width
      Case Is > 1.1                                           ' panorama na šířku
        m_obrazek = New Bitmap(obr, CInt(obr.Width * m_Cntrl.Height / obr.Height), m_Cntrl.Height)
        m_panorama = panorama.pan_horizontal
      Case Is < 0.9                                           ' panorama na výšku
        m_obrazek = New Bitmap(obr, m_Cntrl.Width, CInt(obr.Height * m_Cntrl.Width / obr.Width))
        m_panorama = panorama.pan_vertikal
      Case Else
        m_obrazek = New Bitmap(obr, m_Cntrl.Size)
        m_panorama = panorama.pan_no
    End Select

   obr.Dispose()
  End Set
End Property

(m_obrazek je lokalni promenna s obrazkem pro vlastní animaci - již rozměrově patřičně zmenšená).

Takže pokud v době předání parametru do dédo vlastnosti začínám s pamětí někde na 58MB, po nadeklarování a načtení proměnné obr mi to stoupne na 109MB. Dále následuje (pro jistotu) dispose původního nastavení m_obrazek, které se neprojeví nijak.

Novým nastavením m_obrazek mi to stoupne na 112MB. No a nastává jádro pudla, při volání dispose na obr se mi to skutečně přesně dle Vašich slov projeví okamžitě, ale neklesne to o původních 50 MB, ale pouze o 27 na nějakých 85MB.

Pak proběhne vlastní animace (metoda spuštěná v samostatném vlákně), po ukončení které to klesne ještě o nějaké 3-4MB (odpovídá to, jako by se uvolnil m_obrazek, ale nechápu proč - vysvětlovalo by to ale, proč se neprojeví dispose na tuto proměnnou).

Zkrátka další kolo začínám na 81MB a končím (když stejným postupem přejdu přes 126 a následně pak 129MB) na 105 (po ukončení animace 102)MB. Atd., atd.

Jestli ono to nemůže souviset s údržbou paměti jako takové (co jsem se dočetl, tak GC jednak uvolňuje prostředky z paměti (což asi voláním dispose částečně supluju), ale také defragmentuje paměť, což už si myslím dělá zcela sám a kdy se mu zachce).

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

Začnu odzadu - ta defragmentace paměti se příliš neprojevuje a GC ji dělá jen pokud zrovna nemá co dělat. Jeden z problémů bude určitě v tomto řádku:

Dim obr As New Bitmap(Image.FromFile(obrFile))

Vytváříte nový objekt Bitmap, do kterého dáváte objekt Image (vrácený metodou FromFile). Máte tak vlastně dva objekty, v obou je obrázek, takže berou 2x více místa. Dispose pak ale voláte pouze na obr a vnitřní objekt zůstane na Garbage Collectoru.

Řešení je triviální (při návrhu .NET frameworku pamatovali opravdu na všechno, takže udělali vhodné přetížení):

Dim obr As New Bitmap(obrFile)

nebo

Dim obr As Bitmap = Image.FromFile(obrFile)

Bitmap je totiž odvozená od Image, můžeme tedy Image přiřadit do Bitmap.

Možná tam bude ještě něco, ale nevidím zatím nic. Pamatujte, že cokoliv neodstraníte ručně, zůstane na GC. Pokud je to nějaký Integer nebo něco malého, je to jedno. V okamžiku, kdy jsou to velké obrázky, už se to projeví.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

DÍÍÍKY, tak tady byl pes zakopán - už je to OK. V tom volání byla tvorba dalšího objektu tak skryta (mým neznalým očím), že jsem to skutečně přehlédl. A to přetížení jsem taky nějak přešel. Jo hold proč to dělat jednoduše, když to jde složitě, že?

(abych se trošku omluvil, ten vstupní image tam byl proto, že původně jsem již v tomto kroku dělal transformaci velikosti na rozměr obrazovky (a ta chce vstup jako image) no a když jsem došel do stádia, že ne všechny fotky budou přesně v požadovaném poměru stran a že doplním do slideshow i panorámování širších či vyšších fotek, tak jsem z volání pouze odstranil tu přesnou specifikaci cílové size a více jsem o daném kroku nepřemýšlel). No a na vrub mé neznalosti a nezkušenosti padá to, že jsem vůbec netušil, že i při takovéto konstrukci, jakou jsem použil, vzniká nový objekt (aniž je uvnitř použito "new") a že je tedy nutno se i o něj postarat.

Díky za Vaše přínosné rady a těším se na další pokračování Vašich seriálů - stejně jste tento můj problém zavinil hlavně Vy - na straně jedné jsou Vaše články natolik čtivé, sdělné a pochopitelné, že i já jsem se na svá stará kolena pustil do oblasti, kterou bych snad měl přenechat už radši svým potomkům, a na straně druhé frekvence vydávání vašich lekcí nejsou natolik dostatečné, abych byl zaměstnán jejich louskáním a nepředbíhal svými praktickými pokusy probíranou látku. :-)))

(P.S. ten poslední odstavec není myšlen jako výtka (!), spíše jako kompliment.)

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

Je mi jasné, že je to blbá chyba, člověk se prostě přehlédne, i mě se to stává dnes a denně. Jinak pro ujasnění objekt vzniká vždy pomocí klíčového slova New, ale to ještě neznamená, že to New musíte napsat vy. Ono i v tom Image.FromFile to New někde je, ale napsali jej již programátoři .NET Frameworku.

Jinak chápu, že by frekvence článků mohla být vyšší (je nás tady víc redaktorů, ale nejčastěji stíhám psát já), ale čas prostě není.

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