folyamok és fájlok. Mi a legegyszerűbb módja ennek

Miután megvizsgáltuk a tömörítési, egyesítési, gyorsítótárazási és párhuzamos kapcsolatok létrehozásának módszereit, érdemes megfontolni a következő kérdést: Az oldal melyik részét töltsük be a fő HTML fájllal, és melyik részét csak külső fájlokkal. ?

Egyetlen oldal formájában összeállítottunk egy tesztkörnyezetet, amelyhez különféle optimalizálási technikákat alkalmaztunk (egyúttal valódi gyorsulást kaptunk egy tetszőleges oldal betöltésére, és megmutattuk, hogy mindezek a technikák valójában hogyan befolyásolják az oldal betöltési sebességét ).

Ezen túlmenően elméleti számításokat is végeztünk az optimális terheléseloszlás szakaszonkénti meghatározására, minden szempont figyelembevételével.

Valós helyzet

Rizs. 29. A (változatlan) WebHiTech.ru webhely betöltési diagramja

A letöltési folyamat variációjának fő ötlete az volt, hogy minimális számú "fehér szóközt" hozzon létre a letöltési diagramon. ábrából látható. Ahogy a 29. ábra is mutatja, az oldalletöltés kb. 80%-át a tétlen kapcsolatok teszik ki (természetesen ez a grafikon nem tükrözi a böngészőben megnyitott letöltési csatornák tényleges terhelését, azonban a kép tisztázásával gyakorlatilag nem változás). A párhuzamos letöltések csak a szűk keresztmetszet áthaladása után indulnak el, ami (jelen esetben) az oldal előtöltése után – a CSS fájl után – ér véget.

A letöltési sebesség optimalizálása érdekében csökkenteni kell a párhuzamosan letöltődő fájlok számát (függőleges nyilak), és amennyire csak lehetséges, balra kell "eltolnunk" (vízszintes nyíl). A "fehér terület" csökkentése (valójában a letöltési csatornák leállási idejének csökkentése) elméletileg növeli a letöltési sebességet a párhuzamosítás miatt. Nézzük meg, hogy ez igaz-e, és hogyan érhetjük el.

Első lépés: egy egyszerű oldal

Először egy normál oldal készült, amelyhez csak a HTML fájl gzip tömörítését használtuk. Ez a legegyszerűbb dolog, amit meg lehet tenni az oldalbetöltés felgyorsítására. Ezt az optimalizálást vették alapul minden más összehasonlításához. A tesztekhez a WebHiTech verseny főoldalát (http://webhitech.ru/) kis számú kiegészítő képpel készítettük el (hogy több külső objektum legyen, és megnőtt az oldal mérete).

Az oldal legelején (head) mérik a kezdő időpontot, és a window.onload esemény (megjegyzendő, hogy csak ez által, mert csak ez garantálja, hogy a teljes oldal a kliens böngészőben van) a végső időpont, akkor a különbözetet kiszámítjuk. De ez egy nagyon egyszerű példa, térjünk át a következő lépésekre.

Második lépés: Képek átméretezése

Először is minimalizáljuk az összes forrásképet (a főbb alkalmazott technikákat a második fejezetben már tárgyaltuk). Egészen viccesre sikerült: a teljes oldalméret 8%-kal csökkent, a letöltési sebesség pedig 8%-kal nőtt (vagyis arányos gyorsulást kaptunk).

Ezenkívül a képek minimalizálásával csökkentették a stíluslapot (a CSS Tidy-n keresztül) és magát a HTML-fájlt (a felesleges szóközöket és sortöréseket eltávolították). Nem voltak szkriptek az oldalon, így a teljes betöltési idő nem sokat változott. De ez még nem a vége, és továbblépünk a harmadik lépésre.

Harmadik lépés: minden az egyben

Használhatja a data:URI-t, és beágyazhatja az összes képet a megfelelő HTML/CSS-fájlokba, így további 15%-kal csökkentheti az oldal méretét (gzip-tömörítéssel, főleg azért, mert a stíluslapot korábban nem tömörítették), de a betöltési idő alatt. mindössze 4%-kal csökkent (a gyorsítótárazás bekapcsolásával csökkent a 304-es választ tartalmazó kérések száma). Az oldal első betöltésekor sokkal stabilabbak a fejlesztések: 20%.

A CSS fájl természetesen a HTML-ben is benne volt, így a teljes oldal betöltésekor csak egy kérés érkezett a szerverhez (a teljes oldal megjelenítésére pár tucat objektummal).

Negyedik lépés: vágja le a patakot

Megpróbálhatja felosztani az eredeti monolitikus fájlt több (5-10) egyenlő részre, amelyeket azután összeállítanak, és közvetlenül a document.body.innerHTML-be fecskendeznek be. Azok. maga a kezdeti HTML fájl nagyon kicsi (sőt, csak egy előtöltőt tartalmaz), és nagyon gyorsan betöltődik, és ezt követően elindul a párhuzamosan több azonos fájl betöltése, amelyek a lehető legsűrűbben használják a letöltési csatornát.

A tanulmányok azonban kimutatták, hogy az XHR kérések és az innerHTML összeállítása az ügyfélen messze meghaladja az ilyen párhuzamosításból származó előnyöket. Ennek eredményeként az oldal 2-5-ször hosszabb ideig töltődik be, miközben a méret nem változik sokat.

Megpróbálhat klasszikus iframe-eket használni XHR-kérések helyett, hogy elkerülje a többletköltséget. Segít, de nem sokat. Az oldal továbbra is 2-3-szor hosszabb ideig töltődik be, mint szeretnénk.

És egy kicsit a keretek használatáról: nagyon gyakran az oldal leggyakrabban használt részei készülnek rajtuk a továbbított adatok méretének csökkentése érdekében. Ahogy fentebb említettük, a legtöbb késés az oldalon lévő külső objektumok nagy számából, nem pedig a külső objektumok méretéből adódik. Ezért jelenleg ez a technológia messze nem olyan releváns, mint a múlt század 90-es éveiben.

Érdemes megemlíteni azt is, hogy iframe használatakor a webhely navigációjához éppen ennek a navigációnak a frissítése okoz gondot (például ha valamelyik menüpontot aktívként szeretnénk kiemelni). A probléma helyes megoldása megköveteli, hogy a felhasználó engedélyezze a JavaScriptet, és ez technikai szempontból meglehetősen nem triviális. Általánosságban elmondható, hogy ha egy webhely tervezésekor megteheti keretek nélkül, akkor nem kell használnia őket.

Ötödik lépés: Algoritmikus gyorsítótár

Miután az első három lépéssel elemeztük a helyzetet, azt látjuk, hogy a gyorsulás egy része elérhető, ha lehetővé tesszük, hogy a böngésző külön objektumként töltsön be külső fájlokat, és ne JSON-kódként, amit valahogyan át kell alakítani. Ezen kívül felbukkannak a gyorsítótárazás szempontjai: elvégre gyorsabban tölti be az oldal felét, a második felét pedig 304-es állapotkódú kérésekkel ellenőrizzük, hogy az objektumok nem változtak-e. A teljes oldal kliens általi első betöltése ebben az esetben kissé lassabb lesz (természetesen az ebben a kérdésben hozott döntés az erőforrás rendszeres felhasználóinak számától függ).

Ennek köszönhetően a betöltési idő további 5%-kal csökkenthető, a végső gyorsulás (teljes gyorsítótár esetén) elérte a 20%-ot, míg az oldalméret 21%-kal csökkent. A külső objektumok betöltésére az oldalméret legfeljebb 50% -át lehet átvinni, miközben az objektumok körülbelül azonos méretűek legyenek (az eltérés legfeljebb 20%). Ebben az esetben a teljes gyorsítótárral rendelkező felhasználók oldalbetöltési sebessége lesz a legmagasabb. Ha az oldal üres gyorsítótárral rendelkező felhasználók számára van optimalizálva, akkor a legjobb eredmény csak akkor érhető el, ha az összes külső fájl szerepel az eredeti HTML-ben.

Döntő asztal

Az alábbiakban látható az összes optimalizálási eredmény egyetlen oldal elkészítéséhez. A letöltést 100 Kb/s-os kapcsolaton teszteltük, teljes szám eredeti tárgyak: 23.

Lépésszám

Leírás

Teljes méret (kb)

Letöltési idő (ms)

1 Normál oldal. Semmi sincs tömörítve (csak a html jelenik meg a gzip-en keresztül) 63 117
2 A HTML/CSS fájlok és képek kicsinyítettek 58 108
3 Egyetlen fájl. A képek az adatokon keresztül vannak beszúrva: URI 49 104
4 A HTML fájl 6 adatot tölt be párhuzamosan és összegyűjti a kliensen 49 233
4.5 A HTML fájl 4 iframe-et tölt be 49 205
5 3. lehetőség: csak a JPEG képeket (megközelítőleg azonos méretű) a rendszer fájlba helyezi és az oldal fejlécében található (new Image()).src-n keresztül tölti be. 49 98

5. táblázat Különböző módokon objektumok párhuzamos betöltése az oldalon

Hat lépés: A terhelési szakaszok kiegyensúlyozása

Tehát hogyan tudjuk a legjobban egyensúlyba hozni az oldalbetöltést a szakaszok között? Hol van az "arany középút", amely biztosítja az optimális terhelést? Kezdjük azzal a feltételezéssel, hogy az adatmennyiség csökkentésére vonatkozó összes tippet már teljesítettük. Ez mindig megtehető, nagyon egyszerű (a legtöbb esetben csak kisebb változtatásokra van szükség a szerver konfigurációjában). Tételezzük fel azt is, hogy a statikus üzenet már gyorsítótárazott fejlécekkel van kiszolgálva (304 válasz visszaadásához, ha az erőforrásfájl fizikailag nem változott az utolsó látogatás óta).

Mi a következő lépés? A további műveletek a külső fájlok szerkezetétől függenek. Nagy (kettőnél több) számú fájllal oldalon kell egyesítenie a stílusfájlokat és a szkriptfájlokat. Az oldal előtöltésének felgyorsulása nyilvánvaló lesz.

Ha a szkriptek mennyisége még tömörítés után is elég nagy (több mint 10 Kb), akkor érdemes bezárás előtt beépíteni őket, vagy általában betöltődik a kombinált esemény ablakán.onload (a hetedik fejezet eleje a szkriptek dinamikus betöltésének van szentelve). Itt tulajdonképpen a letöltés egy részét átvisszük a második szakaszból a negyedikbe, csak a „vizuális” oldalbetöltés gyorsul.

A képek teljes számának minimálisnak kell lennie. Azonban itt is nagyon fontos, hogy térfogatukat egyenletesen osszák el a töltés harmadik szakaszában. Gyakran előfordul, hogy egy 50-100 KB-os kép lelassítja a letöltés befejezését, 3-4 részre bontása pedig felgyorsíthatja a teljes folyamatot. Ezért, ha nagyszámú háttérképet használ, jobb, ha 10-20-as blokkokra bontja őket, amelyek párhuzamosan töltődnek be.

Hetedik lépés: A gyorsítótár kiegyensúlyozása

Ha ennek ellenére a harmadik szakaszban 10-nél több külső objektum van az oldalon (képek és különféle multimédiás fájlok), akkor már érdemes egy további gazdagépet bevezetni a párhuzamos streamek számának növelésére. Ebben az esetben a DNS-lekérdezés költsége megtérül azáltal, hogy csökkenti az átlagos kapcsolatlétesítési időt. 20 objektum után 3 gazdagépet kell megadni, és így tovább. Összesen nem több, mint 4 (amint azt a tanulmány kimutatta munkacsoport Jehu! 4 host után a rezsi nagyobb valószínűséggel nő, mint csökken).

Nagyon egyszerűen megoldható az a kérdés, hogy mennyit kell beletenni az oldalból a HTML-fájlba (CSS, JavaScript vagy data:URI formátumú kód), és mennyit kell hagyni a külső objektumokon. Az egyenleg ebben az esetben megközelítőleg megegyezik az állandó és az egyszeri látogatások számának arányával. Például, ha a felhasználók 70%-a a hét folyamán keresi fel az oldalt, akkor az oldal körülbelül 70%-a kell külső objektumokban, és csak 30%-a a HTML-dokumentumban.

Ha egy oldalt csak egyszer kell látni, érdemes mindent magában az oldalon szerepeltetni. Itt azonban a pszichológiai szempontok is szerepet játszanak. Ha egy átlagos felhasználó oldalának betöltése több mint 3-4 másodpercig tart (figyelembe véve a DNS-lekérdezés és a szerverhez való csatlakozás idejét), akkor két részre kell osztani: a kezdeti verzióra, amely elég gyorsan megjelenik, és az oldal többi része.

Nagyon fontos megérteni, hogy melyik betöltési szakasz van optimalizálva, és mit lát a valódi felhasználó (tiszta gyorsítótárral és esetleg lassú csatornával). Az oldalbetöltési folyamat konkrét példákon történő elemzéséről a nyolcadik fejezetben olvashat bővebben.

Következtetés

Tehát egy normál oldal példáját használva (már elég jól sikerült, érdemes megjegyezni), további 15-20% -kal gyorsítottuk a betöltődését (és ez anélkül, hogy figyelembe vesszük a gzip tömörítést a HTML-hez, ami ebben az esetben a teljes sebesség körülbelül 10%-át adja ). A legfontosabb módszereket már fentebb megadtuk, egyelőre csak annyit lehet megemlíteni, hogy az oldalsebesség optimalizálásakor jobb, ha mindig a böngésző belső mechanizmusaira hagyatkozunk, és nem próbáljuk meg azokat JavaScriptben emulálni (jelen esetben a patak mesterséges „szeleteléséről” beszélünk). Talán a jövőben az ügyfélgépek elég erősek lesznek (vagy a JavaScript-motorok jobban optimalizálva lesznek) ahhoz, hogy az ilyen módszerek működjenek. Most már csak egy választás van - az algoritmikus gyorsítótár.

A probléma lényege.

Egy ponton a futó alkalmazás elkezdi aktívan betölteni a CPU-t, a tesztelő felhív, és kéri a javítást!

Mik a programozók szokásos tevékenységei ebben az esetben?

  • Kérik, hogy lehetőség szerint lokalizálják, majd idő kérdése megoldják a problémát.
  • Megkezdődik a naplók, áthaladásszámlálók és hasonlók hozzáadása. Mindent megadnak a tesztelőnek vagy az ügyfélnek azzal a feltétellel, hogy reprodukálja és küldje vissza a naplót elemzésre. Nos, ha sikerül reprodukálni, és minden világossá válik.
  • Tegyük fel azt az időt, amikor "minden működött", és keresse meg a lehetséges okokat a verziókezelő rendszer változásai alapján.


Hogyan egyszerűbb mit kell tenni ebben az esetben?

azt jelenti, hogy egyes adatfeldolgozó szálak felébredtek/elindultak, és elkezdték aktívan ellátni a dolgukat, vagy néha egyszerűen elakadtak egy hurokban. Miután a betöltéskor megtanulta a végrehajtási veremet, megteheti magas arányban valószínűleg megérti ennek a viselkedésnek az okát.

Honnan tudhatod, mert nem vagyunk a hibakereső alatt?Személy szerint én használom a segédprogramot Process Explorer lehetőséget adva a szálak listájának és veremük megtekintéséhez . A telepítő nem igényel.

A szemléltetés kedvéért az alkalmazásomat a folyamatnévvel indítottam Qocr.Application.Wpf.exe", amiben tette hozzá hamisítvány végtelen ciklusú kód. Most keressük meg a rendszermag hibakereső nélküli betöltésének okát. Ehhez a folyamat tulajdonságaihoz megyek , További:

  1. Ugrás a lapra Szálakés azt látjuk, hogy van 1 adatfolyam, amely 16%-kal töltődik be CPU.
  2. Válassza ki ezt a szálat, és kattintson Kazal, a „Stack for thread ID".
  3. Az ablakban látjuk, hogy itt jött létre a szálunk Qocr.Application.Wpf.exe!<>c. b__36_1+0x3a, és jelenleg hív GetDirectories módszerből InitLanguages().

A fenti műveleteket a képen mutatom be nyilakkal:

A program forráskódjának megnyitásával és a metódusra lépve InitLanguages láthatod a hamis kódomat. Ezen információk, nevezetesen a megállóhely ismeretében már intézkedhet.

A veremkód (a fenti példából), amely végtelen hurkot okoz (ellenőrizhető):

Privát void InitLanguages() ( new Thread (() => ( while (true ) ( var dir = Directory .GetDirectories(@"C:\" ); ) ; )).Start(); )

Fly a kenőcs egy hordó mézes.

Két dolgot kell tudnia, ha a fenti módszer alkalmazása mellett dönt:
  1. Szálak létrehozva CLR(kódban készült .HÁLÓ alkalmazások) ne folytassa a végrehajtást a leállítás után. Ennek eredményeként a szál leáll, és a program újraindításáig lógva marad.
  2. Ha a végrehajtási verem nem tartalmazza hasznos információ, akkor érdemes megállni és többször megnézni a verem. Nagyon nagy a valószínűsége annak, hogy belebotlik egy hurokpontba.
  • Fordítás
  • oktatóanyag

A fordítótól: ez a cikk a hetedik az SFML-könyvtár hivatalos útmutatójának fordításai sorozatában. Az előző cikk megtalálható Ez a cikksorozat célja, hogy az eredeti nyelvet nem ismerők számára lehetőséget biztosítson a könyvtár megismerésére. Az SFML egy egyszerű és többplatformos multimédiás könyvtár. Az SFML egyszerű felületet biztosít játékok és egyéb multimédiás alkalmazások fejlesztéséhez. Az eredeti cikk megtalálható. Kezdjük.

Mi az a patak?

A legtöbben már tudják, mi az a stream, de elmagyarázzuk azoknak, akik még nem ismerik ezt a témát.

A szál lényegében olyan utasítások sorozata, amelyek párhuzamosan futnak más szálakkal. Minden program létrehoz legalább egy szálat: a főszálat, amely a main() függvényt futtatja. A csak a főszálat használó program egyszálú; ha egy vagy több szálat ad hozzá, akkor többszálas lesz.

Tehát röviden: a szálak egy módja annak, hogy egyszerre több dolgot is elvégezzünk. Ez hasznos lehet például animációk megjelenítéséhez és a felhasználói bevitel feldolgozásához képek vagy hangok betöltése közben. A szálakat a hálózati programozásban is széles körben használják, miközben az adatok beérkezésére várnak, az alkalmazás folyamatosan frissíti és rajzol.

SFML adatfolyamok vagy std::szál?

Legújabb verziójában (2011) a C++ Standard Library egy sor osztályt biztosít a szálakkal való munkavégzéshez. Az SFML írásakor még nem írták meg a C++11 szabványt, és nem volt szabványos módszer a szálak létrehozására. Amikor megjelent az SFML 2.0, sok fordító nem támogatta ezt új szabvány.

Ha olyan fordítóval dolgozik, amely támogatja az új szabványt és fejlécfájlt tartalmaz, felejtse el az SFML adatfolyam osztályokat, és használja helyette a szabványos C++ osztályokat. De ha olyan fordítóval dolgozik, amely nem támogatja ezt a szabványt, vagy ha a kód terjesztését tervezi, és teljes hordozhatóságot szeretne elérni, az SFML streaming osztályok jó választás.

Szálak létrehozása SFML-lel

Elég a beszédből, vessünk egy pillantást a kódra. Az SFML használatával szálak létrehozását lehetővé tevő osztályt sf::Threadnak hívják, és így néz ki (szál létrehozása) működés közben:

#beleértve #beleértve void func() ( // ez a függvény akkor fut le, amikor a thread.launch() meghívásra kerül (int i = 0; i< 10; ++i) std::cout << "I"m thread number one" << std::endl; } int main() { // создание потока с функцией func в качестве точки входа sf::Thread thread(&func); // запуск потока thread.launch(); // главные поток продолжает быть запущенным... for (int i = 0; i < 10; ++i) std::cout << "I"m the main thread" << std::endl; return 0; }
Ebben a kódban a main és a func függvények párhuzamosan futnak a thread.launch() meghívása után. Ennek az az eredménye, hogy mindkét függvény szövegkimenete összekeveredik a konzolban.

Stream belépési pont, azaz a szál indításakor végrehajtandó függvényt át kell adni az sf::Thread konstruktornak. Az sf::Thread igyekszik rugalmas lenni és különböző belépési pontokat fogadni: nem tag függvények vagy osztálymetódusok, függvények argumentumokkal vagy anélkül, függvények stb. A fenti példa bemutatja a tagfüggvény használatát, íme néhány további példa.

  • nem tag függvény egy argumentummal:

    void func(int x) ( ) sf::Szálszál(&func, 5);

  • osztály módszer:

    Class MyClass ( publikus: void func() ( ) ); MyClass objektum; sf::Szálszál(&MyClass::func, &object);

  • funktor (függvényobjektum):

    Struct MyFunctor ( void operator()() ( ) ); sf::Szálszál(MyFunctor());

Az utolsó, funktort használó példa a legerősebb, mert bármilyen típusú funktort képes elfogadni, és ezért az sf::Thread osztályt számos olyan funkcióval kompatibilissé teszi, amelyek közvetlenül nem támogatottak. Ez a funkció különösen érdekes a C++11 vagy std::bind lambda kifejezéseknél.

// lambda függvénnyel sf::Thread thread(()( std::cout<< "I am in thread!" << std::endl; });
// with std::bind void func(std::string, int, double) ( ) sf::Thread thread(std::bind(&func, "hello", 24, 0.5));
Ha egy osztályon belül szeretné használni az sf::Threadot, ne feledje, hogy nincs alapértelmezett konstruktora. Ezért inicializálnia kell az osztálykonstruktorban az inicializálási listában:

ClassWithThread ( public: ClassWithThread() : m_thread(&ClassWithThread::f, this) ( ) private: void f() ( ... ) sf::Thread m_thread; );
Ha valóban példányosítani kell az sf::Thread-et az objektum inicializálása után, akkor létrehozhatja a kupacban.

Egy szál indítása

Miután példányosította az sf::Thread fájlt, el kell indítania egy futtató függvénnyel.

Sf::Szálszál(&func); thread.launch();
A launch meghívja az új szál konstruktorának átadott függvényt, és azonnal kilép, így a hívó szál azonnal folytathatja a végrehajtást.

Szálak leállítása

A szál automatikusan kilép, amikor a szál belépési pontjaként szolgáló függvény visszaadja az értékét. Ha meg akarja várni, hogy egy szál egy másik szálból befejeződjön, akkor meghívhatja a várakozás funkcióját.

Sf::Szálszál(&func); // szál indítása thread.launch(); ... // végrehajtási blokkok a szál befejezéséig thread.wait();
A várakozás függvényt implicit módon az sf::Thread destruktora is meghívja, így a szál nem maradhat futva (és felügyelet nélkül), miután az sf::Thread példánya megsemmisült. Tartsa ezt szem előtt a szálak kezelésekor (lásd a cikk előző részét).

Egy szál felfüggesztése

Az SFML-ben nincs olyan funkció, amely módot adna egy szál szüneteltetésére; A szál szüneteltetésének egyetlen módja az, hogy ezt magának a szálnak a kódjából kell megtenni. Más szóval, csak az aktuális szálat szüneteltetheti. Ehhez meghívhatja az sf::sleep függvényt:

Void func() ( ... sf::sleep(sf::milliseconds(10)); ... )
sf::sleep egy argumentumot vesz igénybe, az alvásidőt. Ez az idő tetszőleges mértékegységben kifejezhető, ahogy az a cikkben is látható.

Vegye figyelembe, hogy ezzel a funkcióval bármilyen szálat szüneteltethet, még a főszálat is.

Az Sf::sleep a szál felfüggesztésének leghatékonyabb módja: a szál felfüggesztésének időtartama alatt ez (a szál) szinte semmilyen CPU-erőforrást nem fogyaszt. A várakozáson alapuló szünet, mint egy üres while ciklus, a CPU 100%-át fogyasztja, és nem csinál... semmit. Ne feledje azonban, hogy a felfüggesztés időtartama csak tipp; a szünet tényleges hossza (több vagy kevesebb, mint a megadott idő) az operációs rendszertől függ. Tehát ne hagyatkozzon erre a funkcióra a nagyon pontos időzítés érdekében.

A megosztott adatok védelme

A program minden szála közös memóriával rendelkezik, és hozzáférnek a hatókörükben található összes változóhoz. Ez nagyon kényelmes, de veszélyes is: attól a pillanattól kezdve, hogy egy szál párhuzamosan indul, a változókat vagy függvényeket egyidejűleg különböző szálak használhatják. Ha a művelet nem szálbiztos, meghatározatlan viselkedést eredményezhet (azaz összeomolhat vagy megsérülhet az adatok).

Számos szoftvereszköz létezik, amelyek segíthetnek a megosztott adatok védelmében és a kódszálak biztonságossá tételében. Ezeket szinkronizálási primitíveknek nevezzük. A leggyakoribb primitívek a mutexek, a szemaforok, a feltételváltozók és a spinlockok. Mindegyik ugyanannak a koncepciónak a változata: egy kódrészletet védenek azzal, hogy csak egy szálnak adnak hozzáférést az adatokhoz, a többit pedig blokkolják.

A leggyakoribb (és használt) primitív a mutex. A Mutex a kölcsönös kizárást jelenti. Ez garancia arra, hogy csak egy szál tudja végrehajtani a kódot. Nézzük meg, hogyan működnek a mutexek az alábbi példában:

#beleértve #beleértve sf::Mutex mutex; void func() ( mutex.lock(); for (int i = 0; i< 10; ++i) std::cout << "I"m thread number one" << std::endl; mutex.unlock(); } int main() { sf::Thread thread(&func); thread.launch(); mutex.lock(); for (int i = 0; i < 10; ++i) std::cout << "I"m the main thread" << std::endl; mutex.unlock(); return 0; }
Ez a kód megosztott erőforrást (std::cout) használ, és amint látjuk, ez nemkívánatos eredményekhez vezet. A streamek kimenete össze van keverve a konzolban. Annak érdekében, hogy a kimenet helyesen kerüljön kinyomtatásra, ahelyett, hogy összekevernénk, mutex-el védjük a kód megfelelő területeit.

Az első szál, amely eléri a mutex.lock() hívást, zárolja a mutexet, és hozzáfér a szöveget kinyomtató kódhoz. Amikor más szálak elérik a mutex.lock() hívást, a mutex már zárolva van, és a többi szál leállítja a végrehajtást (hasonlóan az sf::sleep hívásához, az alvó szál nem fogyaszt CPU-időt). Amikor az első szál feloldja a mutex zárolását, a második szál folytatja a végrehajtást, zárolja a mutexet, és kinyomtatja a szöveget. Ez azt eredményezi, hogy a konzol szövege következetesen kerül kinyomtatásra, és nem keveredik.

A mutex nem csak egy primitív, amelyet a megosztott adatok védelmére használhat, hanem sok más módon is felhasználhatja. Ha azonban az alkalmazás trükkös dolgokat művel a szálfűzéssel, és úgy érzi, hogy a mutexek ereje nem elegendő, keressen egy másik, több funkcióval rendelkező könyvtárat.

Mutex védelem

Ne aggódjon: a mutexek már szálbiztosak, nem kell védeni őket. De nem kivételesek. Mi történik, ha kivételt adnak, miközben a mutex zárolva van? Soha nem lehet feloldani, és örökre zárva marad. Minden szál, amely megpróbálja feloldani a zárolt mutex zárolását, örökre zárolva lesz. Egyes esetekben az alkalmazás "lefagy".

Annak biztosítására, hogy a mutex mindig feloldva legyen olyan környezetben, ahol (a mutex) kivételt jelenthet, az SFML egy RAII osztályt biztosít, amely lehetővé teszi a mutex becsomagolását az sf::Lock osztályba. A zárolás a konstruktorban, a feloldás a destruktorban történik. Egyszerű és hatékony.

Sf::Mutex mutex; void func() ( sf::Lock lock(mutex); // mutex.lock() functionThatMightThrowAnException(); // mutex.unlock() ha a függvény kivételt dob ​​) // mutex.unlock()
Ne feledje, hogy az sf::Lock olyan függvényekben is használható, amelyeknek több visszatérési értéke van.

Sf::Mutex mutex; bool func() ( sf::Lock lock(mutex); // mutex.lock() if (!image1.loadFromFile("...")) return false; // mutex.unlock() if (!image2. loadFromFile("...")) return false; // mutex.unlock() if (!image3.loadFromFile("...")) return false; // mutex.unlock() return true; ) // mutex .kinyit()

Gyakori tévhitek

Egy dolog, amit gyakran figyelmen kívül hagynak: egy szál nem létezhet megfelelő példány nélkül

Ez a negyedik cikk a "A Windows határainak feszegetése" sorozatban, amelyben a Windows alapvető erőforrásaira vonatkozó korlátokról beszélek. Ezúttal a Windows által támogatott szálak és folyamatok maximális számának korlátozásáról fogok beszélni. Itt röviden leírom a szál és a folyamat közötti különbséget, a felmérési szál határértékét, majd a folyamatokhoz kapcsolódó korlátokról lesz szó. Mindenekelőtt úgy döntöttem, hogy a szálak korlátairól beszélek, mivel minden aktív folyamatnak van legalább egy szála (az a folyamat, amely kilépett, de amelynek hivatkozása egy másik folyamat által biztosított kezelőben van tárolva, nincs egyetlen szál), így a folyamatkorlátok közvetlenül függenek a mögöttes szálra vonatkozó korlátoktól.

A UNIX egyes változataitól eltérően a legtöbb Windows-erőforrásnak nincs rögzített korlátja az operációs rendszerbe a felépítés idején, hanem az operációs rendszer rendelkezésére álló mögöttes erőforrások alapján korlátozott, amiről korábban már beszéltem. A folyamatok és szálak például fizikai memóriát, virtuális memóriát és poolmemóriát igényelnek, így az adott Windows rendszeren létrehozható folyamatok és szálak számát végső soron ezen erőforrások valamelyike ​​határozza meg, attól függően, hogy ezek a folyamatok, ill. szálak jöttek létre, és az alapul szolgáló erőforráskorlátok közül melyik éri el először. Ezért javaslom, hogy olvassa el korábbi cikkeimet, ha még nem tette meg, mert a továbbiakban olyan fogalmakra fogok hivatkozni, mint a lefoglalt memória, a lefoglalt memória és a rendszermemória-korlát, amelyekről korábbi cikkeimben beszéltem.

Folyamatok és szálak
A Windows-folyamatok lényegében egy olyan tároló, amely egy futtatható fájlból származó parancsok kódját tárolja. Ez egy kernel folyamatobjektum, és a Windows ezt a folyamatobjektumot és a hozzá tartozó adatstruktúrákat használja az alkalmazás futtatható kódjával kapcsolatos információk tárolására és karbantartására. Például egy folyamatnak van egy virtuális címtere, amely tárolja privát és nyilvános adatait, és leképezi a végrehajtható lemezképet és a hozzá tartozó DLL-eket. A Windows diagnosztikai eszközöket használ a folyamat erőforrás-felhasználásával kapcsolatos információk rögzítésére az elszámoláshoz és a lekérdezések végrehajtásához, és regisztrálja a folyamat hivatkozásait az operációs rendszer objektumaira a folyamatleíró táblában. A folyamatok egy biztonsági kontextussal, úgynevezett tokennel működnek, amely azonosítja a felhasználói fiókot, a fiókcsoportokat és a folyamathoz rendelt jogosultságokat.

Egy folyamat egy vagy több szálat tartalmaz, amelyek ténylegesen végrehajtják a kódot a folyamatban (technikailag nem folyamatokat, hanem szálakat), és a rendszerben kernelszál objektumokként jelennek meg. Számos oka lehet annak, hogy az alkalmazások az eredeti kezdeti szálon kívül szálakat hoznak létre: 1) a felhasználói felülettel rendelkező folyamatok általában azért hoznak létre szálakat, hogy elvégezzék a munkájukat, miközben a fő szál reagál a felhasználói bevitellel kapcsolatos parancsokra és az ablakkezelésre; 2) Azok az alkalmazások, amelyek több processzort szeretnének használni a teljesítmény skálázásához, vagy tovább akarnak futni, miközben a szálak tétlenül várnak az I/O szinkronizálására, hozzon létre szálakat a többszálú kezelés előnyeinek kihasználása érdekében.

Szálkorlátok
A szálra vonatkozó alapvető információkon kívül, beleértve a CPU-regiszterek állapotát, a szálhoz rendelt prioritást és a szál erőforrás-felhasználásával kapcsolatos információkat, minden szálhoz hozzá van rendelve a folyamat címterének egy része, ún. a verem, amelyet a szál munkamemóriaként használhat a programkód végrehajtása során, függvényparaméterek átadására, helyi változók és függvényeredmények címeinek tárolására. Így, hogy elkerüljük a rendszer virtuális memóriájának pazarlását, kezdetben a veremnek csak egy része kerül lefoglalásra, vagy egy része átkerül a szálba, a többit pedig egyszerűen lefoglaljuk. Mivel a memóriában lévő veremek lefelé növekszenek, a rendszer a memória úgynevezett "védőoldalait" (az angol védőoldalakból) a verem lefoglalt részén kívül helyezi el, amelyek szükség esetén automatikus kiegészítő memória lefoglalását (az úgynevezett verembővítést) biztosítják. . A következő ábra bemutatja, hogyan mélyül a verem kiosztása, és hogyan mozognak a védőoldalak, amikor a verem bővül egy 32 bites címtérben:

A végrehajtható képek hordozható végrehajtható (PE) struktúrái határozzák meg a lefoglalt és kezdetben lefoglalt címterület mennyiségét a szálak vereméhez. A linker alapértelmezés szerint 1 MB-ot foglal le, és egy oldalt foglal le (4K), de a fejlesztők megváltoztathatják ezeket az értékeket a PE-értékek megváltoztatásával, amikor kommunikálnak a programjukkal, vagy a CreateTread függvény meghívásával egy külön szálon. A Visual Studióhoz tartozó segédprogramok, például a Dumpbin segítségével megtekintheti a végrehajtható programok beállításait. Íme a Dumpbin futtatásának eredménye a /headers opcióval az új Visual Studio projekt által generált végrehajtható fájlon:

A számokat hexadecimálisból konvertálva láthatja, hogy a veremtartalék mérete 1 MB, a lefoglalt memóriaterület pedig 4 Kb; A Sysinternals új, MMap nevű segédprogramjával csatlakozhat ehhez a folyamathoz, és megnézheti annak címterét, és ezáltal megtekintheti a folyamat kezdetben lefoglalt veremmemória oldalát, védőoldalát és a lefoglalt veremmemória többi részét:

Mivel minden szál felhasználja a folyamat címterének egy részét, a folyamatoknak van egy alapkorlátja a létrehozható szálak számára, ami egyenlő a címterük méretével osztva a szál veremének méretével.

A 32 bites adatfolyamok korlátozásai
Még ha a folyamatnak egyáltalán nem volt kódja vagy adata, és a teljes címteret fel lehetne használni a veremekhez, akkor egy 32 bites, 2 bájtos alapértelmezett címterű folyamat legfeljebb 2048 szálat hozhat létre. Íme a Testlimit program, amely 32 bites Windows rendszeren fut a -t (szálak létrehozása) opcióval, megerősítve a korlátozás fennállását:

Ismétlem, mivel a címtér egy részét már felhasználták a kódhoz és a kezdeti kupachoz, így nem állt rendelkezésre mind a 2 GB a szálveremekhez, így a létrehozott szálak száma összesen nem érhette el a 2048 szál elméleti határát.

Megpróbáltam a Testlimit futtatását azzal a kiegészítő lehetőséggel, hogy az alkalmazásnak kiterjesztett címteret adjon, remélve, hogy ha több mint 2 GB címterületet kapott (például 32 bites rendszereken, ezt úgy érheti el, hogy az alkalmazást a /3 GB-tal futtatja. vagy /USERVA opciót a Boot.inihez, vagy az ezzel egyenértékű BCD opciót Vistán és később a largeuserva-n), ezt fogja használni. A 32 bites folyamatoknak 4 GB címterület van lefoglalva, amikor 64 bites Windowson futnak, tehát hány szálat tud létrehozni egy 64 bites Windowson futó 32 bites Testlimit? A már tárgyaltak alapján a válasz 4096 (4 GB osztva 1 MB-tal), de a gyakorlatban ez a szám jóval alacsonyabb. Itt van egy 32 bites Testlimit, amely 64 bites Windows XP rendszeren fut:

Ennek az eltérésnek az oka abban rejlik, hogy amikor egy 32 bites alkalmazást futtat 64 bites Windows rendszeren, az valójában egy 64 bites folyamat, amely 64 bites kódot hajt végre a 32 bites szálak nevében, így a memóriában. minden szálhoz a 64 bites és 32 bites szálveremek számára vannak fenntartva területek. 64 bites verem esetén 256 Kb van fenntartva (a kivételek a Vista előtti operációs rendszerek, amelyekben a 64 bites szálak kezdeti veremmérete 1 Mb). Mivel minden 32 bites szál 64 bites módban indul, és az indításkor lefoglalt verem mérete nagyobb, mint az oldalméret, a legtöbb esetben látni fogja, hogy legalább 16 Kb van lefoglalva egy 64 bites szálveremhez. Íme egy példa egy 32 bites adatfolyam 64 bites és 32 bites veremére (a 32 bites verem "Wow64" címkével rendelkezik):

A 32 bites Testlimit 3204 szálat tudott létrehozni 64 bites Windowson, ami azzal magyarázható, hogy minden szál 1Mb + 256Kb címterületet használ a verem alatt (ez is kivétel a Vista előtti Windows verziók, ahol 1Mb + 1Mb használt). A 32 bites Testlimit 64 bites Windows 7 rendszeren való futtatásával azonban más eredményt kaptam:

A Windows XP és a Windows 7 eredményei közötti különbség a Windows Vista Address Space Layout Randomization (ASLR) véletlenszerűbb jellegéből adódik, ami némi töredezettséget eredményez. A DLL-betöltés, a szálverem és a dinamikus memóriafoglalás véletlenszerűsítése javítja a kártevő elleni védelmet. Amint az a következő VMMap pillanatképen látható, a tesztrendszerben még 357 MB címterület áll rendelkezésre, de a legnagyobb szabad blokk 128 KB, ami kevesebb, mint a 32 bites veremhez szükséges 1 MB:

Mint megjegyeztem, a fejlesztő visszaállíthatja az alapértelmezett veremtartalék méretét. Ennek egyik lehetséges oka lehet, hogy elkerüljük a címterület pazarlását, ha előre ismert, hogy a szálverem mindig kevesebbet fog használni, mint az alapértelmezett 1 MB. A Testlimit PE kép alapértelmezés szerint 64 KB-os veremtartalékot használ, és ha az -n kapcsolót a -t kapcsolóval adjuk meg, a Testlimit 64 KB-os veremekkel hoz létre szálakat. Íme a segédprogram 32 bites Windows XP és 256 MB RAM rendszeren való futtatásának eredménye (ezt a tesztet kifejezetten gyenge rendszeren futtattam le, hogy kiemeljem ezt a korlátozást):

Itt meg kell jegyezni, hogy egy másik hiba történt, ami azt jelenti, hogy ebben a helyzetben az ok nem a címtér. Valójában a 64 Kb-os veremeknek körülbelül 32 000 szálat kell biztosítaniuk (2Gb/64Kb = 32768). Tehát mi a korlátozás ebben az esetben? A lehetséges jelölteket tekintve, beleértve a lefoglalt memóriát és a készletet, nem adnak támpontot a kérdés megválaszolásához, mivel ezek az értékek mindegyike a határérték alatt van:

A választ a kernel hibakereső memóriájával kapcsolatos további információkban találhatjuk meg, amelyek megmondják a keresett korlátot a rendelkezésre álló rezidens memóriára vonatkozóan, amelynek teljes mennyisége kimerült:

A rendelkezésre álló rezidens memória az adatokhoz vagy kódokhoz lefoglalt fizikai memória, amelynek a RAM-ban kell lennie. A lapozatlan készlet és a lapozatlan illesztőprogramok méretét a rendszer egymástól függetlenül számítja ki, valamint például a RAM-ban az I/O műveletekhez fenntartott memóriát. Minden szál rendelkezik mindkét felhasználói módú veremmel, amint azt korábban említettem, de van egy privileged-mode (kernel-mode) verem is, amelyet akkor használnak, amikor a szálak kernel módban működnek, például rendszerhívásokat hajtanak végre. Amikor egy szál aktív, a kernel verem a memóriában van rögzítve, hogy a szál olyan kódot tudjon végrehajtani a kernelben, amelyhez nem hiányozhatnak a szükséges oldalak.

Az alap kernel verem 12 kb 32 bites Windows és 24 kb 64 bites Windows esetén. Az 14225 szál körülbelül 170 MB rezidens memóriát igényel, ami pontosan annyi szabad memória ezen a rendszeren, ahol a Testlimit letiltva:

Amint eléri a rendelkezésre álló rendszermemória korlátját, számos alapvető művelet meghiúsul. Például a következő hibaüzenetet kaptam, amikor duplán kattintottam az Internet Explorer ikonra az asztalon:

Ahogy az várható volt, 64 bites Windows rendszeren, 256 MB RAM-mal futva, a Testlimit 6600 szálat tudott létrehozni – ez a segédprogram körülbelül feleannyi szálat tudott létrehozni 32 bites Windows rendszeren 256 MB RAM-mal – mielőtt a rendelkezésre álló memória kifogyott:

Az ok, amiért korábban használtam az "alap" kernelverem kifejezést, az az, hogy a grafikával és ablakozási függvényekkel dolgozó szál az első híváskor egy "nagy" veremot kap, amely egyenlő (vagy nagyobb) 20 000 bájt/32- bites Windows és 48 Kb 64 bites Windows rendszeren. A Testlimit szálak nem hívják meg ezeket az API-kat, így az alapul szolgáló kernelveremekkel rendelkeznek.
A 64 bites adatfolyamok korlátozásai

A 32 bites adatfolyamokhoz hasonlóan a 64 bites adatfolyamok is alapértelmezés szerint 1 MB-os veremtartalékkal rendelkeznek, de a 64 bites adatfolyamok sokkal több felhasználói címterülettel (8 TB) rendelkeznek, így ez nem jelenthet problémát, ha nagy címet kell létrehozni. szálak száma. Mégis egyértelmű, hogy a rendelkezésre álló rezidens memória továbbra is potenciálkorlátozó. A Testlimit 64 bites verziója (Testlimit64.exe) körülbelül 6600 szálat tudott létrehozni az -n kapcsolóval és anélkül egy 64 bites Windows XP rendszeren és 256 MB RAM-mal, pontosan ugyanannyit, mint a 32 bites verzió létrehozva, mert elérte a rendelkezésre álló rezidens memória korlátját. Egy 2 GB RAM-mal rendelkező rendszeren azonban a Testlimit64 csak 55 000 szálat tudott létrehozni, ami jelentősen kevesebb, mint amennyi szálat tud létrehozni ez a segédprogram, ha a rendelkezésre álló állandó memória a korlát (2 GB/24 Kb = 89 000):

Ebben az esetben az ok egy lefoglalt kezdeti szálverem, ami miatt a rendszer kifogy a virtuális memóriából, és az oldalfájl elégtelen mérete miatt hibát generál. Amint a lefoglalt memória mennyisége eléri a RAM méretét, az új szálak létrehozásának sebessége jelentősen lecsökken, mert a rendszer elkezd "dobni", a korábban létrehozott szál veremeket elkezdik kirakni a swap fájlba, hogy helyet csináljanak az új szálak halmát, és a swap fájlnak növekednie kell. Ha az -n opció engedélyezve van, az eredmények ugyanazok, mert a lefoglalt veremmemória kezdeti mennyisége változatlan marad.

A folyamat korlátai
A Windows által támogatott folyamatok számának nyilvánvalóan kevesebbnek kell lennie, mint a szálak számának, mivel minden folyamatnak egy szála van, és maga a folyamat további erőforrás-felhasználást okoz. A 64 bites Windows XP-vel és 2 GB rendszermemóriával rendelkező rendszeren futó 32 bites Testlimit körülbelül 8400 folyamatot hoz létre:

Ha megnézi a kernel hibakereső eredményét, világossá válik, hogy ebben az esetben elérte a rendelkezésre álló rezidens memória korlátját:

Ha egy folyamat a rendelkezésre álló rezidens memóriát csak a privilegizált módú szálveremhez használná fel, a Testlimit sokkal több mint 8400 szálat tud létrehozni egy 2 GB-os rendszeren. A rendszeren rendelkezésre álló rezidens memória mennyisége a Testlimit futtatása nélkül 1,9 GB:

Ha a Testlimit által felhasznált rezidens memória mennyiségét (1,9 GB) elosztjuk az általa létrehozott folyamatok számával, akkor folyamatonként 230 KB rezidens memóriát kapunk. Mivel a 64 bites kernel stack 24 KB, így körülbelül 206 KB hiányzik minden egyes folyamathoz. Hol van a maradék felhasznált rezidens memória? A folyamat létrehozásakor a Windows elegendő fizikai memóriát foglal le ahhoz, hogy minimális munkalapkészletet biztosítson. Ez annak biztosítására szolgál, hogy a folyamat minden helyzetben elegendő fizikai memóriával rendelkezzen ahhoz, hogy tárolja a minimálisan működő oldalkészlet fenntartásához szükséges adatmennyiséget. Az alapértelmezett munkakészlet mérete gyakran 200 Kb, amely könnyen ellenőrizhető, ha hozzáad egy Minimális munkakészlet oszlopot a Process Explorer ablakhoz:

A fennmaradó 6 kb állandó szabad memória, amely további nem lapozható memóriához van lefoglalva, amely magát a folyamatot tárolja. A 32 bites Windows folyamatai valamivel kevesebb rezidens memóriát használnak, mivel a privilegizált szálverem kisebb.

A felhasználói módú szálveremekhez hasonlóan a folyamatok felülírhatják az alapértelmezett munkalap-méretüket a SetProcessWorkingSetSize függvénnyel. A Testlimit támogatja az -n kapcsolót, amely a -p kapcsolóval kombinálva a fő Testlimit folyamat gyermekfolyamatait 80 Kb-os minimális munkalap-méretre állítja be. Mivel az alárendelt folyamatoknak időre van szükségük, hogy csökkentsék működő oldalkészleteiket, a Testlimit, miután már nem tud folyamatokat létrehozni, felfüggeszti, és megpróbálja folytatni, így esélyt ad az alárendelt folyamatoknak a futtatásra. A 4 GB RAM-mal rendelkező Windows 7 rendszeren az -n opcióval futtatott Testlimit korlátja eltér a rendelkezésre álló állandó memória korláttól – a lefoglalt rendszermemória korláttól:

Az alábbi képernyőképen látható, hogy a kernel hibakeresője nem csak a rendszermemória korlátjának elérését jelzi, hanem azt is, hogy több ezer memóriafoglalási hiba történt, mind a virtuális, mind a memória lefoglalásával a korlát elérése óta. (a lefoglalt rendszermemória korlátját valóban többször is elértük, mivel amikor az oldalfájl méretének hiányával kapcsolatos hiba lépett fel, akkor ez a mennyiség megnőtt, és ezt a határt kitolta):

A Testlimit megjelenése előtt az átlagosan lefoglalt memória 1,5 GB körül volt, tehát a szálak körülbelül 8 GB-ot foglaltak el a lefoglalt memóriából. Ezért mindegyik folyamat körülbelül 8 GB/6600 vagy 1,2 MB-ot fogyasztott. A kernel hibakereső !vm parancsának végrehajtásának eredménye, amely megmutatja a privát memória eloszlását (angolul. Private memory) minden egyes folyamathoz, megerősíti ennek a számításnak a helyességét:

A szál vereméhez lefoglalt memória kezdeti mennyisége, amelyet korábban leírtunk, csekély hatással van a folyamat címterének adatstruktúráihoz, oldaltábla-bejegyzésekhez, leírótáblázathoz, folyamat- és szálobjektumokhoz, valamint a folyamat során létrehozott natív adatokhoz szükséges egyéb memóriakérésekre. az inicializálása.

Hány folyamat és szál lesz elég?
Tehát a válaszok a "hány szálat támogat a Windows?" és "hány folyamatot lehet futtatni egyszerre Windowson?" össze vannak kötve. Eltekintve attól, hogy a szálak hogyan határozzák meg a verem méretét, és a folyamatok határozzák meg a minimális munkalapkészletüket, a két fő tényező, amely ezekre a kérdésekre adható választ egy adott rendszeren, a fizikai memória mennyisége és a lefoglalt rendszermemória korlátja. Mindenesetre, ha egy alkalmazás elegendő szálat vagy folyamatot hoz létre ahhoz, hogy megközelítse ezeket a korlátokat, akkor a fejlesztőjének újra kell terveznie az alkalmazást, mivel mindig különböző módokon lehet elérni ugyanazt az eredményt ésszerű számú folyamattal. Például egy alkalmazás méretezésekor a fő cél az, hogy a futó szálak száma egyenlő maradjon a CPU-k számával, és ennek egyik módja az, hogy a szinkron I/O használatáról áttérünk a befejező portokat használó aszinkronra, ami segít megőrizni a futó szálak száma összhangban a CPU számával.