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ólCPU ciklusokIdő
Fő memóriaTö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 ciklusNagyon 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.


$config[zx-auto] not found$config[zx-overlay] not found