A holtpont párhuzamosságának és tárgy monitorainak alapjai (1. és 2. szakasz) (a cikk fordítása)
A párhuzamosság alapjai: holtpontok és tárgymonitorok (1. és 2. szakasz) (a cikk fordítása)
Ez a cikk része a Párhuzamos alapismereteknek Java nyelven.
Ebben a kurzusban merüljön el a párhuzamosság mágiájába. Meg fogod tanulni a párhuzamosság és a párhuzamos kód alapjait, ismerkedj meg olyan fogalmakkal, mint atomitás, szinkronizálás, menetbiztonság. Nézze meg itt!
Olyan alkalmazások fejlesztésekor, amelyek a célok elérése érdekében használják a konkurenciát, akkor találkozhat olyan helyzetekkel, amelyekben a különböző szálak blokkolják egymást. Ha ebben az esetben az alkalmazás a vártnál lassabban működik, azt mondanánk, hogy az időben nem működik a várt módon. Ebben a részben jobban megismerhetjük azokat a problémákat, amelyek veszélyeztethetik a többszálú alkalmazások túlélését.
A "holtpont" kifejezés jól ismert a szoftverfejlesztők számára, és a legtöbb közönséges felhasználó időről időre is használja, bár nem mindig a megfelelő értelemben. Szigorúan véve, ez a kifejezés azt jelenti, hogy a két (vagy több) szálakat vár egy másik téma, hogy felszabadította a lezárt forrás, míg az első forrás maga blokkolt, amelyekhez való hozzáférés várja a második:
A probléma jobb megértése érdekében nézzük meg a következő kódot:
Amint a fenti kódból látható, két szál indul el, és két statikus erőforrást próbál meg bezárni. De egy holtpontnál egy másik sorozatra van szükségünk mindkét szálhoz, ezért a Véletlen objektum egy példányát használjuk annak kiválasztásához, hogy a szál melyik forrást akarja elsőként lezárni. Ha a b logikai változó igaz, akkor először az erőforrás 1 blokkolódik, és miután a szál megpróbálja megszerezni a forrást2. Ha b hibás, akkor a szál blokkolja az erőforrást2, majd megpróbálja megragadni az erőforrást1. Ez a program nem tart sokáig az első patthelyzet eléréséhez, azaz. a program örökké lefagy, ha nem szakítjuk meg:
Ebben a futtatásban a tread-1 zárolt erőforrást2 és várja, hogy az erőforrás1 záródjon, míg a tread-2 blokkolja az erőforrást1, és az erőforrás2-re vár.
Ha az értéket egy logikai változó b a fenti kódot IGAZ, nem tudtuk megfigyelni olyan patthelyzet, mert a sorrend, amelyben a futófelület-1 és 2 szál-lock kérik, mindig ugyanaz. Ebben a helyzetben az egyik a két szál zár az első, majd a kért kapott egy második, amely még mindig rendelkezésre áll, mert egy másik szál az első zárat.
Általánosságban meg tudjuk határozni a következő szükséges feltételeket a holtpont előfordulásához:
- Közös végrehajtás: Van olyan erőforrás, amelyet bármikor csak egyetlen szálzal lehet elérni.
- Erőforrás megőrzése: Egy erőforrás rögzítése során a szál megpróbálja megszerezni egy másik zárat egy bizonyos erőforráson.
- Nincs prioritásos szolgáltatási megszakítás: nincs erőforrás-felszabadítási mechanizmus, ha egy szál meghatározott ideig zárolja a zárat.
- Kerekes várakozás: A futásidő folyamán egy sor szálat jelenít meg, amelyben két (vagy több) szál vár az egymástól blokkolt forrás felszabadítására.
Habár a feltételek listája hosszúnak tűnik, gyakran a jól hangolt többszálú alkalmazásoknak vannak holtponthibák. De megakadályozhatja, ha eltávolíthatja a fent felsorolt feltételeket:
- Közös végrehajtás: ezt a feltételt gyakran nem lehet eltávolítani, ha az erőforrást csak egy személy használhatja. De ennek nem kell az oka. A DBMS rendszerek használata során a táblázatban lévő pesszimista zárolás helyett egy olyan megoldást kell alkalmazni, amelyet frissíteni kell, az Optimistic Lock nevű technikát használva.
- Az erőforrás egy másik exkluzív erőforrásra való várakozás elkerülésének módja az, hogy blokkolja az összes szükséges erőforrást az algoritmus elején, és mindent felszabadítson, ha egyszerre nem tudja letiltani. Természetesen ez nem mindig lehetséges, előfordulhat, hogy olyan forrásokról van szó, amelyek előzetesen ismeretlenek, vagy ez a megközelítés egyszerűen az erőforrások pazarlásához vezet.
- Ha a zár nem fogadható azonnal, a lehetséges holtpont megkerülésének módja az időkorlát beírása. Például az SDK-ból származó ReentrantLock osztály lehetővé teszi a zárolás lejárati dátumának beállítását.
- Amint azt a fenti példából láttuk, a holtpont nem jelenik meg, ha a kérések sorrendje nem különbözik a különböző szálaktól. Ez könnyen ellenőrizhető, ha az összes blokkolási kódot egy olyan módszerrel tudja elhelyezni, amelyen keresztül minden szálnak át kell haladnia.
A fejlettebb alkalmazásoknál gondolhatsz arra, hogy végrehajtunk egy rendszert a holtpontok felderítésére. Itt kell végrehajtania egyfajta szálellenőrzést, amelyben minden egyes szál a blokkolási jog sikeres megszerzését és a zárolás megpróbálását jelenti. Ha a szálak és a zárak irányított gráfként vannak modellezve, akkor észlelheti, ha két különböző szál tartja fel az erőforrásait, miközben más blokkolt erőforrásokat próbál elérni egyszerre. Ha a lezáró szálak felszabadíthatják a szükséges erőforrásokat, akkor automatikusan megoldhatja a holtpont helyzetét.
Az ütemező eldönti, hogy melyik szál van a FUTTATÓ állapotban. a következőket kell tennie. A döntés a menet elsőbbségén alapul; így az alacsonyabb prioritású szálak kevesebb CPU időt kapnak, mint a magasabb prioritásúak. Mi úgy tűnik, mint egy ésszerű megoldás is okozhat problémát visszaélés. Ha a magas prioritású szálak a legtöbb idő alatt végrehajtásra kerülnek, úgy tűnik, hogy az alacsony prioritású szálak "éheződnek", mert nem kapnak elég időt a munkájuk megfelelő elvégzéséhez. Ezért javasoljuk, hogy a szál prioritása csak akkor legyen beállítva, ha erre kényszerítő okok vannak.
A szálhúzás nem nyilvánvaló példája például a finalize () módszer. A Java nyelven biztosítja a kód végrehajtását, mielőtt az objektumot törölné a szemétgyűjtő. De ha megnézzük a véglegesítés menetének prioritását, akkor észre fogod venni, hogy nem a legmagasabb prioritással kezdődik. Ezért vannak előfeltételek a menetes böjtre, amikor az objektum finalize () metódusa túl sok időt tölt a kód többi részéhez képest.
A végrehajtási idő másik problémája az, hogy nem határozható meg, hogy a szálak milyen sorrendben haladnak át a szinkronizált mondaton. Amikor sok párhuzamos szálak egy kódot, ami díszített egy szinkronizált blokk, előfordulhat, hogy egy szál kell várnia, mint mások, mielőtt a készüléket. Elméletileg soha nem juthatnak oda.
A probléma megoldása az úgynevezett "fair" zár. A tisztességes zárak figyelembe veszik a szálak várakozási idejét, amikor meghatározzák, hogy ki kell ugrani. Az érvényes zárolás végrehajtási példája a Java SDK: java.util.concurrent.locks.ReentrantLock. Ha olyan konstruktort használ, amelynek logikai zászlós értéke true, akkor a ReentrantLock hozzáférést biztosít egy olyan szálhoz, amely hosszabb ideig várja a többieket. Ez biztosítja az éhség hiányát, ugyanakkor a prioritások figyelmen kívül hagyásával jár. Emiatt gyakrabban lehet végrehajtani a kevésbé fontosabb folyamatokat, amelyek gyakran ezen a gáton várhatóak. Végül, fontosabb, hogy a ReentrantLock osztály csak olyan szálakat vizsgálhat, amelyek egy zárolást várnak, pl. A szálak gyakran futottak, és elérték a gátat. Ha a téma elsőbbsége túl alacsony, ez nem fordul elő gyakran, ezért a legfontosabb szálak még mindig gyakoribbak lesznek.
A többszálú számítástechnika esetében a szokásos helyzet az, hogy olyan munkasorok jelen vannak, amelyek arra számítanak, hogy a termelőjük valamilyen munkát végez számukra. De, ahogy megtudtuk, az aktív várakozás a hurokban bizonyos értékek ellenőrzésével nem jó választás a CPU idő szempontjából. A Thread.sleep () metódus használata ebben a helyzetben szintén nem különösebben alkalmas arra, hogy az átvételt követően azonnal elkezdjük a munkánkat.
Ehhez a Java programozási nyelvnek eltérő struktúrája van, amelyet a rendszerben lehet használni: wait () és notify (). A java.lang.Object osztály összes objektumából örökölt wait () metódus az aktuális szálat szüneteltetheti, és várhatja, amíg egy másik szál ébreszt fel az értesítési () módszerrel. A helyes működés érdekében a várakozás () módot hívó szálnak meg kell felelnie a szinkronizált kulcsszó használatával korábban kapott zárolásnak. Várakozás () hívásakor a zár kioldódik, és a szál vár, amíg egy másik szál, amely most megszerezte a zárat, az ugyanazon objektum-példányra értesíti ().
Egy többszálas alkalmazásban természetesen több mint egy szál várható, hogy egy objektumról értesítést kapjon. Ezért kétféle mód van a szálak ébresztésére: notify () és notifyAll (). Míg az első módszer felébreszti az egyik várakozási szálat, a notifyAll () módszer felébreszti őket. De vegye figyelembe, hogy a szinkronizált kulcsszavakhoz hasonlóan nincs olyan szabály, amely meghatározza, hogy melyik szálat fogja ébreszteni a következő, ha értesítést hív (). Egyszerű példa a gyártó és a fogyasztó számára, ez nem számít, mivel nem számít, hogy melyik szál ébred fel.
A következő kód azt mutatja, hogy a mechanizmus várakozás (), és erről értesíti () lehet használni, hogy megszervezze a várakozó szál-fogyasztók egy új munkahely, amely bekerül a sorban szál gyártó:
A main () módszer öt fogyasztói szálat és egy gyártósorozatot indít, majd várakozik a munkájuk befejezésére. Miután az értékesítési téma új értéket ad a sorhoz, és értesíti az összes váró szálat, hogy valami történt. A fogyasztók egy sorzárat kapnak (jegyezzük meg az egyik tetszőleges fogyasztót), majd elaludtunk, hogy később felkeljük, amikor a sor ismét tele van. Amikor a gyártó befejezi munkáját, értesíti a fogyasztókat, hogy felébredjenek. Ha nem tettük meg az utolsó lépést, akkor a fogyasztói szálak mindig megvárnák a következő értesítést, mert nem állítottunk be várakozási időt. Ehelyett a várakozás (hosszú időtúllépés) módszert használhatjuk, legalább egy idő után.
Amint azt az előző szakaszban említettük, a várakozás () hívás az objektummonitoron csak a monitoron található reteszelést távolítja el. Azok az egyéb zárak, amelyeket ugyanaz a szál tartott, nem szabadulnak meg. Ahogyan könnyű megérteni, a mindennapi munkában előfordulhat, hogy a várakozó () hívó szál megtartja a zárakat. Ha más szálak is számítanak ezekről a zárakról, akkor előfordulhat, hogy patthelyzet alakul ki. Nézzük meg a zárat a következő példában:
Mint korábban megtudtuk. A metódus aláírásával szinkronizálva ezzel egyenértékű a szinkronizált (ez)<>. A fenti példában véletlenül adunk a szinkronizált kulcsszót a módszert, majd szinkronizálja az összes monitor tárgy sorba, hogy küldje ezt a szálat aludni várva a következő érték a sorban. Ezután az aktuális szál a zárat sorba állítja, de nem zárja le. A putInt () módszer értesíti az új érték hozzáadott alvó szálat. De véletlenül hozzáadtuk a szinkronizált kulcsszót ehhez a módszerhez. Most, hogy a második szál elaludt, még mindig tartja a zárat. Ezért az első téma nem léphet be a putInt () metódusba, amíg ezt a zárat a második téma nem tartja. Ennek eredményeképpen a patthelyzet és a függő program áll. Ha végrehajtja a fenti kódot, akkor a program azonnal elindul.
A mindennapi életben ez a helyzet nem lehet olyan nyilvánvaló. Zárak tartott egy szál lehet függ a paraméterek és körülmények a működés során felmerült, és a blokk szinkronizált, okozza a problémát nem lehet olyan közel a hely a kódot, ahol helyeztük hívás várakozás (). Ez megnehezíti az ilyen problémák felkutatását, különösen akkor, ha egy idő után vagy nagy terhelés esetén előfordulhat.
Gyakran ellenőriznie kell bizonyos feltételek végrehajtását, mielőtt egy műveletet elvégezne egy szinkronizált objektummal. Ha például van egy sor, akkor meg kell várni, hogy töltse ki. Ezért írhat egy olyan eljárást, amely ellenőrzi a várólista teljességét. Ha még mindig üres, akkor az aktuális szálat alvó állapotba küldi, amíg fel nem ébred:
A fenti kódot várakozási sorban szinkronizáljuk, mielőtt hívjuk a () várakozást, majd várakozunk az while hurokban, amíg legalább egy elem meg nem jelenik a sorban. A második szinkronizált blokk ismét a várólistát objektummonitorként használja. Meghívja a várakozás poll () módját, hogy megkapja az értéket. Tájékoztatási célból az IllegalStateException dobásra kerül, ha a lekérdezés null értékre tér vissza. Ez akkor történik, ha nincsenek a sorban lévő elemek a letöltéshez.
Ha futtatod ezt a példát, látni fogod, hogy az IllegalStateException nagyon gyakran dob. Annak ellenére, hogy helyesen szinkronizáltuk a sor monitoron, kivételt vetettünk ki. Ennek oka az, hogy két különböző blokk van szinkronizálva. Képzeld el, hogy két szál van, amelyek az első szinkronizált blokkhoz érkeztek. Az első szál belépett a blokkba és egy álomba esett, mert a sor üres. Ugyanez igaz a második szálra is. Most, hogy mindkét szál a másnaposság (köszönhetően, hogy hívja a notifyAll () hívás egy másik szál ellenőrzésére), mindketten látni az értéket (elem) a sorban a gyártó által hozzáadott. Aztán mindketten a második akadályba kerültek. Itt az első szál beírta és kivette az értéket a sorból. Amikor a második szál belép, a várólista már üres. Ezért, amint az érték a várólistáról visszaküldött, nulla lesz, és kivételt hoz.
Az ilyen helyzetek elkerülése érdekében minden olyan műveletet végrehajtania, amely a monitor állapotától függ, ugyanabban a szinkronizált mondatban:
Itt végezzük el a poll () metódust ugyanabban a szinkronizált mondatban, mint az isEmpty () metódus. A szinkronizált mondatnak köszönhetően biztosak vagyunk benne, hogy csak egy szál végrehajt egy módszert a monitor számára egy adott időpontban. Ezért egyetlen más téma sem távolíthatja el az elemeket a hívások között az isEmpty () és a poll () között.