Referenciaszámlálás, elszálló mc

Fórumok

Korábbi topikokban már írtam, hogy az mc sajnos nagyon elszállós, és ez a hajlama az idő múltával nem csökken hanem inkább verzióról verzióra nő. Leggyakoribb esetek: Kijelölöm inserttel a törölni kívánt fájlokat, F8-at nyomok, mire az mc el-SIGSEGV-zik, és hagy egy core fájlt. Vagy csak ki akarok lépni, F10-et nyomok, mire keletkezik egy core fájl. A jelenség nem determinisztikus, néha előjön, néha nem.

Hogy hardverhibás a gépem. Persze, mind a húsz, és mindegyiknél éppen az mc-ben jön elő a hardverhiba. Más fórumokon is panaszkodnak az F8-as elszállásra. Egy helyen a Fejlesztő Úr azt válaszolta, hogy ő többezer fájlon tesztelte sikeresen az F8-as törlést, és szerinte a program jó. Persze hibás a logikája, millió fájlon végzett sikeres tesztből is csak annyi következik, hogy a program nem biztosan rossz.

Gondoltam, megnézem hol szálldos el az mc. A /etc/rc.local-ba bettettem ezt a sort:


echo '/var/crash/%e-%t.core' >/proc/sys/kernel/core_pattern

Ha ezután bármi elszáll, akkor /var/crash-ben keletkezik egy egyedi core fájl (lásd man core). Az mc esetében ilyesmi:


mc-1452426735.core

Hogy kényelmes legyen nézelődni, az mc-t úgy konfiguráltam, hogy az ilyen fájlon entert ütve rögtön induljon el az adott fájlra a gdb (debugger), amiben a bt parancs mutatja a callstacket. Ilyeneket látok:


...
Core was generated by `mc'.
Program terminated with signal SIGABRT, Aborted.
#0  0x00007f8404de0cc9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
(gdb) bt
#0  0x00007f8404de0cc9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1  0x00007f8404de40d8 in __GI_abort () at abort.c:89
#2  0x00007f8404e1d394 in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f8404f2bb28 "*** Error in `%s': %s: 0x%s ***\n") at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007f8404e2966e in malloc_printerr (ptr=<optimized out>, str=0x7f8404f2bcc8 "free(): invalid next size (fast)", action=1) at malloc.c:4996
#4  _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:3840
#5  0x0000000000471043 in release_hotkey (hotkey=...) at widget-common.c:103
#6  0x000000000048680e in menu_entry_free (entry=0x25464a0) at menu.c:791
#7  0x00007f84053d2648 in g_list_foreach () from /lib/x86_64-linux-gnu/libglib-2.0.so.0
#8  0x00007f84053d266b in g_list_free_full () from /lib/x86_64-linux-gnu/libglib-2.0.so.0
#9  0x0000000000486991 in destroy_menu (menu=0x25c9060) at menu.c:831
#10 0x00007f84053d2648 in g_list_foreach () from /lib/x86_64-linux-gnu/libglib-2.0.so.0
#11 0x00007f84053d266b in g_list_free_full () from /lib/x86_64-linux-gnu/libglib-2.0.so.0
#12 0x0000000000486a82 in menubar_set_menu (menubar=0x25cedb0, menu=0x0) at menu.c:862
#13 0x0000000000486312 in menubar_callback (w=0x25cedb0, sender=0x0, msg=MSG_DESTROY, parm=0, data=0x0) at menu.c:618
#14 0x000000000041c2ea in send_message (w=0x25cedb0, sender=0x0, msg=MSG_DESTROY, parm=0, data=0x0) at ../../lib/widget/widget-common.h:162
#15 0x000000000041c48e in dlg_broadcast_msg_to (h=0x25ce030, msg=MSG_DESTROY, reverse=0, flags=0) at dialog.c:148
#16 0x000000000041dd49 in dlg_broadcast_msg (h=0x25ce030, msg=MSG_DESTROY) at dialog.c:986
#17 0x000000000041e4c4 in dlg_destroy (h=0x25ce030) at dialog.c:1268
#18 0x000000000044b3da in do_nc () at midnight.c:1793
#19 0x00000000004099e2 in main (argc=1, argv=0x7ffeefc18588) at main.c:400
(gdb) q

Ebből annyi látszik, hogy a widget-common.c modul 103. sorában meghívott release_hotkey függvényben következett be a baj. Lássuk, mi van ott:


void
release_hotkey (const hotkey_t hotkey)
{
    g_free (hotkey.start);
    g_free (hotkey.hotkey);
    g_free (hotkey.end);   //  <-- 103-dik sor
} 

Mi a tanulság. A gobject rendszer referenciaszámlálós szemétgyűjtést használ. Ha "valaki" használ egy objektumot, azaz referenciát tart fenn az objektumra, az megnöveli 1-gyel az objektumban tárolt referenciaszámlálót. Ha már nem kell az objektum, akkor meg 1-gyel csökkenti. Az objektum figyeli a saját referenciaszámlálóját, és ha a számláló 0-ra csökken, az azt jelenti, hogy már senkinek sincs rá szüksége, és megszünteti magát. Egyszerű, de sajnos több sebből vérző módszer. Ott vannak a körök. Ha az objektumok kölcsönösen referenciát tartanak fenn egymásra, akkor a referenciszámlálójuk sosem csökken 0-ra, zárványként beragadnak. Még nagyobb baj, hogy a referenciaszámláló módosítgatása sok esetben az alkalmazási program részévé válik, és nem világos, hogy kinek, és milyen időzítéssel kell elvégezni a módosítást. Az mc esetében is ez lehet a baj. A program g_free-vel meg akar szüntetni egy olyan objektumot, amelynek (vagy egy beágyazott objektumának) a refszámlálója korábban (tévesen) már le volt csökkentve. Na és persze a program elszállása, nem a hiba elkövetésekor következik be, hanem jóval később. Vagyis a hiba helyéről nem tudtunk meg semmit. Az ilyet baromi nehéz kinyomozni.

A referenciaszámlálós szemétgyűjtést nem is tekintik igazi szemétgyűjtésnek. Őszintén szólva az IT tudományban dilettáns modszernek számít. Éppen ezért kár, hogy ilyen rendszerek, mint a gtk (gnome, xfce4), python, referenciaszámlálós szemétgyűjtésre épülnek. Agyaglábakon állnak.

Hozzászólások

Szép nyomozás, respect. Ugyanakkor nem tudom visszafojtani a kérdést: a 'glib' az nem egy külön komponens, amit a 'mc' meghív, de nem része a 'mc'-nek?

Megj: mondjuk ez a release_hotkey nem igazán látszik valid C-kódnak, ott valami ilyesmi lenne:

void release_hotkey (hotkey_t *hotkey)
{
    g_free (hotkey->start);  hotkey->start= NULL;
    g_free (hotkey->hotkey); hotkey->hotkey= NULL;
    g_free (hotkey->end);    hotkey->end= NULL;
}

Hát, lefordulni lefordul, most néztem meg a saját gépemen a mc-4.8.11 forrását, de azért említsük meg, hogy egyrészt hagyományos C-programozói ésszel struktúrát nemigen passzolunk függvénynek, inkább a struktúra címét, másrészt a 'const' szerepét nem vélem én itt érteni, különös tekintettel a 'g_free'-re...

Off: belenéztem a gmem.h-ba is, láttam, hogy van egy g_malloc, ami gsize paramétert kap, és gpointert ad gvissza... ez már gkicsit gtúlzásnak is gtűnik...

Kicsit cikis a konkluzio azert. Azt mondani, hogy az rc rossz, a gc jo, feluletes. Nezzuk sorban az erveket:

- Hogy alsobbrendu lenne az rc? Nem feltetlenul. Egyreszt lenyegesen egyszerubb az mukodese, trivialisan szalbiztossa teheto. Es valami, amit a gc sosem fog tudni: determinisztikus a mukodese. Hogy ez miert is erdekes, arrol majd lejjebb. Az, amibe itt hibakent botlottal, az inkabb tekintheto a c egy limitaciojanak. Ugyanis itt hianyzik a pl. C++-ban mar jelen levo stack alapu destruktor mechanizmus, ami igen komoly garanciakat biztosit a programozonak, az eroforrasok preciz es korrekt kezelesere (RAII).
- Korkoros referenciak: igen, nagyon feluletesen nezve ez az a dolog, amit az rc nem tud kezelni, de a gc meg automatikusan megoldja, anelkul, hogy erre gondolni kellene, ugye? Hat nem. Egyreszt praktikusan minden referenciaszamlalt nyelvben letezik a gyenge referencia fogalma (pl. c++-ban a std::weak_ptr), amivel ezt a jelenseget meg lehet kerulni, vagy valaszthatja a programozo, hogy a kort "explicit" modon szakitja meg (valamit nullptr-re allit). Ezzel olyat nyer, amit a gc sosem tud biztositani: onnantol kezdve az eroforrasok felszabaditasanak helye es ideje determinisztikussa valik. Azaz nem felszabadul "majd valamikor", hanem pontosan lehet tudni a helyet/helyeket. Ezen felul gc kodban a destruktorok eseten a kulso referenciakkal ugy kell banni, mint a himes tojasokkal (leginkabb az a good practice, hogy kulso referenciakra nem hivatkozunk a destruktorban, vagy nem is irunk destruktort, mert kiszamithatatlan/nemdeterminisztikus).
- Kivetelek felszabaditas soran. Disclaimer: "children, don't try this at home". Nem batoritok senkit arra, hogy olyan destruktorokat irjon, amik kiveteleket dobalnak, de ez C++-ban egy valid dolog es van nemi haszna is esetenkent. Na ezt pl. gc nyelvben megintcsak nem lehet megjatszani, mert a destruktorbol dobott kivetel nemigen tud hol landolni, gondolom, ha senki nem kapja el, akkor a runtime-ban vegzi, ami vagy elnyeli, vagy terminalja a programot, egyik sem nyero.
- Egeszen biztosan nem szamit "dilettans modszernek" az rc, ha valahol ezt tanultad, akkor dobd el a konyvet messzire. A gc es az rc ugyanarra a problemara ad megoldast csak a trade-off-ok mashol vannak. Vannak produktiv rc nyelvek (c++11, obj-c, swift), es vannak produktiv gc nyelvek (C#, java, haskell, ...). Es most hagy legyek egy kicsit inkorrekt: eppen pont nem a java programok hiresek arrol, hogy a memoriat milyen korrekten es konzervativan kezelnek ;) (eclipse? tomcat? ismerosen csengenek ;)).

Igen, csak hiaba van java-ban weak reference, attol meg nem lesz determinisztikus az objektumok felszabaditasa. De lehet, hogy en ertettem felre valamit ;)....

C#-ban van using es IDisposable (meg van ennek valami analogiaja Java-ban is). Ha valaki megvalositja az IDisposable-t es a using konstrukcioval hasznalja az objektumot, akkor annak az egy szem objektumnak a felszabaditasa mar determinisztikus lesz (a Dispose() meghivodik, amikor a vegrehajtas elhagyja a using scope-jat).

Nyilvan ez is egy trade-off. Igy lehet azt mondani, hogy az adott eroforras elengedese biztosan megtortenik, amikor a scope elhagyasra kerul. Ez neha nem csak optimalizacio miatt fontos, hanem azert mert ha az eroforras egy lock (vagy valami hasonlo szinkronizacios primitiv), azert eleg ciki lenne, ha az elso gc-ig blokkolnanak a program egyeb reszei. Viszont ennek egyreszt van egy szintaktikus "terhe": minden eroforrashoz uj scope-ot kell nyitni a forraskodban (ez pl. c++-ban nem igy van). Masfelol meg ha valahol elfelejtodik a using, akkor ugyanott vagyunk, ahol a part szakad...

Van meg az az orokke tarto alfejezete ennek a vitanak, ami a teljesitmenyre vonatkozik. Bar altalaban a gc nyelveket lassunak szoktak tartani, sokat javitottak mar ezen (meg ugye mar van hatekony multi-threaded gc), meg mar boven van folos szufla a mai memoriak teljesitmenye miatt. Van valahol egy tavoli igeret, hogy a heap compact miatt a gc programok memoria lokalitasa jobb lesz, szoval hatekonyabb lesz a cache kihasznalas es elkerulik a heap fragmentaciot. Ez a gyakorlatban szerintem nem ervenyesul (inkabb ugy mondom, hogy szintetikus benchmarkoknal mar latszanak az eredmenyek), de ha egyszer bejon az jo lehet.

Bottom-line: nem kell ezt a vitat tul hosszura nyujtani, csak arra probaltam utalni, hogy a gc nem egyertelmuen "felsobbrendu" az rc-hez kepest. Ezek hasonlo technikak egy adott problema megoldasara, de mas szempontokra optimalizalnak. Ha valakinek fontos a determinisztikussag, es tobb opciot akar kezben tartani az eroforrasok menedzseleseben, annak az rc a jobb. Ha valaki inkabb tobb automatizmust szeretne (meg nem akar memory leak-et es undefined behaviour-t vadaszni :) ), akkor jobb a gc.

> akkor annak az egy szem objektumnak a felszabaditasa mar determinisztikus lesz

ööö, itt sem az objektum felszabadítása lesz determinisztikus, csak egy adott függvény lesz meghívva a blokk végén. Persze, ott elengedhetek bármit, amit szeretnék, de a memóriafoglalás nem csökken - s a try-with-resources csak syntax sugar, szóval a nélkül is elérheted ugyanezt a működést.
--
blogom

Az objektum altal "kozvetlenul" elfoglalt memoria talan a kisebb problema ezekben az esetekben (nyilvan nem orom, de lenyelheto beka a gyakorlatban). Inkabb az a necces, amikor az objektum valamilyen eroforrast kezel (legyen ez tranzakcio, socket, lock, file, nagyobb buffer). Es ebbol a szempontbol nagyon nem mindegy, hogy az adott eroforras mikor lesz elengedve. Ha nem a using-ot hasznalod, akkor pl. a lock biztosan megmarad a kovetkezo gc-ig, de lehet, hogy meg azutan is, ugyanis semmi nem garantalja egy gc rendszerben, hogy az osszes elerhetetlen objektum felszabaditasra kerul!!! Pl. donthet ugy az algoritmus, hogy mivel mar sok munkat vegzett, es hogy ne alljon tul sokaig a program (pl. gui vagy real-time alkalmazasok eseten ez lehet problemas), ezert benthagy egy kis szemetet a kovetkezo gc-re. Ha az eroforras egy listening socket, amit te mar elengedtel, es rogton utana szeretnel egy ujat letrehozni, akkor a bind() el fog hasalni, mivel a regi socket meg nincs bezarva. Ez konkretan helytelen viselkedes es csak a using tud segiteni rajtad.

Horribile dictu: ha tenyleg vannak real-time kriteriumok a rendszerben (tehat nem futhat gc bizonyos muveletek vegrehajtasa kozben), akkor szokas a gc-t idolegesen letiltani. Ilyenkor kis figyelmetlenseggel es a using hanyagolasaval azert mar lehet dead-lock-ot is csinalni! Ha becsuletesen hasznalod a using-ot, akkor ez a gond nem all, mert a Dispose() meg fog hivodni meg letiltott gc mellett is.

Asszem a gc-rendszerekben egyebkent is a destruktorok irogatasa nem eppen good practice. Helyette az IDisposable megvalositasa preferaltabb. Viszont onnantol kezdve tenyleg a programozon all, hogy explicite using-on keresztul kell az osszes objektumot hasznalnia, ha korrekt viselkedest akar es nem szeretne szivni a fentiekkel.

teljesen értem, mit szeretnél mondani, de az továbbra sem pontos.

A következő két kód tökéletesen* ekvivalens Javaban:


    public void test1() {
        InputStream is = getInputStream();
        try {
            is.read();
        } catch (IOException e) {
            //catch
        } finally {
            // close
            if (is != null) {
                try {
                    is.close();
                } catch (Exception ignore) {
                }
            }
        }
    }
    
    public void test2() {
        try(InputStream is = getInputStream()) {
            is.read();
        } catch (IOException e) {
            // catch
        }
    }

Meg merem kockáztatni, C#-ban is hasonló syntax sugar a using, semmi több.
Itt, az első esetben, bár nem használok Closeable interfészt, se try-with-resources dolgot, mindenféle GC nélkül elenged minden erőforrást az InputStream a blokk végén. Ezt el lehet érni mindenféle IDisposable, meg using, meg egyebek nélkül is, rendesen odafigyelve. És nem, ennek továbbra sincs semmi köze ahhoz, hogy „annak az egy szem objektumnak a felszabaditasa mar determinisztikus lesz”. Meg lesz hívva egy függvény, amit sacc-per-kábé ugyanarra szokás használni, mint a destruktort c++-ben (engedje el a korlátos erőforrásait az objektum), de ez merőben más, mint amit te állítottál.

*: a tökéletesen túlzás, tudom. a példa szempontjából azonban lényegtelen a különbség.
--
blogom

Én ezt a részt nem értem teljesen:

"amit te mar elengedtel, es rogton utana szeretnel egy ujat letrehozni, akkor a bind() el fog hasalni, mivel a regi socket meg nincs bezarva. Ez konkretan helytelen viselkedes es csak a using tud segiteni rajtad."

Ha a megfelelő módon (serverSocket.close())-zal kerül a socket lezárásra, utána szerintem simán lehet újra socket-et nyitni, ennek működnie kell.

Ket kriterium van:
1. Ne kelljen expliciten hivogatni a close()-t mert az hibaforras (el lehet felejteni). Tortenjen meg automatikusan, amikor mar nem kell az eroforras.
2. Az automatizmus determinisztikus kell, hogy legyen, tehat a close() meghivasa nem lehet a gc resze, hiszen nem alapozhatsz ra, hogy a gc mikor tortenik meg.

Erre van ugye a scope based resource management:


void foo() {
try (Socket s = new Socket()) {
}
}

Itt megvan az a garanciad, hogy amint elhagyod a try scope-ot, az s.close() meghivasra kerul, fuggetlenul a gc-tol.

Ennek a megoldasnak az a problemaja, hogy maga a try-with-resource egy explicit keres, amit szinten el lehet felejteni. Mondjuk azert mert a Socket osztaly egy korabbi iteracioban meg nem volt Closeable (kicsit eroltetett a pelda de van realitasa mas kontextusban).

pl. ha ezt csinalod:


void foo() {
try {
Socket s = new Socket();
} finally {
// ...
}
}

Es a finally-ban elfelejted a socketet zarni, akkor megszivtad, marad jo sokaig egy elerhetetlen Socket-ed nyitva. Es ha a foo() ujra meghivodik, akkor majd a bind() elhasal es megvan a baj.

Gondolom van erre okos felmegoldas a java-ban, pl. a Socket osztaly finalizere megnezi, hogy nyitva van-e a socket es ha igen, akkor bezarja. Akkor mondjuk a kovetkezo gc-ig all csak a problema, de ez mindenesetre nem korrekt eroforraskezeles (megserti az 1. kriteriumot).

Es arra hogy rosszul hasznalod az objektumot, arra nincs warning, nincs semmi (sajnos ezt eleg nehez lenne kikovetkeztetni a forditonak).

Nekem eros a gyanum, hogy a Java programok egy jo reszeben ezzel a gc-be patkolt close()-al tortenik az implicit eroforras management. Bar tobbnyire ez valid mukodest eredmenyez, de nagyon nem hatekony...

Milyen két kritérium? Szó nincs két kritériumról. De azt mondtad, hogy a using használatával determinisztikus lesz az objektum felszabadítása (idézek: „Ha valaki megvalositja az IDisposable-t es a using konstrukcioval hasznalja az objektumot, akkor annak az egy szem objektumnak a felszabaditasa mar determinisztikus lesz (a Dispose() meghivodik, amikor a vegrehajtas elhagyja a using scope-jat).”).

Nem, ettől nem lesz az osztály felszabadítása determinisztikus, csak egy adott függvény garantáltan meghívódik a blokk végén. Ezt pedig más konstrukciókban is meg tudod oldani. A GC-t meg nem is tudom, minek kevered ide, mert az egész IDisposable, AutoClosable, using, try-with-resources dolognak semmi, de az égegyadta világon kívül semmi köze nincs a GC-hez.

> Es a finally-ban elfelejted a socketet zarni, akkor megszivtad, marad jo sokaig egy elerhetetlen Socket-ed nyitva.
Ha meg saját resource-kezelő osztályt írok, akkot a close() függvényt is implementálhatom rosszul. És?

> Nekem eros a gyanum, hogy a Java programok egy jo reszeben ezzel a gc-be patkolt close()-al tortenik az implicit eroforras management. Bar tobbnyire ez valid mukodest eredmenyez, de nagyon nem hatekony...
Fogalmad sincs arról, miről beszélsz. A close-nak semmi köze a GC-hez: a close nem fog meghívódni GC alatt.

Részemről a szál lezárva, mert mind a témát tereled, s mind a Java-.NET világ koncepcionálisan ismeretlen számodra. Ami alapvetően nem baj, csak akkor ne kezdj el olyan állításokhoz ragaszkodni, amire felhívják a figyelmedet, hogy nem igaz.
--
blogom

Figyi, csak arról van szó, hogy keveri az erőforrás-felszabadítást meg a memória-felszabadítást.
Az előbbi meg kell, hogy előzze az utóbbit, de az utóbbi nem kell, hogy midnig kövesse az előbbit. Csak éppen C++ világban a két dolog nem tér el egymástól: van a destruktor, és kész.
Pedig a két dolog koncepcionálisan különbözik, de a C++ nem képes a megkülönböztetésre sajnos. És ez nem a Java/.NET hibája :)

Egeszen eddig arrol az esetrol beszeltem, amikor az objektumhoz az altala lefoglalt memorian tul tartozik valamilyen egyeb tipusu resource. Keresd vissza, majdnem minden hozzaszolasban errol volt szol.

A C++ refcount/RAII parositas azon tulmenoen, hogy kepes a memoriat korrekten kezelni, az eroforras felszabaditast is megoldja automatikusan, determinisztikusan es nem lazy modon. A javaban ehhez neked egy specialis konstrukciot kell hasznalnod (try-with-resource), vagy expliciten meg kell hivnod a close()-t. Es mivel mindketto elfelejtheto, ezert a bennragado eroforras az gond....

Ja es ugy gondolod, hogy mondjuk a C++ std::fstream-nek nincs close() metodusa? Hat van. Igenis szetvalaszthatod az eroforras felszabaditast es a memoria felszabaditast. Masik peldakent a shared_ptr-t tudom idecincalni, ahol van reset() es release() a destruktoron tulmenoen, tehat igen finoman szabalyozhatod, hogy mi is tortenjen a RAII wrapper altal kezelt eroforrassal.

" Es mivel mindketto elfelejtheto, ezert a bennragado eroforras az gond...."
Ugyanúgy elfelejthető a destruktor megírása is. Vagy éppen a destruktorban az erőforrás-deallokálás.
A shared_ptr reset()/release() ugyanúgy elfelejthető.

Ha azzal érvelsz, hogy elfelejthető a try-with meg a close, ugyanúgy elfelejthető a másik is, hiszen semmi nem kényszerít arra, hogy helyes destruktort írj, vagy hogy egyáltalán legyen destruktorod.
Ez egy eléggé hibás érvelés.

1. Ha hibas a destruktorod nyilvan rossz az osztaly, nincs mit kezdeni. Felteve, hogy ezt a feladatot meg tudja oldani valaki *egyszer*.
2.

void foo {
auto os = std::make_shared("foo.txt");
// vagy auto os = std::make_unique("foo.txt");
// vagy szimplan std::ofstream os("foo.txt");
}

Milyen potencialis elbaszasi forrast latsz ezen a kodon? Marpedig ez a kod garantalja neked, hogy a scope lezarasakor az stream bezarul es a memoria felszabadul.

Volt egy ezzel analog forraskod idevagva, ami kb. 6-10 sor volt, es agyrem. Egy csomo realis peldat tudok mondani neked arra, hogy hol lehet elrontani oket figyelmetlenseg/faradtsag/tudatlansag miatt (es minden egyes helyen szamit, ahol hasznaljak az osztalyt). Es ami a legrosszabb, hogy a hiba egyaltalan nem trivialis, a kovetkezmenyei kiszamithatatlanok. Lehet, hogy tesztelesnel minden fasza, kesobb meg leterdel a program...

Nem azt mondom, hogy ez megoldhatatlan problema, nem azt mondom, hogy a Java hasznalhatatlan emiatt. Egyaltalan nem. Azt mondom, hogy ebben a konkret esetben a c++ megoldas biztonsagosabb. Es ezt az rc es a stack alapu RAII tesz lehetove.

Kíváncsiságból kérdezem, nem ismerem annyira a C++-féle RAII-t: ha létrehozok egy A-t, ami létrehoz egy B-t és elmenti egy mezőbe, majd eldobom az A-t, akkor az általa elmentett B is automatikusan felszabadul? Mi van akkor, ha van egy A-m, aminek van egy B-je, és a B körkörösen visszahivatkozik A-ra, és eldobom az utolsó A referenciámat? Vannak esetleg más olyan trükkös helyzetek, amiket nem kezel le, és nagyon észnél kell lenni? (Azt leszámítva, hogy egy statikus helyre elteszek egy referenciát, és direkt örökre otthagyom.)

--

Igazabol sokfele eset van, mivel ugye B-t sokfelekeppen tudod eltarolni (ertek, referencia, pointer, unique_ptr, shared_ptr, weak_ptr, ...).

Ha a RAII-t tekintjuk akkor slendrianul nezve harom dolog jon szoba:
- ertek szerint
- unique_ptr
- shared_ptr

1. ertek:


class A {
B b;
};

Itt A egy peldanyanak memoriatartomanya tartalmazza b-memoriatartomanyat. Ha A destruktora lefut a tartalmazott objektum is biztosan megsemmisul. Ez alol nincs kivetel.

Ez egyebkent RAII, de nem rc. A garanciat a C++ runtime biztositja: scope elhagyasnal a scope-ra lokalis obkektumok destruktora meghivodik (nincs kivetel), tovabba egy objektum destruktoranak lefutasaval az objektum altal tartalmazott tagok es a bazisosztaly destruktora is mehivodik (egy megadott sorrendben (eloszor a "most-derived" destruktor, aztan a bazisosztalyok vegul az adattagok.)

2. unique_ptr


class A {
std::unique_ptr<B> b;
};

Hasonlo az elozo helyzethez, annyi a kulonbseg, hogy lehetoseg van b-t hamarabb felszabaditani (vagy reset()-et hivni b-n, vagy feladni a tulajdonjogot move konstruktor vagy move assignment segitsegevel).

Itt szinten nincs rc.

3. shared_ptr


class A {
std::shared_ptr<B> b;
};

Az elozohoz hasonlo, annyi a kulonbseg, hogy ha A destruktoranak lefutasakor b meg nem nullptr, akkor itt csak a refcount csokken. Az hogy az objektum felszabadul-e, az attol fugg, hogy a refcount 0-ra csokkent-e.

Nagyjabol ennyi.

A visszahivatkozasok kerdese:
1. Esetben biztonsagos a szuloobjektumra visszamutato referencia, mert egeszen biztosan hosszabb a szulo lifetime-ja. Annyira kell figyelni, hogy B destruktoraban a back-reference mar egy olyan objektumra mutat, aminek a destruktora lefutott, de a memoriaja meg nincs felszabaditva. Ritkan elofordulo hiba ilyenkor B destruktoraban hasznalni a visszahivatkozast. Bar nem dob segfault-ot, de valszeg undefined behaviour a c++ szabvany szerint.
2. Esetben ha a unique_ptr-t el-move-oljak, akkor elofordulhat, hogy A peldanya megsemmisul, de B peldanya meg minding tartalmaz egy backreference-t. Ez undefined behaviour => tobbnyire segfault.
3. Lenyegeben a 2. eset. Annyi, hogy nem csak a move, hanem a masolas is veszelyes helyzetet teremt a kesobbiekben.

Ami a tankonyvi megoldas az ilyen parent-child kapcsolatra,


class A {
std::shared_ptr<B> b;
};

class B {
std::weak_ptr<A> a;
};

Ebben az esetben a B::a mindig tudja ellenorizni, hogy a visszahivatkozas valid-e. Van egy limitacio: a shared_from_this() konstruktor prekondicioja miatt, nem lehet felallitani a relaciot A konstruktoraban.

Tehat ez a kod sajnos invalid:


using namespace std;

class A : public enable_shared_from_this<A> {
public:
A() : b(shared_from_this()) {}

shared_ptr<B> b;
};

class B {
public:
B(shared_ptr<A< parent) : a(parent) {}

weak_ptr<A> a;
};

Jo lenne, ha igy lenne, de sajnos nem igy van. Az okok eleg messzire vezetnek, mondjuk erdemes raszanni az idot, mert hasznos dolgokat tanul meg az ember :).

Ez Andrei-nek szól, csak a hozzászólásában van egy link.

"mert egeszen biztosan hosszabb a szulo lifetime-ja."

Erre azért nem kellene feltétlen alapozni, rengetegszer futottam már bele olyanba, hogy itt vagy ott, de maradt még ilyen vagy olyan okból egy referencia egy belső objektumra.

Viszont sikeresen mondtál három olyan esetet, amiből igazából segfault is lehet és/vagy a viselkedés nem meghatározott, míg egy GC-s nyelvben olyanból nem fordulhat elő hiba, hogy körreferencia van. Nem állítom egyébként, hogy lehetetlen ott is ocsmányságot elkövetni (pl. C#-ban eventekre való feliratkozáskor el lehet követni memleaket szélsőségesebb esetben, illetve még a resourcek kavarhatnak be), de ahhoz általában már valamit alapvetően el kell rontani.

Másrészt determinisztikusság. Igazából attól, hogy számolod a referenciát és tudod, hogy scope elhagyásakor fel fog szabadulni a resource, még egyáltalán nem lesz determinisztikus. Hiszen több referenciád is lehet és nem fogod tudni feltétlen megmondani pontosan, hogy mikor is fog valójában felszabadulni a resource. Ilyen szempontból az IDisposable jóval explicitebb még akár az ilyen szempontból trükkösebb generator method-oknál is. (Igaz, ott a using használata nem túl praktikus, csak a try-finally-é, ugyanis az első yield return-nál meghívná a Dispose() -t.)

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

Az aggregalt objektumra egeszen biztosan alapozhatsz. Egyszeruen nincs olyan, hogy a beagyazott objektum megsemmisul es a kulso megmarad. Ez tuti. Egy lehetoseg van, amikor valami raw pointeren keresztul egyszeruen felulirod a memoriat. Ez nyilvan a szandekos szabotazs es nem a veletlen hiba.

GC nyelvben is lehet korreferencia. Az egyik ok, amiert a gc nyelvekben a finalizerek hasznalata ellenjavallt: ugye a gc eloszor egy listat csinal az elerhetetlen objektumokbol es azokat felszabaditja, de nincs semmi determinisztikussag abban, hogy milyen sorrendben. Tehat egy n elemu kor eseten nem arrol van szo, hogy begyujti az egyik obkektumot, majd szepen sorban a tobbit. Ezert a finalizerekben a referenciak allapota eleg valtozatos lehet. Tehat nagyon ovatosan kell ott eljarni, es minden referenciat ellenorizni kell hivatkozas elott (lehet, hogy egy begyujtott objektumra mutat, lehet, hogy nem). Nyilvan ez veszelyes uzem (veszelyesebb uzem, mint azok a hibalehetosegek, amiket en soroltam fel).

Egyfelol a scope-based-resource eset c++-ban nem referenciaszamitas alapjan megy. Ott unique_ptr, vagy siman ertek szerint a stack-re kerul az objektum, es elnek a szigoru garanciak.

Az egyik gyakori eset (legalabbis az en napjaimban), kicsit pszeudokodos:


void foo() {
lock_guard g;
// ...
}

Itt nincs kivetel, nincs rc. Ez tisztan csak RAII. Az objektum fel fog szabadulni a scope elhagyasakor. Minden esetben (akkor is, ha van kivetel, akkor is ha nincs).

Nyilvan az osztott tulajdonjog egy komplexebb kerdes. Ott az "utolso tavozo ember kapcsolja le a villanyt" strategia megy. Es ez determinisztikus, nyilvan nem egy konkret scope-nal tortenik a felszabaditas, hanem tobb potencialis konkret hely kozul valamelyiken. Nyilvan lehet itt kotozkodni a determinisztikussag fogalman, de szerintem ertheto a kulonbseg Java es C++ kozott ebben a tekintetben.

Igazabol nem tudom, hogy Java-ban mi a korrekt idioma itt. Tehat megosztott AutoCloseable objektumnal hogy intezzuk el, hogy az utolso ember lekapcsolja a villanyt?

"Egyszeruen nincs olyan, hogy a beagyazott objektum megsemmisul es a kulso megmarad."

Na de én pont a fordítottjáról beszéltem, amikor a belső marad meg egy referencián és a külső nem.

" Es ez determinisztikus, nyilvan nem egy konkret scope-nal tortenik a felszabaditas"

Na most az, hogy "tudom, hogy valamikor meg fog történni, de nem tudom, hogy mikor, az számomra nem túl determinisztikus :) Ha egy resource-ra meghívom a Dispose-t vagy kilép az using blokkból, akkor abban biztos lehetek, hogy egy gc nyelvben ott a resource el lesz engedve. Memória nem, de az jelen esetben kit érdekel.

Másrészt megint kevered a memóriát a resourcekkel.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

"Na de én pont a fordítottjáról beszéltem, amikor a belső marad meg egy referencián és a külső nem."

Az aggregacio eseten a ket objektum elettartama szigoruan ossze van kapcsolva. Tehat a beagyazott objektum destruktora az egyetlen veszelyes hely, ahol a visszahivatkozas nem hasznalhato. weak_ptr-el a destruktor is biztonsagos.

A determinisztikussag jelentese: meg fog tortenni az objektum felszabaditasa abban a pillanatban, amikor az utolso referencia megszunik. "az utolso ember, aki elmegy, lekapcsolja a villanyt".

A keveres az en szempontombol azert forog fenn, mert ugye a RAII-nal a ketto egyutt tortenik (ez a lenyeg).

A disposable/autocloseable dologgal az a baj, hogy sokkal tobb munkat rak az API userre.

De most tenyleg erdekelne egy pszeudokodos java megoldas a megosztott eroforras problemara. Itt a feladat:
Tehat van egy stream-ed, n parent objektum (potencialisan kulonbozo osztalyokbol akar) hasznalja sajat (member) referencian keresztul, amint elfogy a referencia az utolso parent objektumra is, tehat mind az n objektum eelerhetetlenne valik (ergo a stream tobbe nem szukseges), akkor *azonnal* szeretnem zarni a stream-et (a memoria nem erdekel, ha a gc kesobb gyujti be, az okes). Erre mi az idiomatikus Java megoldas?

c++-ban minden parent objektum aggregal egy shared_ptr-t a stream-re, amint az utolso is felszabadul v. nullptr-re fut, a stream azonnal bezarodik.

A fenti megosztott erőforrás kezelőre lehet egy jó megoldás a következő pszeudo kód (bármi, akár Java is):


resource = openResource()
useResource(resource)
close(resource)

Ebben az a jó, hogy egy helyen van a megnyitás és lezárás, könnyen átlátható, nem nagyon van hibalehetőség. A useResource-ban tetszőleges sok objektum megkaphatja a hivatkozást, használhatja kedvére, nem kell törődniük a nyitással, zárással.

Ha össze van kötve a memória felszabadítás az erőforrás felszabadítással, akkor a felszabadítatlan objektummal a lezáratlan erőforrás is együtt jár, illetve, ha már egy új erőforrást kellene nyitnia, helyette egy régit fog tovább használni.

Ha jól értelek, arra gondolsz, hogy úgy hívják meg az objektum (amelyik eltette az erőforrás referenciáját) egy függvényét, hogy már a close(resource) meghívódott, de az objektum nem kapott új erőforrást.
Szerintem ilyen esetben mindenképp rosszul működik a program.
A mi esetünkben lezárt erőforrást akar használni (még ez a legjobb működés).
Más esetben pedig vagy nincs erőforrás beállítva (nem írta felül a korábbit), vagy ha megvan még a régi resource, akkor pedig nem szabadna használni, mert az üzleti logika véget ért (useResource). Pl. ez utóbbi esetben oda nem illő dolgokat ír egy fájlba vagy küld a hálózaton.

Jellemzően kétféle módon használnak erőforrást. Az egyiknél egyszerre egy üzleti logikára használják. Megnyitják, használják az üzleti logikára, majd lezárják.
A másiknál több üzleti logikára akarják használni, időben elnyújtva, többet is egymás mellett.
Az alapja ennek egy pool, a program indításakor létrehozzák, a program leállításakor felszabadítják.
Az erőforrás használat itt is lehet ilyen, mint a pszeudo kódomnál. Tehát az erőforrást elkérik a pool-tól, használják az üzleti logikára, majd visszaadják a poolnak.

Uhm... Léccine.

Kezdjük azzal, hogy mi az isten fogja neked lezárni a resourced, ha az useResource dob egy exceptiont? Mi van, akkor, ha az useResource eltárolja a referenciát? Utána Java/C# esetén is minimum Exceptiont, C/C++ esetén jó esellyel segfaultot fogsz kapni a képedbe, ha azt utána megpróbálod használni.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

> Kezdjük azzal, hogy mi az isten fogja neked lezárni a resourced, ha az useResource dob egy exceptiont?

Pszeudo kód.
Ha a useResource exception-t dobhat, akkor nyilván el kell kapni. Jobb exception nélkül megcsinálni, de ez egy másik szál.

> Mi van, akkor, ha az useResource eltárolja a referenciát? Utána Java/C# esetén is minimum Exceptiont, C/C++ esetén jó esellyel segfaultot fogsz kapni a képedbe, ha azt utána megpróbálod használni.

A useResource egy függvény, ami megkapja a használandó resource-t. Arra gondolsz, hogy később nem adsz neki resource-t és úgy akarod használni?

Ez metódus, nem függvény.
Ha Java-ra akarjuk fordítani a pszeudo kódomat, akkor pl. ilyesmi:


try(FileInputStream resource = new FileInputStream("file.txt")) { // openResource
// useResource
} // close

vagy


InputStream input = null;
try {
    input = new FileInputStream("file.txt");

    // useResouce
} finally {
    if(input != null) {
        input.close(); 
    }
}

> De most tenyleg erdekelne egy pszeudokodos java megoldas a megosztott eroforras problemara. Itt a feladat:
Tehat van egy stream-ed, n parent objektum (potencialisan kulonbozo osztalyokbol akar) hasznalja sajat (member) referencian keresztul, amint elfogy a referencia az utolso parent objektumra is, tehat mind az n objektum eelerhetetlenne valik (ergo a stream tobbe nem szukseges), akkor *azonnal* szeretnem zarni a stream-et (a memoria nem erdekel, ha a gc kesobb gyujti be, az okes). Erre mi az idiomatikus Java megoldas?

--
blogom

"Itt a feladat:" vs "n parent objektum (potencialisan kulonbozo osztalyokbol akar) hasznalja sajat (member) referencian keresztul,"

Ha úgy nézzük, akkor ez igazából nem a feladat, hanem már a megoldás.
pl.
Feladat: Web oldalakat olvassunk fel, szedjük ki belőlük a különböző email címeket, mindegyikre küldjünk egy spam levelet.
Megoldás: Készítsük el a következő n db class-t, ..., tegyük bele némelyikbe az URL olvasó erőforrást, másokba a levél küldőt, ...
(van rá jobb megoldás is)

Nézzük úgy, hogy tényleg az a feladat.
Az idiomatikus Java megoldás akkor is az, hogy megnyitod az erőforrást, használod, majd bezárod; vagy poolnál, elkéred, használod, majd visszaadod. Lehetőség szerint egy metóduson belül. Ha közben szopatni akarod magad azzal, hogy ezer helyen eltárolod, akkor rajta, van rá lehetőség, hogy a használat során új objektumokat hozz létre a resource-szal, rosszabb esetben már meglévőkbe injektáld be a resource-ot.

Figyu, szinte bitre ugyanezt a kodot mar mas is mutatta, csak az itt a problema, hogy semmi nem garantalja neked, hogy a finally-ban az inputra nem hivatkozik mas. Es ezt (az en tudomasom szerint) nemigen tudod kideriteni, hogy a close() vegrehajthato-e? Amirol te gyozkodsz minket az egy egyszeru eset (mondjuk ugy, hogy "kizarolagos tulajdon").

Ennek a c++ analogiaja a kovetkezo:

void foo() {
std::ofstream res;
use_resource(res);
}

a scope zarasakor a stream bezarodik es a memoria felszabadul. De ez egy nagyon egyszeru problema. Nem errol megy a vita. Ha neked csak ilyenek jutott az eletben az tok kiraly, de sajnos vannak bonyolultabb szituaciok, amikre ez a pattern nem huzhato ra.

> hogy semmi nem garantalja neked, hogy a finally-ban az inputra nem hivatkozik mas.

Ezt már írtam feljebb, de akkor itt is kifejtem. Nem is kell garantálja semmi sem, hogy hivatkozik-e rá bármi is. Ha hivatkozik rá még valami és a zárás után akarná használni az erőforrást, akkor az a legjobb, ha ott hiba lép fel, mert már az üzleti logika miatti erőforrás használat lezárult. Sokkal rosszabb, ha még a nyitott erőforrást használja olyankor valami.
Mondok egy példát:
Szeretnénk kiírni egy fájlba xml formában n db osztályunkból álló objektumhierarchiát.


resource = openFile(...)
writeObjHier(resource)
close(resource)

A writeObjHier-ben felparaméterezzük az összes osztályunkat az adott resource-cal és meghívjuk a write metódusaikat, hogy kiírják magukat.
pl. ez íródik ki: D(C,A,B(E,F(I),G),H)
Ha a close(resource) futása után valamely felparaméterezett objektumunkra hív valaki egy write-ot, akkor ott hibaüzenet lesz. Ez így a jó, mert a hierarchia már kiíródott.
Ha nem zártuk volna le a resource-ot, mert ez automatikusan akkor záródna, ha nincs már rá sehol hivatkozás, akkor megint kiíródik az adott objektum, ezzel elrontva a hierarchiát a kiírt állományban.
pl. kiíródna egy plusz B objektum és ez lenne a kiírt állományban: D(C,A,B(E,F(I),G),H),B(E,F(I),G)
Ráadásul ilyenkor nem záródik le az erőforrásunk azért, mert hibásan elfelejtettünk egy objektumunkat megszüntetni.

> De most tenyleg erdekelne egy pszeudokodos java megoldas a megosztott eroforras problemara.

Nem vagyok benne biztos, hogy egy magas-szintű nyelvben maga a feladat, amit megfogalmaztál nem-e egy hibás megoldási kísérlet születik-e meg.
ha egy erőforrás véges, akkor nem zárnám le, soha, hanem egy poolban tárolnám, s onnan kérném el - ha nincs szabad erőforrás, akkor várok addig, amíg lesz.
ha nem véges, hanem nyugodtan nyitható-csukható, akkor minden alkalommal újra-nyitnám.
--
blogom

Bocsi, kicsit "sok ho esett" melo fronton, nem jutott ido forumozni.

Egy nagyjabol valid problema, ami -szerintem- az altalam felvetett problemara vezetheto vissza.

A szereplok:

- Message (v. Job) osztaly, ami AutoCloseable, mert valamilyen egyeb resource-ot is aggregal.
- n db MessageHandler osztaly, aminek van egy void process(Job) szignaturaju metodusa. Fontos: a process metodus aszinkron muvelet, tehat visszater mielott befejezodne, tovabba a MessageHandlerek parhuzamosan dolgoznak.

Tehat az uzenet feldolgozasa a kovetkezokeppen nezne ki idealis esetben:


for(;;) {
Message msg = nextMessage();
for(MessageHandler handler : messageHandlers) {
handler.process(msg);
}
}

Itt a using-with-resource idioma nem mukodik, leven a process() aszinkron. Szoval szerintem itt nincs automatikus strategia az msg bezarasara (Java/C# esetben). Termeszetesen van megoldas: adni kell egy callback-et extra argumentumkent a process() fuggvenynek igy tudnak jelezni a handler-ek, amikor vegeztek. Viszont csodak-csodaja: ezzel eppen referenciaszamlalast valositasz meg (tulajdonkeppen minden egyes process hivas egy +1 a referencianak es a callback hivas egy -1).

C++-ban RAII + rc-vel (shared_ptr a Message objektumra) nagyon szepen impliciten kezeli.

Jelen esetben a Message objektum a resource, amihez a MessageHandler-ek parhuzamosan fernek hozza. A problemat vegso soron az jelenti, hogy nehez meghatarozi, hogy melyik az a determinisztikus pillanat, amikor a close() biztonsagosan meghivhato az uzenetre (amikor az utolso MessageHandler is vegzett).

Termeszetesen a pool egy hasznos megoldas ebben a problemaban (en is hasznalnam). Ugyanis szeretnenk magunknak egy flow-control elemet a rendszerben. Ha a handlerek lassuak es az uzeneteket tul gyorsan olvassuk, akkor elszall a memoriahasznalat. Ezert csinalunk mondjuk egy 1000 elemu pool-t az uzeneteknek. Ha van meg a pool-ban szabad elem, akkor olvasunk a halozatrol es letrehozzuk a Message objektumot es kezdjuk a feldolgozast. Egyebkent varunk, hogy a pool-ban ujra legyen hasznalhato eroforras. Tehat ez hasznos komponense a megoldasnak, de a teljes problemat nem oldja meg: jelesul mikor adhatjuk vissza az eroforrast legkorabban.

Hat roviden ennyi. Remelem nagyjabol erthetoen irtam le a dolgokat.

Van más (jobb) megoldás is (Java/C#/Scala esetén), mégpedig a Future-ök (C# esetén Task) használata.
A pszeudo kód:


message = openMessage // Megynitjuk/elkérjük az erőforrást
listOfFutures = useMessage(message) // Használjuk az erőforrást
listOfFutures.onComplete { closeMessage(message) } // Bezárjuk/visszaadjuk az erőforrást

Miért jobb?
- Nem függ az erőforrást tároló objektumok élettartamától az erőforrás lezárása.
- Az üzleti logika vezérli az erőforrás lezárását. Ebben az esetben kétféle lehet:
a) akkor kell azonnal lezárni az erőforrást, ha minden processHandler végzett,
b) akkor kell azonnal lezárni az erőforrást, ha valamelyik processHandler elsőként elszáll, mert pl. nem akarjuk, hogy a még futó processHandlerek hozzáférjenek az erőforráshoz.

Scala-ban írtam is egy kis példát erre:


import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Random, Try }

case class Message(number: Int)

object AsyncResource {
  val processMessage = (waitSecond: Int) => (message: Message) => Future {
    Try {
      Thread.sleep(waitSecond * 1000)
      if (Random.nextInt(6) == 1) throw new Exception("Crash")
      s"Handled $waitSecond sec"
    }
  }

  val openMessage = () => Message(Random.nextInt(100))
  val closeMessage = (message: Message) => println(s"Close message: $message.")
  val resource = () => {
    val handlers = (1 to 5) map { i => processMessage(Random.nextInt(5)) }

    val message = openMessage()
    val processors = handlers map { handler => handler(message) }
    val result = Future.sequence(processors)
    result.onComplete { r => closeMessage(message); println(s"Result: $r") }
  }
}

A resource függvényben van a teszt kód. 5 db processHandler függvényt teszek egy collection-be.
Mindegyik processHandler véletlenszerűen 0-4 mp-ig fut, és 1:6 valószínűséggel elszáll.
Nyitok egy Message erőforrást, feldolgoztatom a handlerekkel, majd ha mind végzett bezárom a Message erőforrást.
A példa kód az a) üzleti logika szerint működik. Ha kivesszük a Try szerkezetet a processMessage függvényből, akkor a b) szerint fog.
Példa kimenetek:


Close message: Message(78).
Result: Success(Vector(Failure(java.lang.Exception: Crash), Success(Handled 1 sec),
Failure(java.lang.Exception: Crash), Success(Handled 2 sec), Success(Handled 3 sec)))

Close message: Message(65).
Result: Success(Vector(Success(Handled 4 sec), Success(Handled 1 sec), Success(Handled 4 sec),
Success(Handled 2 sec), Success(Handled 3 sec)))

Ez is csak refcounting.

Nem arrol vitatkoztunk, hogy a problemat nem lehet Javaban megoldani. Vegig arrol probaltam ervelni, hogy az eroforraskezelesi problemak egy resze csak valamilyen refcounting (vagy azzal analog) modszerrel oldhato meg korrekten. Jelen esetben pl. a Future.sequence az, ami ezt csinalja. Elinditja az n-taskot, megvarja a callback-et, majd a future kompoziciot hasznalva lezarja az eroforrast.

Azt hiszem, hogy még mindig félre megy egy kicsit, hogy mit is vitatok. Valahol ezt írtad:
> En azt probaltam kifejteni, hogy az rc+raii idiom jobb, mint a Disposable/AutoCloseable idiom (flexibilisebb, tomorebb, nehezebben elbaszhato).

Én pedig azt, hogy rc+raii-nál jobb az explicit rc nyitás, használat, rc zárás (és ez utóbbinál teljesen mindegy, hogy milyen a memóriakezelés).

A magam részéről le is zárom itt. További szép napot!

Ez szerintem eros tulzas, hogy csak szintaktikus diszitgetesrol van szo.

test1: ezt hivjak a javaorszagban automatikus memoriakezelesnek? Vicces. Marmint az a vicces, hogy ezt barmilyen szempontbol jobb megoldasnak gondolod, mint az mc c-n alapulo fele refcount bangingjet.

Egyfelol rossz a pelda, mert a feladat az lenne, hogy egy megosztott eroforras *azonnal* automatikusan bezarul/felszabadul, amint az utolso hivatkozas megszunik ra. No ezt a te peldad meg nem tudja (mert expliciten hivod meg a close-t, amit el lehet felejteni). De ugye megtortenhet az a csufos eset, hogy a getInputStream() valami eltarolt referenciat ad vissza. Ellenben te a test1 fuggvenyben szorgosan bezarod ha kell, ha nem (ha te vagy az utolso referencia, akkor be kell zarni, ha nem, akkor nem...).

Csakhogy ne a pofamat jartassam, mondjuk mutatom ennek a c++-analogiajat (rc):


void test1() {
std::shared_ptr is = get_input_stream();
*is << "foobar";
}

ez a kod sok mindent tud, amit jozan esszel el lehet varni eroforras kezelestol
1. Kivetelek eseten is jol mukodik
2. Ha a get_input_stream() nem tart meg referenciat az stream-re, akkor a scope vegen a stream felszabadul es a file bezarodik.
3. Ha a get_input_stream() fenntart referenciat a stream-re, akkor a scope elhagyasakor csak csokken a refcount es semmi egyeb nem tortenik.
4. Nagyjabol nulla a szintaktikus overhead ... Gondolj bele, hogy a te kodod majd hogy kezd el kinezni, ha mondjuk 3-4 streamet kell kezelni a fuggvenyben, ez a technika rosszul skalazodik.

a test2() egy szofisztikaltabb megoldas, bar meg az is bezarja az eroforrast, akkor is ha nem kellene, mert meg el egy referencia valahol.

Es a "rendes odafigyeles" resze az ervelesed leggyengebb pontja, sajnos ez nem mukodik a gyakorlatban. Eppen maga az rc es a gc azert lett kitalalva, mert a fejlesztok nem tudtak kovetni/eszben tartani, hogy milyen hivatkozasok vannak egy komplex objektumgrafban, ezert kellett valami, ami ezt megteszi helyettuk. Annyira automatikusan, amennyire lehet.

" hogy egy megosztott eroforras *azonnal* automatikusan bezarul/felszabadul, amint az utolso hivatkozas megszunik ra."
Es akkor itt kezdünk el két dolgot keverni. Az egyik a memóriafelszabadítás, a másik az erőforrásfelszabadítás.
A GC a memóriafelszabadításért felel.
Az erőforrásfelszabadítás ettől részben független:
Egy objektum által foglalt erőforrást akkor is felszabadíthatsz, amikor magát a memóriát nem szabadítod fel, erre való az AutoCloseable, IDisposable illetve a manuális erőforrásfelszabadítás (close).
Azaz az erőforrás-felszabadítást nem mindig kell, hogy kövesse memóriafelszabadítás. Mondjuk magát az objektumot újra fel akarod használni, illetve nem akarsz a memóriafelszabadításra várni (mondjuk GC pause), mert nem az a lényeg, csak az erőforrás elengedése.

Viszont ez fordítva nem igaz: az objektum memória alóli felszabadulását mindig meg kell előznie az erőforrás-felszabadításnak.
Mivel C++-ban a memóriafelszabadítás explicit tud lenni, ezért a desktruktoroknak van értelme.
Míg egy GC-t használó nyelvben ez nincs így, de ott akár így is lehetne. Hiszen GC-t használó nyelvnél sincs semmilyen elvi tilalma annak, hogy te explicit memóriafelszabadítást kérj azonnal, csak épp nem szükséges.z

Még egyszer: csak azért, mert C++-ban így van, ne keverjük az erőforrás-felszabadítást meg a memóriafelszabadítást, mert baromira nem ugyanazok, és a sorrendiségi szabályok sem igazak rájuk.
Memóriafelszabadítást mindig meg kell előzzönk erőforrás-felszabadítás (erre valók a C++ destruktorok és az explicit delete, ott így tudták megoldani).
Viszont erőforrás-felszabadítást nem feltétlenül kell mindig kövessen azonnal memóriafelszabadítás, mert mondjuk nem akarod, hogy álljon a thread csak amiatt, mert te visszaadod a memóriát az OS-nek (ezért van IDisposable,AutoClosable, és nincsenek destruktorok a GC nyelvekben).

Válasszuk ketté a resourcek (file, socket, stb.) és a sima objectek kérdését.

Resourcek a Dispose() meghívásakor (vagy a randomnyelv megfelelőjének meghívásakor) felszabadulnak azonnal. Ezzel nincs is probléma. Viszont ugye nem biztos, hogy neked nem fog kelleni a scope elhagyása után a resource (mert mondjuk nyitottál egy hálózati kapcsolatot és azt mondjuk szeretnéd fenntartani a program futása során). Ilyenkor ugye ugyanúgy neked kell kézzel gondoskodni, hogy az bizonyos feltételek esetén lezáródjon. Vélhetőleg ezért oldották meg Java-ban és C#-ban úgy, hogy explicit meg kelljen mondani, hogy mit akarsz Disploseolni és mit nem.

Sima objektek esetén történő refcountolás meg megint érdekes kérdés. Az, hogy ennek nincs költsége, azzal vitatkoznék. Egyrészt minden egyes objektumpéldány esetén fenn kell tartanod plusz memóriahelyet (valószínűleg 4 byte), ahol számolod a referenciák számát. Ezen kívül minden egyes hozzáférésnél/elengedésnél növelni/csökkenteni kell a refcount-ot. Na most ez use-case-től függően akár költségesebb is lehet, mint egy mark&sweep GC. Arról nem beszélve, hogy a GC sokkal általánosabb és több esetet fed le.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

A runtime cost-rol tenyleg nem nyitnek vitat, mert az nem egyertelmu dolog. El tudok kepzelni olyan scenariot, ahol a gc-nek lehetnek elonyei. Annyit tudnek a c++ mentsegere hozni, hogy az rc opcionalis. Egyszeru objektumoknal a stacken ertekkent vagy unique_ptr-en keresztul tarolva nagyjabol zero az overhead.

Igen fontos a szetvalasztas, es ez volt vegig a mondanivalomnak a lenyege:

A Dispose() explicit meghivasanak szuksegessege. Szerintem egy gyakori es fontos use case (scope-lifetime resource) van viszonylag error-prone modon megoldva Java/C#-ban. Szerintem a RAII/rc itt egy jobb alternativa.

Es asszem a zavar onnan jott, hogy az en hibas fogalmazasom miatt az jott le, hogy en resource-ot akarok felszabaditani gc-vel. En azt probaltam kifejteni, hogy az rc+raii idiom jobb, mint a Disposable/AutoCloseable idiom (flexibilisebb, tomorebb, nehezebben elbaszhato).

Köszi a választ. Elismerem, hogy a véleményem (gc>rc) szubjektív. Végül is a glib sok programban egész jól működik. De vesd össze ezt a két idézetet:

Tőlem: "Még nagyobb baj, hogy a referenciaszámláló módosítgatása sok esetben az alkalmazási program részévé válik, és nem világos, hogy kinek, és milyen időzítéssel kell elvégezni a módosítást."

Tőled: "minden referenciaszamlalt nyelvben letezik a gyenge referencia fogalma (pl. c++-ban a std::weak_ptr), amivel ezt a jelenseget meg lehet kerulni, vagy valaszthatja a programozo, hogy a kort "explicit" modon szakitja meg (valamit nullptr-re allit)"

Hát épp ezt mondom. Az mc-ben valahogy nincs eltalálva, hogyan kell módosítgatni a számlálót.

A belebotlásról: Sok éve élek együtt az mc hibáival. A felületességről és könyv eldobásról: Nem csak könyvből tanultam, hanem implementáltam egyiket is másikat is. A szemétgyűjtésben futtatott destruktorról: Rosszul kitalált dolognak tartom, ideértve a Jávát is.

--
ulysses.co.hu

Ezert probaltam azt megpenditeni, hogy inkabb az a problema, hogy a c nyelv limitacioi miatt nem lehet olyan mechanizmust csinalni, mint amit a c++-ban a smart-pointerekkel mar meg lehet oldani, hogy a referenciak kezelese tobbnyire automatikus es biztonsagos legyen.

Azert irtam, hogy tobbnyire, mert ugye a korok problemaja attol meg letezik. Csak erre irtam peldakent, hogy az eppenseggel nem teljesen problemamentes gc nyelvekben sem (nemdeterminisztikus felszabaditasi sorrend es az explicit using szuksegessege miatt).

Nem a memoriakezelesi modszer hibas az mc esetben, hanem programozoi hiba + hianyzo kepessegek a programozasi nyelvbol.

Mind a 20 gép azonos környezet? Ugyanaz a disztro, ugyanaz a verzió?
Én használom Fedora workstation és szerver kiadáson, Gentoon, Ubuntu szerveren, s még egyszer nem találkoztam ilyen elszállással. Szerintem átok ül rajtad :)

Különféle Ubuntuk, Debian, Raspbian, sok mc verzió. Tudom, hogy vannak, akik szerint, ez elképzelhetetlen, éppen ezért említettem, hogy a fórumokon lehet találni másokat is, akik észlelik a hibát. És hát az elszállás helye (g_free) elég erősen valószínűsíti, hogy programozási hibá(k)ról van szó.
--
ulysses.co.hu

Akkor bocsánat, én kérek elnézést: azt hittem, te a probléma megoldásában vagy érdekelt, de nyilván tévedtem...

Szerk: nem, nem a g_free(NULL) elkerüléséről szól a patch (bár igen, az is benne van: ez olyan, mint a defenzív vezetés: abból biztos nem lesz baj, ha nem bízunk túlságosan másokban;).

Szerk2: esetleg próbáltad efence-cel futtatni?

Igazából mindig jót röhögök azon, mikor jönnek egyesek, hogy szar GC-s nyelvek, bezzeg a C, mert ott milyen jó programokat lehet írni, aztán előbb-utóbb mindig kiderül, hogy a gyakorlatban valami GC pótlékot próbálnak újra és újra leimplementálni, persze szarul.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

Ez most rant akart lenni arról hogy milyen szar a reference counting, vagy egy konkrét mc bugról panaszkodás? :)

> Egy helyen a Fejlesztő Úr azt válaszolta, hogy ő többezer fájlon tesztelte sikeresen az F8-as törlést, és szerinte a program jó.

Link?

Asszem elég nyilvánvaló, hogy ha csak a felhasználók 1%-ánál jelentkezne ez a hiba, az már elég súlyos lenne ahhoz, hogy a fejlesztők levadásszák és kijavítsák. Szóval valószínűleg még annál is sokkal kevesebb felhasználónál jelentkezik. Lehet, hogy nálad van valami olyan speciális körülmény (konfig opció, rendszerszintű beállítás, vagy teszem azt távoli fájlrendszer hibás szerverrel), ami ezt előhozza.

> Mi a tanulság. A gobject rendszer referenciaszámlálós szemétgyűjtést használ.[...]

Tudsz gdb-t használni, forráskódot olvasni. Panaszkodás helyett hasznos volna, ha a hiba tényleges levadászásában tudnál segíteni, előre is köszi!

Ha szabad a fa**méregetést megszakítani egy apró kérdéssel, hol lehetne azt írásban látni, hogy g_malloc/g_free referenciaszámolást végez? Egy glib-2.16.6 forrása van előttem, abban is a glib/gmem.c, de nem vélek ilyesmit látni benne.

Pont ez a baj, hogy az mc fejlesztői hülyék: g_free-t használnak olyan helyen, amelyik objektum már fel lett szabadítva.
Lásd az OP: "A program g_free-vel meg akar szüntetni egy olyan objektumot, amelynek (vagy egy beágyazott objektumának) a refszámlálója korábban (tévesen) már le volt csökkentve. "
A g_object rendszer használ referenciaszámlálást, a g_free alacsonyszintű dolog, a g_objectet kellene használni, csak mc-ék nem tették.

> Ebből annyi látszik, hogy a widget-common.c modul 103. sorában meghívott release_hotkey

A sorszám alapján a több mint 2 éves 4.8.11-es (vagy annál is régebbi) verzióról van szó. Gondolom a glib se újabb. A legújabb mc is elhasal?

subscribe, tetszik Andrei hozzáállása a szakmai vitához

mindennap használok mc-t:
archon 4.8.15
debianon 4.8.3, 4.8.13

soha emiatt ki nem crashelt. Volt valami find in files regexp-es crash, de azt azota megoldották.