Samtidighet med LMAX Disruptor - En introduksjon

1. Oversikt

Denne artikkelen introduserer LMAX Disruptor og snakker om hvordan det hjelper å oppnå programvare samtidig med lav ventetid. Vi vil også se en grunnleggende bruk av Disruptor-biblioteket.

2. Hva er en forstyrrer?

Disruptor er et open source Java-bibliotek skrevet av LMAX. Det er et samtidig programmeringsrammeverk for behandling av et stort antall transaksjoner, med lav latens (og uten kompleksiteten til samtidig kode). Ytelsesoptimaliseringen oppnås ved en programvaredesign som utnytter effektiviteten til underliggende maskinvare.

2.1. Mekanisk sympati

La oss starte med kjernekonseptet med mekanisk sympati - det handler om å forstå hvordan den underliggende maskinvaren fungerer og programmering på en måte som best fungerer med den maskinvaren.

La oss for eksempel se hvordan CPU og minneorganisasjon kan påvirke programvareytelsen. CPUen har flere lag med cache mellom den og hovedminnet. Når prosessoren utfører en operasjon, ser den først i L1 etter dataene, deretter L2, deretter L3 og til slutt hovedminnet. Jo lenger det må gå, jo lenger tid vil operasjonen ta.

Hvis den samme operasjonen utføres på et datastykke flere ganger (for eksempel en sløyfeteller), er det fornuftig å laste dataene inn på et sted veldig nær CPU.

Noen veiledende tall for kostnadene ved hurtigbuffer:

Latens fra CPU tilCPU-sykluserTid
HovedminneFlere~ 60-80 ns
L3-hurtigbuffer~ 40-45 sykluser~ 15 ns
L2-hurtigbuffer~ 10 sykluser~ 3 ns
L1-hurtigbuffer~ 3-4 sykluser~ 1 ns
Registrere1 syklusVeldig veldig raskt

2.2. Hvorfor ikke køer

Køimplementeringer har en tendens til å ha skrivestrid på hodet, halen og størrelsesvariablene. Køene er vanligvis alltid nær full eller nær tomme på grunn av forskjellene i tempo mellom forbrukere og produsenter. De opererer veldig sjelden i en balansert mellomgrunn hvor produksjons- og forbrukshastigheten er jevnt samsvarende.

For å håndtere skrivestriden bruker en kø ofte låser, noe som kan føre til en kontekstbytte til kjernen. Når dette skjer, vil prosessoren som er involvert, sannsynligvis miste dataene i cachene.

For å få best cache-oppførsel, bør designen bare ha en kjerne som skriver til et hvilket som helst minneplassering (flere lesere er fine, da prosessorer ofte bruker spesielle høyhastighetslenker mellom cachene sine). Køer svikter prinsippet om en forfatter.

Hvis to separate tråder skriver til to forskjellige verdier, ugyldiggjør hver kjerne hurtigbufferlinjen til den andre (data overføres mellom hovedminne og hurtigbuffer i blokker av fast størrelse, kalt hurtigbuffelinjer). Det er en skrivestrid mellom de to trådene, selv om de skriver til to forskjellige variabler. Dette kalles falsk deling, fordi hver gang du får tilgang til hodet, får du tilgang til halen, og omvendt.

2.3. Hvordan Disruptor fungerer

Disruptor har en arraybasert sirkulær datastruktur (ringbuffer). Det er en matrise som har en peker til neste tilgjengelige spor. Den er fylt med forhåndsallokerte overføringsobjekter. Produsenter og forbrukere utfører skriving og lesing av data til ringen uten låsing eller konflikt.

I en Disruptor blir alle hendelser publisert til alle forbrukere (multicast), for parallelt forbruk gjennom separate nedstrøms køer. På grunn av parallell behandling fra forbrukerne er det nødvendig å koordinere avhengigheter mellom forbrukerne (avhengighetsgraf).

Produsenter og forbrukere har en sekvensteller for å indikere hvilket spor i bufferen det for tiden jobber med. Hver produsent / forbruker kan skrive sin egen sekvens teller, men kan lese andres sekvens tellere. Produsentene og forbrukerne leser benkene for å sikre at sporet de ønsker å skrive inn er tilgjengelig uten låser.

3. Bruke Disruptor Library

3.1. Maven avhengighet

La oss begynne med å legge til Disruptor-biblioteksavhengighet i pom.xml:

 com.lmax disruptor 3.3.6 

Den siste versjonen av avhengigheten kan sjekkes her.

3.2. Definere en hendelse

La oss definere hendelsen som bærer dataene:

offentlig statisk klasse ValueEvent {private int-verdi; offentlig endelig statisk EventFactory EVENT_FACTORY = () -> ny ValueEvent (); // standard getters og setters} 

De EventFactory lar Disruptor forhåndslokalisere hendelsene.

3.3. Forbruker

Forbrukerne leser data fra ringbufferen. La oss definere en forbruker som skal håndtere hendelsene:

offentlig klasse SingleEventPrintConsumer {... public EventHandler [] getEventHandler () {EventHandler eventHandler = (event, sequence, endOfBatch) -> print (event.getValue (), sequence); returner nye EventHandler [] {eventHandler}; } privat tomtrykk (int id, lang sekvensId) {logger.info ("Id er" + id + "sekvens-ID som ble brukt er" + sekvensId); }}

I vårt eksempel skriver forbrukeren bare ut til en logg.

3.4. Konstruere Disruptor

Konstruer Disruptor:

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

I konstruktøren av Disruptor er følgende definert:

  • Event Factory - Ansvarlig for å generere objekter som vil bli lagret i ringbuffer under initialiseringen
  • Størrelsen på ringbuffer - Vi har definert 16 som størrelsen på ringbufferen. Det må være en styrke på to andre, det ville kaste et unntak mens initialiseringen. Dette er viktig fordi det er enkelt å utføre de fleste operasjonene ved hjelp av logiske binære operatorer, f.eks. mod drift
  • Trådfabrikk - Fabrikk for å lage tråder for hendelsesbehandlere
  • Produsenttype - Spesifiserer om vi vil ha en eller flere produsenter
  • Ventestrategi - Definerer hvordan vi ønsker å håndtere treg abonnent som ikke holder tritt med produsentens tempo

Koble forbrukerhandleren:

disruptor.handleEventsWith (getEventHandler ()); 

Det er mulig å forsyne flere forbrukere med Disruptor for å håndtere dataene som produseres av produsenten. I eksemplet ovenfor har vi bare en forbruker a.k.a. hendelsesbehandler.

3.5. Starte Disruptor

Slik starter du Disruptor:

RingBuffer ringBuffer = disruptor.start ();

3.6. Produsere og publisere arrangementer

Produsenter plasserer dataene i ringbufferen i en sekvens. Produsenter må være oppmerksomme på neste tilgjengelige spor, slik at de ikke overskriver data som ennå ikke er konsumert.

Bruke RingBuffer fra Disruptor for publisering:

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

Her produserer og publiserer produsenten varer i rekkefølge. Det er viktig å merke seg her at Disruptor fungerer i likhet med 2-faseprotokoll. Den leser en ny sekvensId og publiserer. Neste gang det skal bli sekvensId + 1 som neste sequenceId.

4. Konklusjon

I denne veiledningen har vi sett hva en Disruptor er og hvordan den oppnår samtidighet med lav ventetid. Vi har sett begrepet mekanisk sympati og hvordan det kan utnyttes for å oppnå lav ventetid. Vi har da sett et eksempel ved å bruke Disruptor-biblioteket.

Eksempelkoden finnes i GitHub-prosjektet - dette er et Maven-basert prosjekt, så det skal være enkelt å importere og kjøre som det er.


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