Atunci când realizați un program Elixir, adesea trebuie să împărțiți un stat. De exemplu, într-unul din articolele anterioare am arătat cum să codificați un server pentru a efectua calcule diferite și să păstrați rezultatul în memorie (și mai târziu am văzut cum să facem acest server cu bullet-proof cu ajutorul supraveghetorilor). Există însă o problemă: dacă aveți un singur proces care are grijă de stat și multe alte procese care îl accesează, performanța poate fi grav afectată. Acest lucru se datorează faptului că procesul poate difuza o singură cerere la un moment dat.
Cu toate acestea, există modalități de depășire a acestei probleme și astăzi vom vorbi despre una dintre ele. Faceți cunoștință cu mesele de depozitare Erlang Term sau pur și simplu Tabele ETS, o memorie rapidă în memorie care poate găzdui tupluri de date arbitrare. După cum sugerează și numele, aceste tabele au fost inițial introduse în Erlang, dar, ca și în cazul oricărui alt modul Erlang, le putem folosi cu ușurință și în Elixir.
În acest articol veți:
Toate exemplele de cod lucrează atât cu Elixir 1.4 cât și cu 1.5, care a fost lansat recent.
După cum am menționat mai devreme, tabelele ETS sunt stocare în memorie care conține tupluri de date (numite rânduri). Mai multe procese pot accesa tabelul prin id-ul sau un nume reprezentat ca un atom și pot efectua citirea, scrierea, ștergerea și alte operații. Tabelele ETS sunt create printr-un proces separat, deci dacă acest proces este terminat, tabela este distrusă. Cu toate acestea, nu există niciun mecanism automat de colectare a gunoiului, astfel încât masa poate să stea în memorie de ceva timp.
Datele din tabelul ETS sunt reprezentate de un tuplu : cheie, valoare1, valoare2, valoare
. Puteți căuta cu ușurință datele prin cheia sau inserați un rând nou, dar în mod prestabilit nu pot fi două rânduri cu aceeași cheie. Operațiile bazate pe chei sunt foarte rapide, dar dacă dintr-un anumit motiv trebuie să realizați o listă dintr-o tabelă ETS și, de exemplu, să efectuați manipulări complexe ale datelor, este posibil și.
În plus, există tabele ETS bazate pe disc care stochează conținutul lor într-un fișier. Desigur, acestea funcționează mai lent, dar în acest fel veți obține o stocare simplă a fișierelor fără nici un fel de hassle. În plus, ETS în memorie poate fi ușor convertit în disc și viceversa.
Deci, cred că este timpul să începem călătoria și să vedem cum sunt create mesele ETS!
Pentru a crea un tabel ETS, folosiți noi / 2
funcţie. Atâta timp cât folosim un modul Erlang, numele său trebuie scris ca un atom:
cool_table =: ets.new (: cool_table, [])
Rețineți că, până de curând, ați putut crea numai până la 1400 de tabele per instanță BEAM, dar acest lucru nu mai este valabil - vă limitați doar la cantitatea de memorie disponibilă.
Primul argument a fost transmis nou
funcția este numele tabelului (alias), în timp ce al doilea conține o listă de opțiuni. cool_table
variabila conține acum un număr care identifică tabelul din sistem:
IO.inspect cool_table # => 12306
Acum puteți utiliza această variabilă pentru a efectua operațiile ulterioare la tabel (de exemplu, citiți și scrieți date).
Să vorbim despre opțiunile pe care le puteți specifica atunci când creați un tabel. Primul lucru (și ceva ciudat) de reținut este că, în mod prestabilit, nu puteți folosi aliasul tabelului în nici un fel și practic nu are efect. Dar totuși, aliasul trebuie sa să fie transmisă la crearea mesei.
Pentru a putea accesa tabelul prin aliasul acestuia, trebuie să furnizați o : named_table
această opțiune:
cool_table =: ets.new (: cool_table, [: name_table])
Apropo, dacă doriți să redenumiți masa, se poate face folosind redenumi / 2
funcţie:
: ets.rename (cool_table,: cooler_table)
Apoi, după cum sa menționat deja, un tabel nu poate conține mai multe rânduri cu aceeași cheie și acest lucru este dictat de tip. Există patru tipuri de tabele posibile:
:a stabilit
-aceasta este cea implicită. Aceasta înseamnă că nu puteți avea mai multe rânduri cu exact aceleași chei. Rândurile nu sunt rearanjate într-o manieră particulară.: ordered_set
-la fel ca :a stabilit
, dar rândurile sunt ordonate de termeni.:sac
-mai multe rânduri pot avea aceeași cheie, dar rândurile încă nu pot fi pe deplin identice.: duplicate_bag
-rândurile pot fi pe deplin identice.Există un lucru demn de menționat în ceea ce privește : ordered_set
Mese. După cum afirmă documentația Erlang, aceste mese tratează cheile ca egale atunci când aceștia comparați egal, nu numai atunci când acestea Meci. Ce inseamna asta?
Doi termeni din Erlang se potrivesc numai dacă au aceeași valoare și același tip. Deci, intreg 1
se potrivește doar cu un alt număr întreg 1
, dar nu plutesc 1.0
deoarece au diferite tipuri. Cu toate acestea, doi termeni sunt egali, în cazul în care aceștia au aceeași valoare și același tip sau dacă ambele sunt numerice și se extind la aceeași valoare. Aceasta înseamnă că 1
și 1.0
sunt comparate egale.
Pentru a furniza tipul tabelului, pur și simplu adăugați un element la lista opțiunilor:
cool_table =: ets.new (: cool_table, [: name_table,: ordered_set])
O altă opțiune interesantă pe care o puteți trece este : comprimat
. Înseamnă că datele din tabel (dar nu cheile) vor fi ghici ce-au fost stocate într-o formă compactă. Desigur, operațiunile care se execută la masă vor deveni mai lente.
În continuare, puteți controla care element din tuplă ar trebui utilizat ca cheie. Implicit, primul element (poziția 1
), dar acest lucru poate fi schimbat cu ușurință:
cool_table =: ets.new (: cool_table, [: keypos, 2])
Acum cele două elemente din tupluri vor fi tratate ca cheile.
Ultima opțiune, dar nu cea mai mică, controlează drepturile de acces ale tabelului. Aceste drepturi dictează ce procese pot accesa tabelul:
:public
-orice proces poate efectua orice operație la masă.:protejat
-valoarea implicită. Numai procesul de proprietar poate scrie la masă, dar toate procesele pot citi.:privat
-numai procesul de proprietar poate accesa tabelul.Deci, pentru a face o masă privată, ați scrie:
cool_table =: ets.new (: cool_table, [: private])
Bine, suficient de vorbit despre opțiuni - să vedem câteva operații comune pe care le poți efectua la mese!
Pentru a citi ceva din tabel, trebuie mai întâi să scrieți niște date acolo, deci începeți cu ultima operație. Folosește inserați / 2
pentru a pune datele în tabel:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : number, 5)
Puteți trece, de asemenea, o listă de piese de genul:
: ets.insert (cool_table, [: număr, 5, : șir, "test"])
Rețineți că dacă tabelul are un tip de :a stabilit
și o cheie nouă se potrivește cu cea existentă, datele vechi vor fi suprascrise. În mod similar, dacă un tabel are un tip de : ordered_set
și o cheie nouă comparată egală cu cea veche, datele vor fi suprascrise, deci acordați atenție acestui lucru.
Funcția de inserare (chiar și cu mai multe tupluri simultan) este garantată ca fiind atomică și izolată, ceea ce înseamnă că totul este stocat în tabel sau nimic. De asemenea, alte procese nu vor putea vedea rezultatul intermediar al operației. În general, acest lucru este destul de similar cu tranzacțiile SQL.
Dacă sunteți preocupat de duplicarea cheilor sau nu doriți să înlocuiți datele dvs. din greșeală, utilizați insert_new / 2
funcția. Este similar cu inserați / 2
dar nu va introduce niciodată cheile de duplicare și va reveni în schimb fals
. Acesta este cazul pentru :sac
și : duplicate_bag
și tabele:
(): ets.insert (cool_table, : number, 5): ets.insert_new (cool_table, : number, 6) |> IO.inspect # > false
Dacă furnizați o listă de tuple, fiecare cheie va fi verificată și operația va fi anulată chiar dacă una dintre chei este duplicată.
Bine, acum avem niște date în tabelul nostru - cum le putem aduce? Cea mai ușoară modalitate este de a efectua o căutare cu o cheie:
: ets.insert (cool_table, : number, 5) IO.inspect: ets.lookup (cool_table,: number) # => [număr: 5]
Amintiți-vă că pentru : ordered_set
tabel, cheia ar trebui să fie egală cu valoarea furnizată. Pentru toate celelalte tipuri de tabele, ar trebui să se potrivească. De asemenea, dacă un tabel este a :sac
sau an : ordered_bag
, căutare / 2
funcția poate returna o listă cu mai multe elemente:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : number, 5, : number, 6 ) # => [număr: 5, număr: 6]
În loc să preiați o listă, puteți să luați un element în poziția dorită folosind lookup_element / 3
funcţie:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : number, 6) IO.inspect: ets.lookup_element (cool_table,: number, 2) # => 6
În acest cod, primim rândul sub cheie :număr
și apoi luând elementul în a doua poziție. De asemenea, funcționează perfect cu :sac
sau : duplicate_bag
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : număr, 5, : număr, 6]) IO.inspect: ets.lookup_element , 2) # => 5,6
Dacă doriți să verificați dacă există o cheie în tabel, utilizați-o membru / 2
, care se întoarce fie Adevărat
sau fals
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: număr, 5, : număr, 6]) IO.inspect: ets.lookup_element (cool_table,: număr, 2) # => 5,6 final
De asemenea, puteți obține prima sau ultima cheie dintr-un tabel utilizând Primul / 1
și ultima / 1
respectiv:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, : b, 3, : a, 100 # =>: b: ets.first (cool_table) |> IO.inspect # =>: a
În plus, este posibil să se determine cheia anterioară sau următoare, pe baza celei furnizate. Dacă o astfel de cheie nu poate fi găsită, : "$ End_of_table"
va fi returnat:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, b, 3, : a, 100 IO.inspect # =>: a: ets.next (cool_table,: a) |> IO.inspect # =>: b: ets.prev (cool_table,: a) |> IO.inspect # =>: $ end_of_table "
Rețineți, totuși, că traversarea mesei utilizând funcții precum primul
, Următor →
, ultimul
sau prev
nu este izolat. Aceasta înseamnă că un proces poate elimina sau adăuga mai multe date în tabel în timp ce iterați peste el. O modalitate de a depăși această problemă este prin utilizarea safe_fixtable / 2
, care fixează masa și asigură că fiecare element va fi preluat o singură dată. Tabelul rămâne fix, cu excepția cazului în care procesul îl eliberează:
cool_table =: ets.new (: cool_table, [: bag]): ets.safe_fixtable (cool_table, true): ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => 256000,<0.69.0>, 1]: în acest moment este lansat tabelul ets.safe_fixtable (cool_table, false) # =>: ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => false
În cele din urmă, dacă doriți să găsiți un element în tabel și să îl eliminați, angajați-l ia / 2
funcţie:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, b, 3, : a, 100 IO.inspect # => [b: 3]: ets.take (cool_table,: b) |> IO.inspect # => []
Bine, deci să spunem că nu mai aveți nevoie de masă și doriți să scăpați de ea. Utilizare șterge / 1
pentru asta:
cool_table =: ets.new (: cool_table, [: ordered_set]): ets.delete (cool_table)
Desigur, puteți șterge și un rând (sau mai multe rânduri) cu ajutorul cheii sale:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, [a, 100]): ets.delete (cool_table,: a)
Pentru a elimina întreaga masă, utilizați delete_all_objects / 1
:
cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : b, 3, : a, 100]): ets.delete_all_objects
Și, în sfârșit, pentru a găsi și a elimina un anumit obiect, utilizați delete_object / 2
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : a, 3, : a, 100 ): ets.lookup (cool_table,: a) |> IO.inspect # => [a: 100]
Un tabel ETS poate fi convertit într - o listă oricând folosind tab2list / 1
funcţie:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, a, 3, : a, 100]): ets.tab2list (cool_table) # => [a: 3, a: 100]
Amintiți-vă, totuși, că preluarea datelor de pe masă de către chei este o operație foarte rapidă și ar trebui să vă lipiți, dacă este posibil.
De asemenea, puteți să aruncați masa într-un fișier utilizând tab2file / 2
:
cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : a, 3, : a, 100 ) |> IO.inspect # =>: ok
Rețineți că al doilea argument ar trebui să fie o listă de caractere (un șir dintr-o singură listă).
Există o serie de alte operațiuni disponibile care pot fi aplicate în tabelele ETS și, desigur, nu le vom discuta pe toate. Recomand cu adevărat să treceți prin documentația Erlang pe ETS pentru a afla mai multe.
Pentru a rezuma faptele pe care le-am învățat până acum, să modificăm un program simplu pe care l-am prezentat în articolul meu despre GenServer. Acesta este un modul numit CalcServer
care vă permite să efectuați calcule diferite prin trimiterea de cereri către server sau prin preluarea rezultatului:
defmodule CalcServer folosesc GenServer def start (initial_value) do GenServer.start (__ MODULE__, initial_value, nume: __MODULE__) sfarsit def init (initial_value) atunci cand este_number (initial_value) do : ok, initial_value oprire, "Valoarea trebuie să fie un număr întreg!" end end sqrt face GenServer.cast (__ MODULE__,: sqrt) end def add (număr) genServer.cast (__ MODULE__, : add, number ) genServer.cast (__ MODULE__, : multiplicare, numar) sfarsit def div (numar) genServer.cast (__ MODULE__, : div, numar) final rezultat def GenServer.call (__ MODULE__,: rezultat) (), , , , , , multiplicator -> : noreply, state * multiplicator : div, numar -> : noreply, state / numar : noreply, state + number _ , "Nu este implementat", state end end end terminate (_reason, _state) face IO.puts "Terminarea serverului ted "sfârșitul final CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875
În prezent, serverul nostru nu acceptă toate operațiile matematice, dar îl puteți extinde după cum este necesar. De asemenea, alt articol meu explică cum să convertiți acest modul într-o aplicație și să profitați de supraveghetori pentru a avea grijă de accidentele serverului.
Ceea ce aș vrea să fac acum este să adaug o altă caracteristică: abilitatea de a loga toate operațiile matematice care au fost efectuate împreună cu argumentul trecut. Aceste operațiuni vor fi stocate într-o tabelă ETS, astfel încât vom putea să o preluăm mai târziu.
Mai întâi de toate, modificați init
astfel încât un nou tabel numit privat cu un tip de : duplicate_bag
este creat. Noi folosim : duplicate_bag
deoarece se pot efectua două operații identice cu același argument:
def init (initial_value) atunci când is_number (initial_value) face: ets.new (: calc_log, [: duplicate_bag,: privat,: named_table]) : ok, initial_value sfarsit
Acum tweak handle_cast
callback, astfel încât să înregistreze operația solicitată, să pregătească o formulă și apoi să efectueze calculul real:
def handle_cast (operație, stare) operațiune |> prepare_and_log |> calculează (stat) sfârșitul
Aici este prepare_and_log
funcția privată:
defp prepare_and_log (operațiune) operațiunea |> operațiunea casetei log: sqrt -> fn (actual_value) ->: math.sqrt (curent_value) : div, număr -> fn (valoare curentă) -> valoare curentă / număr final : add, number -> fn (valoare curentă) -> valoare curentă + sfârșitul numărului -
Înregistrați imediat operația (funcția corespunzătoare va fi prezentată într-un moment). Apoi returnați funcția corespunzătoare sau zero
dacă nu știm cum să manipulăm operația.
cât despre Buturuga
, ar trebui să susținem fie o tuplă (care conține atât denumirea operației, cât și argumentul) sau un atom (care conține numai numele operației, de exemplu, : sqrt
):
def log (operație) atunci când is_tuple (operațiune) face: ets.insert (: calc_log, operație) sfârșitul def log (operațiune) atunci când is_atom (_) face: ets.insert (: calc_log, : unsupported_operation, nil) sfârșit
Apoi, calculati
, care fie returnează un rezultat bun, fie un mesaj de oprire:
(func, state) atunci când este_function (func) do : noreply, func. (state) end defp calcul (_func, state) do : stop,
În cele din urmă, să prezentăm o nouă funcție de interfață pentru a prelua toate operațiile efectuate prin tipul lor:
operațiunile def (tip) genServer.call (__ MODULE__, : operații, tip) se termină
Gestionați apelul:
def handle_call (: operațiuni, tip, _, starea) do : reply, fetch_operations_by (type), state end
Și efectuați căutarea reală:
defp fetch_operations_by (tip) face: ets.lookup (: calc_log, tip) sfârșit
Acum, testați totul:
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. inspectați # => [adăugați: 1, adăugați: 2]
Rezultatul este corect deoarece am realizat două :adăuga
operațiile cu argumentele 1
și 2
. Bineînțeles, puteți extinde acest program după cum doriți. Totuși, nu abuzați de tabelele ETS și le folosiți atunci când într-adevăr vor crește performanța - în multe cazuri, utilizarea imutabilelor este o soluție mai bună.
Înainte de a încheia acest articol, am vrut să spun câteva cuvinte despre tabelele ETS bazate pe disc sau pur și simplu despre DETS.
DETS sunt destul de asemănătoare cu ETS: folosesc tabele pentru a stoca diverse date sub formă de tupluri. Diferența, așa cum ați ghicit, este că ei se bazează pe stocarea fișierelor în loc de memorie și au mai puține caracteristici. DETS au funcții similare cu cele discutate mai sus, dar unele operații sunt realizate puțin diferit.
Pentru a deschide o masă, trebuie să utilizați oricare dintre acestea open_file / 1
sau open_file / 2
-nu este noi / 2
cum ar fi în : ets
modul. Din moment ce nu avem încă niciun tabel existent, hai să rămânem open_file / 2
, care va crea un nou dosar pentru noi:
: dets.open_file (: file_table, [])
Numele fișierului este egal cu numele tabelului în mod prestabilit, dar acesta poate fi modificat. Al doilea argument a fost transmis deschide fișierul
este lista opțiunilor scrise sub formă de tupluri. Există o mână de opțiuni disponibile cum ar fi :acces
sau :salvare automata
. De exemplu, pentru a schimba un nume de fișier, utilizați următoarea opțiune:
: dets.open_file (: file_table, [: fișier, 'cool_table.txt'])
Rețineți că există și a :tip
care poate avea una dintre următoarele valori:
:a stabilit
:sac
: duplicate_bag
Aceste tipuri sunt aceleași ca pentru ETS. Rețineți că DETS nu poate avea un tip de : ordered_set
.
Nu este : named_table
, astfel încât să puteți utiliza întotdeauna numele tabelului pentru al accesa.
Un alt lucru care merită menționat este că tabelele DETS trebuie închise corespunzător:
: Dets.close (: file_table)
Dacă nu faceți acest lucru, masa va fi reparată la următoarea deschidere.
Efectuați operații de citire și scriere exact așa cum ați făcut cu ETS:
: dets.open_file (: file_table, [: fișier, 'cool_table.txt']): dets.insert (: file_table, : a, 3): dets.lookup (: file_table,: a) | .inspect # => [a: 3]: dets.close (: file_table)
Rețineți însă că DETS sunt mai lente decât ETS deoarece Elixir va avea nevoie să acceseze discul care, desigur, necesită mai mult timp.
Rețineți că este posibil să convertiți ușor tabelele ETS și DETS înainte și înapoi. De exemplu, să folosim to_ets / 2
și copiați conținutul tabelului DETS în memorie:
: dets.open_file (: file_table, [: fișier, 'cool_table.txt']): dets.insert (: file_table, : a, 3) my_ets =: ets.new (: my_ets, [ (de) fișiere (): dets.to_ets (: file_table, my_ets): dets.close (: file_table): ets.lookup (my_ets,: a)
Copiați conținutul ETS la DETS folosind to_dets / 2
:
my_ets:: ets.new (: my_ets, []): ets.insert (my_ets, : a, 3): dets.open_file (: file_table, [file: cool_table.txt] .to_dets (my_ets,: file_table): dets.lookup (: file_table,: a) |> IO.inspect # => [a: 3]: dets.close (: file_table)
Pentru a rezuma, ETS pe disc este o modalitate simplă de a stoca conținutul în fișier, dar acest modul este puțin mai puternic decât ETS, iar operațiile sunt mai lentă.
În acest articol, am vorbit despre tabele ETS și ETS bazate pe disc care ne permit să stocăm termeni arbitrari în memorie și respectiv în fișiere. Am văzut cum să creăm astfel de tabele, ce tipuri sunt disponibile, cum să efectuați operații de citire și scriere, cum să distrugeți tabelele și cum să le convertiți în alte tipuri. Puteți găsi mai multe informații despre ETS în ghidul Elixir și pe pagina oficială Erlang.
Încă o dată, nu utilizați excesiv tabelele ETS și încercați să rămânem cu imutabile dacă este posibil. În unele cazuri, cu toate acestea, ETS poate fi un impresionant performanță, astfel încât cunoașterea acestei soluții este utilă în orice caz.
Sper că te-ai bucurat de acest articol. Ca întotdeauna, vă mulțumesc că ați rămas cu mine și vă voi vedea foarte curând!