Når du lager et Elixir-program, må du ofte dele en stat. For eksempel viste jeg i en av mine tidligere artikler hvordan å kode en server for å utføre ulike beregninger og holde resultatet i minnet (og senere har vi sett hvordan du gjør denne serveren kollisikker ved hjelp av veiledere). Det er imidlertid et problem: Hvis du har en enkelt prosess som tar vare på staten og mange andre prosesser som får tilgang til det, kan ytelsen bli alvorlig påvirket. Dette skyldes ganske enkelt at prosessen kun kan betjene én forespørsel om gangen.
Det er imidlertid måter å overvinne dette problemet, og i dag skal vi snakke om en av dem. Møt Erlang Term Storage-tabeller eller bare ETS-tabeller, en rask lagring i minnet som kan være vert for tilfeldig data. Som navnet antyder, ble disse tabellene innledningsvis introdusert i Erlang, men som med enhver annen Erlang-modul, kan vi også enkelt bruke dem i Elixir.
I denne artikkelen vil du:
Alle kodeeksempler fungerer med både Elixir 1.4 og 1.5, som nylig ble utgitt.
Som nevnt tidligere er ETS-tabeller lagret i minnet som inneholder tupler av data (kalt rader). Flere prosesser kan få tilgang til tabellen med sin id eller et navn som er representert som et atom og utføre lese, skrive, slette og andre operasjoner. ETS-tabeller opprettes ved en egen prosess, så hvis denne prosessen er avsluttet, blir tabellen ødelagt. Imidlertid er det ingen automatisk søppelinnsamlingsmekanisme, slik at bordet kan hænge ut i minnet i ganske lang tid.
Data i ETS-tabellen er representert av en tuple : nøkkel, verdi1, verdi2, verdsettelse
. Du kan enkelt slå opp dataene med nøkkelen eller sette inn en ny rad, men som standard kan det ikke være to rader med samme tast. Nøkkelbaserte operasjoner er svært raske, men hvis du av en eller annen grunn trenger å lage en liste fra en ETS-tabell, og si, utfør komplekse manipulasjoner av dataene, er det også mulig.
I tillegg finnes det diskbaserte ETS-tabeller som lagrer innholdet i en fil. Selvfølgelig opererer de tregere, men på den måten får du en enkel filoppbevaring uten å måtte måtte. I tillegg kan ETS-minne enkelt konverteres til diskbasert og omvendt.
Så jeg tror det er på tide å starte reisen og se hvordan ETS-tabellene er opprettet!
For å opprette en ETS-tabell, bruk nye / 2
funksjon. Så lenge vi bruker en Erlang-modul, bør navnet være skrevet som et atom:
cool_table =: ets.new (: cool_table, [])
Vær oppmerksom på at inntil nylig kan du bare opprette opptil 1400 tabeller per BEAM-forekomst, men dette er ikke tilfelle lenger - du er bare begrenset til mengden tilgjengelig minne.
Det første argumentet passerte til ny
funksjonen er tabellens navn (alias), mens den andre inneholder en liste over alternativer. De cool_table
variabel inneholder nå et nummer som identifiserer tabellen i systemet:
IO.inspect cool_table # => 12306
Du kan nå bruke denne variabelen til å utføre etterfølgende operasjoner til tabellen (lese og skrive data, for eksempel).
La oss snakke om alternativene du kan spesifisere når du lager et bord. Den første (og litt merkelige) ting å merke seg er at du som standard ikke kan bruke tabellens alias på noen måte, og i utgangspunktet har den ingen effekt. Men fortsatt aliaset må bli sendt på bordets skapelse.
For å få tilgang til tabellen med aliaset, må du oppgi en : named_table
alternativ som dette:
cool_table =: ets.new (: cool_table, [: named_table])
Forresten, hvis du vil endre navn på tabellen, kan den gjøres ved hjelp av endre navn / 2
funksjon:
: ets.rename (cool_table,: cooler_table)
Deretter, som allerede nevnt, kan et bord ikke inneholde flere rader med samme nøkkel, og dette dikteres av type. Det finnes fire mulige bordtyper:
:sett
-det er standard en. Det betyr at du ikke kan ha flere rader med nøyaktig samme nøkler. Rammene blir ikke bestilt på noen spesiell måte.: ordered_set
-det samme som :sett
, men radene er bestilt av vilkårene.:bag
-flere rader kan ha samme nøkkel, men radene kan fortsatt ikke være helt identiske.: duplicate_bag
-rader kan være helt identiske.Det er en ting verdt å nevne om : ordered_set
tabeller. Som Erlangs dokumentasjon sier, behandler disse tabellene nøklene like like når de er sammenlign like, ikke bare når de kamp. Hva betyr det?
To termer i Erlang-kamp bare hvis de har samme verdi og samme type. Så heltall 1
matcher bare et annet heltall 1
, men ikke flyte 1.0
som de har forskjellige typer. To termer er sammenlignbare likevel, dersom enten de har samme verdi og type eller hvis begge er numeriske og strekker seg til samme verdi. Dette betyr at 1
og 1.0
er sammenlign like.
For å oppgi tabellens type legger du bare til et element i listen over alternativer:
cool_table =: ets.new (: cool_table, [: named_table,: ordered_set])
Et annet interessant alternativ som du kan passere er : komprimert
. Det betyr at dataene inne i bordet (men ikke nøklene) vil bli gjett hva som lagres i kompakt form. Selvfølgelig vil operasjonene som utføres på bordet bli langsommere.
Deretter kan du kontrollere hvilket element i tupelen som skal brukes som nøkkel. Som standard er det første elementet (posisjon 1
) brukes, men dette kan endres enkelt:
cool_table =: ets.new (: cool_table, [: keypos, 2])
Nå blir de andre elementene i tuplene behandlet som nøklene.
Det siste men ikke minst alternativet styrer tabellens tilgangsrettigheter. Disse rettighetene dikterer hvilke prosesser som er tilgjengelige for å få tilgang til tabellen:
:offentlig
-enhver prosess kan utføre enhver operasjon til bordet.: beskyttet
-standardverdien. Bare eierprosessen kan skrive til bordet, men alle prosessene kan lese.:privat
-bare eieren prosessen kan få tilgang til bordet.Så, for å lage et bord privat, ville du skrive:
cool_table =: ets.new (: cool_table, [: privat])
Ok, nok å snakke om alternativer-la oss se noen vanlige operasjoner som du kan utføre til bordene!
For å lese noe fra bordet må du først skrive noen data der, så la oss starte med sistnevnte operasjon. Bruke sette inn / 2
funksjon for å sette data inn i bordet:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : number, 5)
Du kan også sende en liste over tuples som dette:
: ets.insert (cool_table, [: number, 5, : string, "test"])
Merk at hvis tabellen har en type :sett
og en ny nøkkel samsvarer med en eksisterende, blir de gamle data overskrevet. Tilsvarende, hvis et bord har en type : ordered_set
og en ny nøkkel sammenligner med den gamle, blir dataene overskrevet, så vær oppmerksom på dette.
Innsatsoperasjonen (selv med flere tupler på en gang) er garantert å være atomisk og isolert, noe som betyr at alt er lagret i bordet eller ingenting i det hele tatt. Også andre prosesser vil ikke kunne se mellomresultatet fra operasjonen. Alt i alt er dette ganske lik SQL-transaksjoner.
Hvis du er bekymret for duplisering av nøkler eller ikke vil overskrive dataene dine ved et uhell, bruk insert_new / 2
fungere i stedet. Det ligner på sette inn / 2
men vil aldri sette inn dupliserende nøkler og vil i stedet returnere falsk
. Dette er tilfellet for :bag
og : duplicate_bag
bord også:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : tall, 5): ets.insert_new (cool_table, : number, 6) |> IO.inspect # = > false
Hvis du oppgir en liste over tuples, vil hver nøkkel bli sjekket, og operasjonen vil bli kansellert selv om en av tastene er duplisert.
Flott, nå har vi noen data i bordet vårt - hvordan henter vi dem? Den enkleste måten er å utføre oppslag med en nøkkel:
: ets.insert (cool_table, : number, 5) IO.inspect: ets.lookup (cool_table,: number) # => [nummer: 5]
Husk at for : ordered_set
tabell, nøkkelen skal sammenlignes med den angitte verdien. For alle andre bordtyper bør den matche. Også, hvis et bord er en :bag
eller en : ordered_bag
, de oppslag / 2
funksjonen kan returnere en liste med flere elementer:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: tall, 5, : tall, 6]) IO.inspect: ets.lookup (cool_table, ) # => [nummer: 5, nummer: 6]
I stedet for å hente en liste kan du hente et element i ønsket posisjon ved hjelp av lookup_element / 3
funksjon:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : tall, 6) IO.inspect: ets.lookup_element (cool_table,: tall, 2) # => 6
I denne koden får vi raden under nøkkelen :Nummer
og deretter ta elementet i den andre posisjonen. Det fungerer også perfekt med :bag
eller : duplicate_bag
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: tall, 5, : tall, 6]) IO.inspect: ets.lookup_element (cool_table, , 2) # => 5,6
Hvis du bare vil sjekke om noen nøkkel er til stede i tabellen, bruk medlem / 2
, som returnerer heller ekte
eller falsk
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: tall, 5, : tall, 6]) hvis: ets.member (cool_table, IO.inspect: ets.lookup_element (cool_table,: tall, 2) # => 5,6 ende
Du kan også få den første eller siste nøkkelen i et bord ved å bruke første / 1
og siste / 1
henholdsvis:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.last (cool_table) |> IO.inspect # =>: b: ets.first (cool_table) |> IO.inspect # =>: a
Dessuten er det mulig å bestemme forrige eller neste nøkkel basert på den medfølgende. Hvis en slik nøkkel ikke kan bli funnet, : "$ End_of_table"
vil bli returnert:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.prev (cool_table,: b) |> IO.inspect # =>: a: ets.next (cool_table,: a) |> IO.inspect # =>: b: ets.prev (cool_table,: a) |> IO.inspect # =>: "$ end_of_table "
Vær imidlertid oppmerksom på at tabellen traverserer ved hjelp av funksjoner som først
, neste
, siste
eller prev
er ikke isolert. Det betyr at en prosess kan fjerne eller legge til flere data i tabellen mens du er iterating over det. En måte å løse dette problemet på er å bruke safe_fixtable / 2
, som fikserer bordet og sikrer at hvert element bare hentes en gang. Tabellen forblir løst med mindre prosessen frigjør det:
cool_table =: ets.new (: cool_table, [: bag]): ets.safe_fixtable (cool_table, true): ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => 256000, [#PID<0.69.0>, 1]: ets.safe_fixtable (cool_table, false) # => tabellen er utgitt på dette tidspunktet: ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => false
Til slutt, hvis du vil finne et element i tabellen og fjerne det, bruker du ta / 2
funksjon:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.take (cool_table,: b) |> IO.inspect # => [b: 3]: ets.take (cool_table,: b) | IO.inspect # => []
Ok, så nå la oss si at du ikke lenger trenger bordet og ønsker å bli kvitt det. Bruk slette / 1
for det:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.delete (cool_table)
Selvfølgelig kan du slette en rad (eller flere rader) med nøkkelen også:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, [: b, 3, : a, 100]): ets.delete (cool_table,
For å fjerne hele bordet, bruk delete_all_objects / 1
:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, [: b, 3, : a, 100]): ets.delete_all_objects (cool_table)
Og til slutt, for å finne og fjerne et bestemt objekt, bruk delete_object / 2
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.delete_object (cool_table, : a, 3 ): ets.lookup (cool_table,: a) | IO.inspect # => [a: 100]
En ETS-tabell kan konverteres til en liste når som helst ved å bruke tab2list / 1
funksjon:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.tab2list (cool_table) |> IO.inspect # => [a: 3, a: 100]
Husk at henting av data fra bordet med tastene er en veldig rask operasjon, og du bør holde fast ved det hvis det er mulig.
Du kan også dumpe bordet til en fil ved hjelp av tab2file / 2
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.tab2file (cool_table, 'cool_table.txt' ) |> IO.inspect # =>: ok
Legg merke til at det andre argumentet skal være en charlist (en enkeltnotert streng).
Det finnes en håndfull andre operasjoner som kan brukes på ETS-tabellene, og selvfølgelig skal vi ikke diskutere dem alle. Jeg anbefaler virkelig å skumre gjennom Erlang-dokumentasjonen på ETS for å lære mer.
For å oppsummere fakta som vi har lært så langt, la oss endre et enkelt program som jeg har presentert i artikkelen min om GenServer. Dette er en modul som kalles CalcServer
som lar deg utføre ulike beregninger ved å sende forespørsler til serveren eller hente resultatet:
defmodule CalcServer bruker GenServer def start (initial_value) gjør GenServer.start (__ MODULE__, initial_value, navn: __MODULE__) avslutt def init (initial_value) når is_number (initial_value) gjør : ok, initial_value stopp, "Verdien må være et heltall!" End def sqrt gjør GenServer.cast (__ MODULE__,: sqrt) ende def add (tall) gjør GenServer.cast (__ MODULE__, : add, number) ende def multiplisere ) gjør GenServer.cast (__ MODULE__, : multiply, nummer) ende def div (nummer) gjør GenServer.cast (__ MODULE__, : div, nummer) ende def resultat gjør GenServer.call (__ MODULE__,: result) end def handle_call (: result, _ state) gjør : svar, state, state ende def handle_cast (operasjon, state) gjør saksoperasjon gjør: sqrt -> : noreply: math.sqrt (state) multiplikator -> : noreply, stat * multiplikator : div, tall -> : noreply, state / number : add, number -> : noreply, state + nummer _ -> , "Ikke implementert", state ende ende def terminate (_reason, _state) gjør IO.puts "Serverterminalen ted "slutten CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875
For øyeblikket støtter ikke serveren vår alle matematiske operasjoner, men du kan utvide den etter behov. Også, min andre artikkel forklarer hvordan man konverterer denne modulen til et program og dra nytte av veiledere for å ta vare på serveren krasjer.
Det jeg vil gjerne gjøre nå, er å legge til en annen funksjon: evnen til å logge alle matematiske operasjoner som ble utført sammen med det overførte argumentet. Disse operasjonene lagres i en ETS-tabell slik at vi senere kan hente den.
Først av alt, endre i det
fungere slik at et nytt navngitt privat bord med en type : duplicate_bag
er skapt. Vi bruker : duplicate_bag
fordi to identiske operasjoner med samme argument kan utføres:
def init (initial_value) når is_number (initial_value) gjør: ets.new (: calc_log, [: duplicate_bag,: private,: named_table]) : ok, initial_value slutt
Nå juster du handle_cast
tilbakeringing slik at den logger den forespurte operasjonen, utarbeider en formel, og utfører deretter den faktiske beregningen:
def handle_cast (drift, state) gjør operasjon |> prepare_and_log |> beregne (state) end
Her er prepare_and_log
privat funksjon:
defp prepare_and_log (operasjon) gjør drift |> logg saksoperasjon gjør: sqrt -> fn (current_value) ->: math.sqrt (current_value) ende : multiply, tall -> fn (current_value) -> current_value * : div, tall -> fn (nåværende verdi) -> nåværende verdi / nummer ende : legg til, tall -> fn (nåværende verdi) -> nåværende_verdien + tallendene _ -> null ende
Vi logger operasjonen med en gang (den tilsvarende funksjonen presenteres på et øyeblikk). Deretter returnerer du den aktuelle funksjonen eller nil
hvis vi ikke vet hvordan man skal håndtere operasjonen.
Når det gjelder Logg
funksjon, bør vi enten støtte en tuple (som inneholder både operasjonens navn og argumentet) eller et atom (som bare inneholder operasjonens navn, for eksempel, : sqrt
):
def log (operasjon) når is_tuple (operasjon) gjør: ets.insert (: calc_log, operasjon) ende def log (operasjon) når is_atom (operasjon) gjør: ets.insert (: calc_log, operasjon, null) slutten def log (_) gjør: ets.insert (: calc_log, : unsupported_operation, null) slutten
Deretter regne ut
funksjon, som enten returnerer et riktig resultat eller en stoppmelding:
defp beregne (func state) når is_function (func) gjør : noreply, func. (state) ende defp beregne (_func, state) gjør : stop, "Ikke implementert", state end
Til slutt, la oss presentere en ny grensesnittfunksjon for å hente alle utførte operasjoner av deres type:
def operasjoner (type) gjør GenServer.call (__ MODULE__, : operations, type) slutt
Håndter anropet:
def handle_call (: operasjoner, type, _, state) gjør : svar, fetch_operations_by (type), state end
Og utfør selve oppslaget:
defp fetch_operations_by (type) gjør: ets.lookup (: calc_log, type) ende
Test nå alt:
CalcServer.start (6.1) CalcServer.sqrt CalcServer.add (1) CalcServer.multiply (2) CalcServer.add (2) CalcServer.result |> IO.inspect # => 8.939635614091387 CalcServer.operations (: add) |> IO. inspiser # => [legg til: 1, legg til: 2]
Resultatet er riktig fordi vi har utført to :Legg til
operasjoner med argumentene 1
og 2
. Selvfølgelig kan du videre utvide dette programmet slik det passer best. Likevel, misbruk ikke ETS-tabeller, og bruk dem når det virkelig kommer til å øke ytelsen. I mange tilfeller er det å bruke immutables en bedre løsning.
Før innpakning av denne artikkelen ville jeg si et par ord om diskbaserte ETS-tabeller eller bare DETS.
DETS er ganske lik ETS: de bruker tabeller for å lagre ulike data i form av tuples. Forskjellen, som du har gjettet, er at de stole på lagringsplass i stedet for minne og har færre funksjoner. DETS har funksjoner som ligner de som vi diskuterte ovenfor, men enkelte operasjoner utføres litt annerledes.
For å åpne et bord, må du bruke enten open_file / 1
eller open_file / 2
-det er ingen nye / 2
fungere som i : ets
modul. Siden vi ikke har noen eksisterende tabell ennå, la oss holde oss til open_file / 2
, som skal lage en ny fil for oss:
: dets.open_file (: file_table, [])
Filnavnet er som tabellens navn som standard, men dette kan endres. Det andre argumentet gikk til åpen fil
er listen over alternativer skrevet i form av tuples. Det finnes en håndfull tilgjengelige alternativer som :adgang
eller : auto_save
. For eksempel, for å endre et filnavn, bruk følgende alternativ:
: dets.open_file (: file_table, [: file, 'cool_table.txt'])
Legg merke til at det også er a :type
alternativ som kan ha en av følgende verdier:
:sett
:bag
: duplicate_bag
Disse typene er de samme som for ETS. Vær oppmerksom på at DETS ikke kan ha en type : ordered_set
.
Det er ingen : named_table
alternativet, slik at du alltid kan bruke tabellens navn for å få tilgang til det.
En annen ting å nevne er at DETS-tabellene må være ordentlig lukket:
: Dets.close (: file_table)
Hvis du ikke gjør dette, repareres bordet neste gang det åpnes.
Du utfører lese- og skriveoperasjoner som du gjorde med ETS:
: dets.open_file (: file_table, [: file, 'cool_table.txt']): dets.insert (: file_table, : a, 3): dets.lookup (: file_table,: a) | IO .inspect # => [a: 3]: dets.close (: file_table)
Vær imidlertid oppmerksom på at DETS er tregere enn ETS fordi Elixir trenger tilgang til disken som selvfølgelig tar lengre tid.
Merk at du kan konvertere ETS og DETS tabeller frem og tilbake med letthet. For eksempel, la oss bruke to_ets / 2
og kopier innholdet i DETS-tabellen i minnet:
: dets.open_file (: file_table, [: file, 'cool_table.txt']): dets.insert (: file_table, : a, 3) my_ets =: ets.new (: my_ets, []): dets.to_ets (: file_table, my_ets): dets.close (: file_table): ets.lookup (my_ets,: a) |> IO.inspect # => [a: 3]
Kopier ETS innhold til DETS bruk to_dets / 2
:
my_ets =: ets.new (: my_ets, []): ets.insert (my_ets, : a, 3): dets.open_file (: file_table, [: file, 'cool_table.txt']): ets .to_dets (my_ets,: file_table): dets.lookup (: file_table,: a) |> IO.inspect # => [a: 3]: dets.close (: file_table)
For å oppsummere er diskbasert ETS en enkel måte å lagre innhold på i filen, men denne modulen er litt mindre kraftig enn ETS, og operasjonene er også langsommere.
I denne artikkelen har vi snakket om ETS og diskbaserte ETS-tabeller som tillater oss å lagre vilkårlig vilkår i minnet og i henholdsvis filer. Vi har sett hvordan du lager slike tabeller, hvilke tilgjengelige typer, hvordan du utfører lese- og skriveoperasjoner, hvordan du ødelegger tabeller og hvordan du konverterer dem til andre typer. Du kan finne mer informasjon om ETS i Elixir-veiledningen og på Erlangs offisielle side.
Igjen, ikke bruk ETS-tabellene for mye, og prøv å holde fast med immutables hvis det er mulig. I noen tilfeller kan ETS imidlertid være en fin ytelse, så det er nyttig å vite om denne løsningen.
Forhåpentligvis har du hatt glede av denne artikkelen. Som alltid, takk for at du bodde hos meg, og ser deg veldig snart!