Párhuzamosság az LMAX zavaróval - Bevezetés
1. Áttekintés
Ez a cikk bemutatja az LMAX megszakítót, és arról beszél, hogyan segít elérni a szoftver párhuzamosságát alacsony késéssel. Látni fogjuk a Disruptor könyvtár alapvető használatát is.
2. Mi a rendbontó?
A Disruptor egy nyílt forráskódú Java könyvtár, amelyet az LMAX írt. Ez egyidejű programozási keret nagyszámú tranzakció feldolgozásához, alacsony késleltetéssel (és az egyidejű kód bonyolultsága nélkül). A teljesítmény optimalizálását olyan szoftvertervezéssel érik el, amely kihasználja az alapul szolgáló hardver hatékonyságát.
2.1. Mechanikus rokonszenv
Kezdjük a mechanikus szimpátia alapkoncepciójával - ez arról szól, hogy megértsük, hogyan működik az alapul szolgáló hardver, és programozzunk a hardverrel legjobban együttműködő módon.
Lássuk például, hogy a CPU és a memória szervezése hogyan befolyásolhatja a szoftver teljesítményét. A CPU több réteg gyorsítótárral rendelkezik közte és a fő memória között. Amikor a CPU műveletet végez, először L1-ben keresi az adatokat, majd L2, majd L3, végül pedig a fő memóriát. Minél tovább kell menni, annál tovább tart a művelet.
Ha ugyanazt a műveletet egy adatrészen többször végrehajtják (például egy hurokszámlálóval), akkor célszerű ezeket az adatokat a CPU-hoz nagyon közel lévő helyre betölteni.
Néhány tájékoztató adat a gyorsítótár-hiányok költségeiről:
Késés a CPU-tól | CPU ciklusok | Idő |
---|---|---|
Fő memória | Többszörös | ~ 60-80 ns |
L3 gyorsítótár | ~ 40-45 ciklus | ~ 15 ns |
L2 gyorsítótár | ~ 10 ciklus | ~ 3 ns |
L1 gyorsítótár | ~ 3-4 ciklus | ~ 1 ns |
Regisztráció | 1 ciklus | Nagyon nagyon gyors |
2.2. Miért nem várólisták
A várólista megvalósításai általában versengő versengést mutatnak a fej, a farok és a méret változóinál. A fogyasztók és a gyártók közötti ütembeli különbségek miatt a várólisták általában mindig közel vannak a telthez vagy az üreshez. Nagyon ritkán működnek kiegyensúlyozott középtéren, ahol a termelés és a fogyasztás aránya egyenletesen egyezik.
Az írási versengés kezeléséhez a várakozási sor gyakran zárakat használ, ami környezeti váltást okozhat a kernelre. Ha ez megtörténik, az érintett processzor valószínűleg elveszíti a gyorsítótárában lévő adatokat.
A gyorsítótár-viselkedés legjobb elérése érdekében a tervnek csak egy magot kell írnia bármilyen memóriahelyre (több olvasó is jól van, mivel a processzorok gyakran speciális nagy sebességű linkeket használnak a gyorsítótárak között). A várakozási sorok kudarcot vallanak az egy író elvén.
Ha két különálló szál két különböző értékre ír, akkor mindegyik mag érvényteleníti a másik gyorsítótárát (az adatokat a fő memória és a cache között rögzített méretű blokkokban, ún. Cache-sorokban továbbítják). Ez egy írásbeli versengés a két szál között, annak ellenére, hogy két különböző változónak írnak. Ezt hamis megosztásnak hívják, mert minden alkalommal, amikor a fejhez érnek, a farokhoz is hozzáférnek, és fordítva.
2.3. Hogyan működik a rendbontó

A Disruptor tömb alapú kör alakú adatstruktúrával rendelkezik (gyűrűpuffer). Ez egy tömb, amely mutatóval rendelkezik a következő elérhető nyíláshoz. Meg van töltve előre kiosztott átviteli objektumokkal. A gyártók és a fogyasztók zárolás és vitatás nélkül írják és olvassák az adatokat a gyűrűig.
A Disruptorban minden eseményt közzétesznek az összes fogyasztó számára (multicast), párhuzamos fogyasztás céljából, külön downstream sorokban. A fogyasztók általi párhuzamos feldolgozás miatt koordinálni kell a fogyasztók közötti függőségeket (függőségi grafikon).
A gyártók és a fogyasztók szekvenciaszámlálóval rendelkeznek, hogy jelezzék, hogy a puffer melyik nyílásán dolgozik. Minden gyártó / fogyasztó megírhatja saját szekvenciaszámlálóját, de elolvashatja mások szekvencia számlálóit. A gyártók és a fogyasztók elolvassák a pultokat, hogy biztosítsák, hogy az a hely, ahová be akar írni, zárak nélkül elérhető.
3. A Disruptor Library használata
3.1. Maven-függőség
Kezdjük azzal, hogy hozzáadjuk a Disruptor könyvtár függőségét a pom.xml:
com.lmax zavaró 3.3.6
A függőség legfrissebb verziója itt ellenőrizhető.
3.2. Esemény meghatározása
Határozzuk meg az adatot hordozó eseményt:
public static class ValueEvent {private int value; public final static EventFactory EVENT_FACTORY = () -> new ValueEvent (); // szabványos mérőeszközök és beállítók}
A EventFactory lehetővé teszi a Disruptor számára az események előrendelését.
3.3. Fogyasztó
A fogyasztók adatokat olvasnak a gyűrűs pufferből. Határozzunk meg egy fogyasztót, aki kezeli az eseményeket:
public class SingleEventPrintConsumer {... public EventHandler [] getEventHandler () {EventHandler eventHandler = (esemény, szekvencia, endOfBatch) -> nyomtatás (event.getValue (), szekvencia); return new EventHandler [] {eventHandler}; } private void print (int id, long sequenceId) {logger.info ("Az Id" + id + "használt szekvencia azonosító:" + sequenceId); }}
Példánkban a fogyasztó csak naplóba nyomtat.
3.4. A zavaró ember felépítése
Építsd meg a zavarót:
ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE; WaitStrategy waitStrategy = new BusySpinWaitStrategy (); Disruptor disruptor = new Disruptor (ValueEvent.EVENT_FACTORY, 16, threadFactory, ProducerType.SINGLE, waitStrategy);
A Disruptor konstruktorában a következők vannak meghatározva:
- Eseménygyár - Az inicializálás során gyűrűpufferben tárolt objektumok létrehozásáért felelős
- A gyűrűpuffer mérete - 16-at definiáltunk a gyűrűpuffer méretének. 2 másik erejének kell lennie, kivételt hozhat az inicializálás közben. Ez azért fontos, mert a műveletek nagy részét könnyű végrehajtani logikai bináris operátorok használatával, pl. mod működése
- Szálgyár - Gyár szálak létrehozására az eseményfeldolgozók számára
- Gyártó típusa - Meghatározza, hogy egyetlen vagy több gyártónk lesz-e
- Várakozási stratégia - Meghatározza, hogyan szeretnénk kezelni a lassú előfizetőket, akik nem lépést tartanak a gyártó tempójával
Csatlakoztassa a fogyasztói kezelőt:
disruptor.handleEventsWith (getEventHandler ());
Lehetséges több fogyasztót ellátni a Disruptorral a termelő által előállított adatok kezelésére. A fenti példában csak egy fogyasztónk van, más néven eseménykezelő.
3.5. A Disruptor elindítása
A Disruptor indítása:
RingBuffer ringBuffer = disruptor.start ();
3.6. Események előállítása és közzététele
A gyártók az adatokat a gyűrűpufferbe helyezik egymás után. A gyártóknak tisztában kell lenniük a következő rendelkezésre álló résszel, hogy ne írják felül a még fel nem használt adatokat.
Használja a RingBuffer a Disruptor kiadótól:
for (int eventCount = 0; eventCount <32; eventCount ++) {hosszú sorrendId = ringBuffer.next (); ValueEvent valueEvent = ringBuffer.get (sequenceId); valueEvent.setValue (eventCount); ringBuffer.publish (sequenceId); }
Itt a gyártó egymás után gyárt és publikál elemeket. Fontos itt megjegyezni, hogy a Disruptor hasonlóan működik, mint a 2 fázisú protokoll. Újat olvas sorrendId és publikál. Legközelebb meg kell kapnia sorrendId + 1 a következő sorrendId.
4. Következtetés
Ebben az oktatóanyagban láthattuk, hogy mi a megzavaró és hogyan éri el az egyidejűséget alacsony késéssel. Láttuk a mechanikus szimpátia fogalmát, és azt, hogy miként lehet kihasználni az alacsony látencia elérésére. Ekkor láthattunk egy példát a Disruptor könyvtár használatával.
A példakód megtalálható a GitHub projektben - ez egy Maven-alapú projekt, ezért könnyen importálhatónak és futtathatónak kell lennie.