Samtidighed med LMAX Disruptor - En introduktion

1. Oversigt

Denne artikel introducerer LMAX Disruptor og taler om, hvordan det hjælper med at opnå software-samtidighed med lav latenstid. Vi vil også se en grundlæggende brug af Disruptor-biblioteket.

2. Hvad er en forstyrrer?

Disruptor er et open source Java-bibliotek skrevet af LMAX. Det er en samtidig programmeringsramme til behandling af et stort antal transaktioner med lav latenstid (og uden kompleksiteten af ​​samtidig kode). Performance-optimeringen opnås ved hjælp af et softwaredesign, der udnytter effektiviteten af ​​den underliggende hardware.

2.1. Mekanisk sympati

Lad os starte med kernebegrebet mekanisk sympati - det handler om at forstå, hvordan den underliggende hardware fungerer og programmering på en måde, der bedst fungerer med den hardware.

Lad os for eksempel se, hvordan CPU- og hukommelsesorganisation kan påvirke softwareydelsen. CPU'en har flere lag cache mellem den og hovedhukommelsen. Når CPU'en udfører en operation, ser den først i L1 efter dataene, derefter L2, derefter L3 og endelig hovedhukommelsen. Jo længere det skal gå, jo længere tid vil operationen tage.

Hvis den samme handling udføres på et stykke data flere gange (for eksempel en loop-tæller), er det fornuftigt at indlæse disse data til et sted meget tæt på CPU'en.

Nogle vejledende tal for omkostningerne ved cache-savner:

Ventetid fra CPU tilCPU-cyklusserTid
Primære hukommelseMange~ 60-80 ns
L3 cache~ 40-45 cyklusser~ 15 ns
L2 cache~ 10 cyklusser~ 3 ns
L1 cache~ 3-4 cyklusser~ 1 ns
Tilmeld1 cyklusMeget meget hurtig

2.2. Hvorfor ikke køer

Køimplementeringer har en tendens til at have skrivetvist på hoved-, hale- og størrelsesvariablerne. Køer er typisk altid tæt på fulde eller tæt på tomme på grund af forskellene i tempo mellem forbrugere og producenter. De opererer meget sjældent i en afbalanceret mellemvej, hvor produktions- og forbrugshastigheden er ensartet.

For at håndtere skrivestridelsen bruger en kø ofte låse, hvilket kan forårsage en kontekstskift til kernen. Når dette sker, mister den involverede processor sandsynligvis dataene i cacherne.

For at få den bedste cache-opførsel skal designet kun have en kerne til enhver hukommelsesplacering (flere læsere er fine, da processorer ofte bruger specielle højhastighedsforbindelser mellem deres caches). Køer fejler princippet om en forfatter.

Hvis to separate tråde skriver til to forskellige værdier, ugyldiggør hver kerne cache-linjen for den anden (data overføres mellem hovedhukommelse og cache i blokke af fast størrelse, kaldet cache-linjer). Det er en skrivestrid mellem de to tråde, selvom de skriver til to forskellige variabler. Dette kaldes falsk deling, fordi hver gang der er adgang til hovedet, får man også adgang til halen og omvendt.

2.3. Sådan fungerer Disruptor

Disruptor har en arraybaseret cirkulær datastruktur (ringbuffer). Det er en matrix, der har en markør til den næste tilgængelige plads. Den er fyldt med forudallokerede overførselsobjekter. Producenter og forbrugere udfører skrivning og læsning af data til ringen uden låsning eller strid.

I en Disruptor offentliggøres alle begivenheder til alle forbrugere (multicast) til parallelforbrug gennem separate downstream-køer. På grund af parallel behandling fra forbrugere er det nødvendigt at koordinere afhængigheder mellem forbrugerne (afhængighedsgraf).

Producenter og forbrugere har en sekvenstæller, der angiver, hvilken plads i bufferen, den i øjeblikket arbejder på. Hver producent / forbruger kan skrive sin egen sekvenstæller, men kan læse andres sekvenstællere. Producenterne og forbrugerne læser tællerne for at sikre, at den plads, de ønsker at skrive i, er tilgængelig uden låse.

3. Brug af Disruptor Library

3.1. Maven afhængighed

Lad os starte med at tilføje Disruptor-biblioteksafhængighed i pom.xml:

 com.lmax forstyrrer 3.3.6 

Den seneste version af afhængigheden kan kontrolleres her.

3.2. Definition af en begivenhed

Lad os definere begivenheden, der bærer dataene:

offentlig statisk klasse ValueEvent {private int-værdi; offentlig endelig statisk EventFactory EVENT_FACTORY = () -> ny ValueEvent (); // standard getters og setter} 

Det EventFactory lader Disruptor forudlokalisere begivenhederne.

3.3. Forbruger

Forbrugerne læser data fra ringbufferen. Lad os definere en forbruger, der håndterer begivenhederne:

offentlig klasse SingleEventPrintConsumer {... public EventHandler [] getEventHandler () {EventHandler eventHandler = (event, sequence, endOfBatch) -> print (event.getValue (), sequence); returner nyt EventHandler [] {eventHandler}; } privat tomrumsudskrivning (int id, lang sekvensId) {logger.info ("Id er" + id + "sekvens-id, der blev brugt, er" + sekvensId); }}

I vores eksempel udskriver forbrugeren bare til en log.

3.4. Konstruktion af Disruptor

Konstruer Disruptor:

ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE; WaitStrategy waitStrategy = ny BusySpinWaitStrategy (); Disruptor disruptor = ny Disruptor (ValueEvent.EVENT_FACTORY, 16, threadFactory, ProducerType.SINGLE, waitStrategy); 

I konstruktøren af ​​Disruptor er følgende defineret:

  • Event Factory - Ansvarlig for at generere objekter, der lagres i ringbuffer under initialiseringen
  • Størrelsen på ringbuffer - Vi har defineret 16 som størrelsen på ringbufferen. Det skal være en styrke på 2 ellers, det ville kaste en undtagelse under initialisering. Dette er vigtigt, fordi det er let at udføre de fleste af operationerne ved hjælp af logiske binære operatorer, f.eks. mod-drift
  • Trådfabrik - Fabrik til oprettelse af tråde til begivenhedsbehandlere
  • Producenttype - Angiver, om vi vil have en eller flere producenter
  • Ventestrategi - Definerer, hvordan vi gerne vil håndtere langsom abonnent, der ikke følger producentens tempo

Tilslut forbrugerhandleren:

disruptor.handleEventsWith (getEventHandler ()); 

Det er muligt at forsyne flere forbrugere med Disruptor til at håndtere de data, der produceres af producenten. I eksemplet ovenfor har vi kun en forbruger a.k.a. begivenhedshåndterer.

3.5. Start afbryderen

Sådan startes Disruptor:

RingBuffer ringBuffer = disruptor.start ();

3.6. Producering og udgivelse af begivenheder

Producenterne placerer dataene i ringbufferen i en sekvens. Producenter skal være opmærksomme på den næste tilgængelige plads, så de ikke overskriver data, der endnu ikke er forbrugt.

Brug RingBuffer fra Disruptor til udgivelse:

for (int eventCount = 0; eventCount <32; eventCount ++) {long sequenceId = ringBuffer.next (); ValueEvent valueEvent = ringBuffer.get (sequenceId); valueEvent.setValue (eventCount); ringBuffer.publish (sequenceId); } 

Her producerer og udgiver producenten varer i rækkefølge. Det er vigtigt at bemærke her, at Disruptor fungerer svarende til 2-faset kommitteringsprotokol. Den læser en ny sekvensId og udgiver. Næste gang det skulle komme sekvensId + 1 som den næste sekvensId.

4. Konklusion

I denne vejledning har vi set, hvad en Disruptor er, og hvordan den opnår samtidighed med lav latenstid. Vi har set begrebet mekanisk sympati, og hvordan det kan udnyttes til at opnå lav latenstid. Vi har så set et eksempel ved hjælp af Disruptor-biblioteket.

Eksempelkoden findes i GitHub-projektet - dette er et Maven-baseret projekt, så det skal være let at importere og køre som det er.


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