socialgekon.com
  • Glavni
  • Urejanje
  • Nasveti za iOS
  • Inženirski Management
  • Orodja In Vaje
Back-End

Rubinova sočasnost in paralelizem: Praktična vadnica

Začnimo z razčiščevanjem preveč pogostih točk zmede med Razvijalci Ruby ; in sicer: Sočasnost in vzporednost sta ne ista stvar (tj. sočasno! = vzporedno).

Zlasti Ruby sočasnost je, ko se lahko dve nalogi zaženeta, zaženeta in dokončata prekrivanje časovna obdobja. Ne pomeni pa nujno, da se bosta kdaj vsaka izvajala hkrati (npr. Več niti na enojedrnem računalniku). V nasprotju, vzporednost je, ko se dve nalogi dobesedno zaženeta ob istem času (npr. več niti v večjedrnem procesorju).

Ključna točka tukaj je, da bodo hkratne niti in / ali procesi Ni nujno teči vzporedno.



Ta vadnica nudi praktično (in ne teoretično) obravnavo različnih tehnik in pristopov, ki so na voljo za sočasnost in vzporednost v Rubyju.

Za več primerov Rubyja iz resničnega sveta si oglejte naš članek o Tolmači in časi izvajanja Ruby .

Naš testni primer

Za preprost testni primer bom ustvaril Mailer razredu in dodajte funkcijo Fibonacci (namesto metode sleep()), da bo vsaka zahteva bolj zahtevna CPU, kot sledi:

class Mailer def self.deliver(&block) mail = MailBuilder.new(&block).mail mail.send_mail end Mail = Struct.new(:from, :to, :subject, :body) do def send_mail fib(30) puts 'Email from: #{from}' puts 'Email to : #{to}' puts 'Subject : #{subject}' puts 'Body : #{body}' end def fib(n) n <2 ? n : fib(n-1) + fib(n-2) end end class MailBuilder def initialize(&block) @mail = Mail.new instance_eval(&block) end attr_reader :mail %w(from to subject body).each do |m| define_method(m) do |val| @mail.send('#{m}=', val) end end end end

Nato lahko uporabimo to Mailer razred, kot sledi za pošiljanje pošte:

Mailer.deliver do from ' [email protected] ' to ' [email protected] ' subject 'Threading and Forking' body 'Some content' end

(Opomba: izvorna koda za ta testni primer je na voljo tukaj na githubu.)

Če želite določiti izhodišče za primerjavo, začnimo s preprostim primerjalnim preizkusom in 100-krat pokličemo pošiljatelja:

puts Benchmark.measure{ 100.times do |i| Mailer.deliver do from 'eki_#{i}@eqbalq.com' to 'jill_#{i}@example.com' subject 'Threading and Forking (#{i})' body 'Some content' end end }

To je dalo naslednje rezultate za štirijedrni procesor z MRI Ruby 2.0.0p353:

15.250000 0.020000 15.270000 ( 15.304447)

Več procesov v primerjavi z večnitnostjo

Glede odločitve, ali želite uporabiti več procesov ali večnitno aplikacijo Ruby, ni nobenega odgovora 'ena velikost za vse'. Spodnja tabela povzema nekatere ključne dejavnike, ki jih je treba upoštevati.

Procesi Niti
Uporablja več pomnilnika Uporablja manj pomnilnika
Če starš umre, preden otroci zapustijo otroke, lahko otroci postanejo zombi procesi Vse niti umrejo, ko postopek umre (ni možnosti za zombije)
Dražje za forkirane procese za preklop konteksta, saj mora OS vse shraniti in znova naložiti Niti imajo precej manj režijskih stroškov, saj si delijo naslovni prostor in pomnilnik
Razcepljeni procesi dobijo nov prostor navideznega pomnilnika (izolacija procesa) Niti imajo isti pomnilnik, zato je treba nadzirati in reševati sočasne težave s pomnilnikom
Zahteva medprocesno komunikacijo Lahko 'komunicira' prek čakalne vrste in skupni spomin
Počasneje ustvarjati in uničiti Hitreje ustvariti in uničiti
Lažje kodiranje in odpravljanje napak Za kodiranje in odpravljanje napak je lahko bistveno bolj zapleteno

Primeri rešitev Ruby, ki uporabljajo več procesov:

  • Blaginja : Knjižnica Ruby, ki jo podpira Redis, za ustvarjanje opravil v ozadju, njihovo postavitev v več čakalnih vrst in kasnejšo obdelavo.
  • Samorog : Strežnik HTTP za aplikacije Rack, namenjen samo hitrim odjemalcem z nizko zakasnitvijo in širokopasovno povezavo ter izkoriščanjem funkcij v jedrih, podobnih Unix / Unix.

Primeri rešitev Ruby, ki uporabljajo večnitnost:

  • Sidekiq : Popolnoma opremljen okvir za obdelavo ozadja za Ruby. Njegov namen je enostavna integracija s katero koli sodobno aplikacijo Rails in veliko večja zmogljivost kot druge obstoječe rešitve.
  • Puma : Spletni strežnik Ruby, zgrajen za sočasno delovanje.
  • Tanek : Zelo hiter in preprost spletni strežnik Ruby.

Več procesov

Preden preučimo večnitne možnosti Ruby, raziščimo lažjo pot drstenja več procesov.

V Rubyju je fork() sistemski klic se uporablja za ustvarjanje 'kopije' trenutnega procesa. Ta novi postopek je načrtovan na ravni operacijskega sistema, tako da se lahko izvaja hkrati s prvotnim postopkom, tako kot lahko vsak drug neodvisen postopek. ( Opomba: fork() je sistemski klic POSIX in zato ni na voljo, če uporabljate Ruby na platformi Windows.)

V redu, zaženimo naš testni primer, vendar tokrat z uporabo fork() za uporabo več procesov:

puts Benchmark.measure{ 100.times do |i| fork do Mailer.deliver do from 'eki_#{i}@eqbalq.com' to 'jill_#{i}@example.com' subject 'Threading and Forking (#{i})' body 'Some content' end end end Process.waitall }

(Process.waitall čaka na vse podrejeni proces za izhod in vrne vrsto procesnih stanj.)

Ta koda zdaj daje naslednje rezultate (spet pri štirijedrnem procesorju z MRI Ruby 2.0.0p353):

0.000000 0.030000 27.000000 ( 3.788106)

Ne preveč umazano! Pošiljatelj smo naredili ~ 5-krat hitrejši s samo spremembo nekaj vrstic kode (tj. Z uporabo fork()).

Ne bodite pa preveč navdušeni. Čeprav je morda skušnjava uporabiti forking, saj je enostavna rešitev za Rubyjevo sočasno, ima veliko pomanjkljivost, to je količino pomnilnika, ki jo bo porabil. Vilice so nekoliko drage, še posebej, če a Kopiranje na zapis (CoW) ne uporablja tolmač Ruby, ki ga uporabljate. Če vaša aplikacija na primer porabi 20 MB pomnilnika, lahko na primer 100-kratno viličenje porabi kar 2 GB pomnilnika!

Čeprav ima večnitnost tudi svoje zapletenosti, je pri uporabi fork() treba upoštevati številne zapletenosti, kot so skupni deskriptorji datotek in semaforji (med nadrejenimi in podrejenimi forkiranimi procesi), potreba po komunikaciji po ceveh itd.

Ruby Multithreading

V redu, zato poskusimo isti program hitreje uporabiti z uporabo večnamenskih tehnik Ruby.

Več niti znotraj enega procesa imajo bistveno manj režijskih stroškov kot ustrezno število procesov, saj si delijo naslovni prostor in pomnilnik.

S tem v mislih ponovno poglejmo naš testni primer, a tokrat uporabimo Rubyev Thread razred:

threads = [] puts Benchmark.measure{ 100.times do |i| threads << Thread.new do Mailer.deliver do from 'eki_#{i}@eqbalq.com' to 'jill_#{i}@example.com' subject 'Threading and Forking (#{i})' body 'Some content' end end end threads.map(&:join) }

Ta koda zdaj daje naslednje rezultate (spet pri štirijedrnem procesorju z MRI Ruby 2.0.0p353):

13.710000 0.040000 13.750000 ( 13.740204)

Udarec. To zagotovo ni zelo impresivno! Torej, kaj se dogaja? Zakaj to daje skoraj enake rezultate, kot smo jih dobili, ko smo kodo zagnali sinhrono?

Odgovor, ki je nagnjen k obstoju mnogih programerjev Ruby, je Global Interpreter Lock (GIL) . Zahvaljujoč GIL-u CRuby (izvedba MRI) v resnici ne podpira navojev.

The Global Interpreter Lock je mehanizem, ki se uporablja v tolmačih računalniškega jezika za sinhronizacijo izvajanja niti, tako da lahko hkrati izvaja samo eno nit. Tolmač, ki uporablja GIL, bo nenehno dovoli točno eno nit in eno nit samo za izvajanje naenkrat , tudi če deluje na večjedrnem procesorju. Ruby MRI in CPython sta najpogostejša primera priljubljenih tolmačev z GIL.

Torej, nazaj k naši težavi, kako lahko izkoristimo večnitnost v Rubyju za izboljšanje zmogljivosti glede na GIL?

No, v MRI (CRuby) je žalostni odgovor, da ste v bistvu zataknjeni in da vam večnitnost lahko naredi zelo malo.

Hkrati rubi brez paralelizma je lahko še vedno zelo uporaben za naloge, ki so težke z IO (npr. Za naloge, ki jih je treba pogosto čakati v omrežju). Torej niti lahko še vedno koristno pri magnetno resonančni tehniki za IO težka opravila. Obstaja razlog, da so bile niti navsezadnje izumljene in uporabljene še preden so bili večjedrni strežniki pogosti.

Toda če imate možnost, da uporabite različico, ki ni CRuby, lahko uporabite drugo izvedbo Rubyja, kot je JRuby ali Rubinius , ker nimajo GIL-a in podpirajo resnično vzporedno navajanje Ruby-jevih navojev.

navoj z JRubyjem

Da bi dokazali to, tukaj so rezultati, ki jih dobimo, ko zaženemo popolnoma isto različico kode z navoji kot prej, vendar jo tokrat zaženimo na JRuby (namesto na CRuby):

43.240000 0.140000 43.380000 ( 5.655000)

Zdaj se pogovarjava!

Ampak ...

Niti niso brezplačne

Izboljšana zmogljivost z več nitmi lahko privede do tega, da lahko nekdo verjame, da lahko še naprej dodajamo več niti - v bistvu neskončno - da bo naša koda delovala hitreje in hitreje. To bi bilo res lepo, če bi bilo res, toda resničnost je, da niti niso zastonj, zato vam bodo prej ali slej zmanjkalo sredstev.

Recimo, na primer, da želimo pošiljati vzorce ne 100-krat, ampak 10 000-krat. Poglejmo, kaj se bo zgodilo:

threads = [] puts Benchmark.measure{ 10_000.times do |i| threads << Thread.new do Mailer.deliver do from 'eki_#{i}@eqbalq.com' to 'jill_#{i}@example.com' subject 'Threading and Forking (#{i})' body 'Some content' end end end threads.map(&:join) }

Bum! Po pojavu približno 2.000 niti sem dobil napako z OS X 10.8:

can't create Thread: Resource temporarily unavailable (ThreadError)

Po pričakovanjih prej ali slej začnemo razbijati ali v celoti zmanjka virov. Torej je razširljivost tega pristopa očitno omejena.

Združevanje niti

Na srečo obstaja boljši način; in sicer združevanje niti.

Področje niti je skupina vnaprej izdelanih niti, ki jih je mogoče ponovno uporabiti in ki so na voljo za izvedbo del po potrebi. Skupine niti so še posebej koristne, kadar je treba opraviti veliko število kratkih nalog in ne majhnega števila daljših nalog. To preprečuje, da bi večkrat nastali režijski stroški ustvarjanja niti.

Ključni konfiguracijski parameter za področje niti je običajno število niti v področju. Te niti je mogoče ustvariti naenkrat naenkrat (tj. Ko je bazen ustvarjen) ali lenobno (tj. Po potrebi, dokler ni ustvarjeno največje število niti v področju).

Ko je združenju predana naloga, ki jo mora izvesti, jo dodeli eni od trenutno nedejavnih niti. Če nobena nit ne deluje (in je že ustvarjeno največje število niti), počaka, da nit dokonča svoje delo in postane nedejaven, nato pa tej nalogi dodeli nalogo.

Združevanje niti

Torej, ko se vrnemo k našemu primeru, bomo začeli z uporabo Queue (saj je varno na nit podatkovni tip) in uporabite preprosto izvedbo področja niti:

zahtevajo './lib/mailer' zahtevajo 'primerjalno točko' zahtevajo 'nit'

POOL_SIZE = 10 jobs = Queue.new 10_0000.times jobs.push i workers = (POOL_SIZE).times.map do Thread.new do begin while x = jobs.pop(true) Mailer.deliver do from 'eki_#{x}@eqbalq.com' to 'jill_#{x}@example.com' subject 'Threading and Forking (#{x})' body 'Some content' end end rescue ThreadError end end end workers.map(&:join)

V zgornji kodi smo začeli z ustvarjanjem jobs čakalna vrsta za opravila, ki jih je treba opraviti. Uporabili smo Queue v ta namen, ker je varno pred nitmi (torej, če do njega hkrati dostopa več niti, bo ohranilo doslednost), kar preprečuje potrebo po bolj zapleteni izvedbi, ki zahteva uporabo mutex .

Nato smo ID-je pošiljateljev potisnili v čakalno vrsto in ustvarili svoj nabor 10 delovnih niti.

Znotraj vsake niti delavca izpišemo elemente iz čakalne vrste opravil.

Tako je življenjski cikel delovne niti nenehno čakati, da se opravila postavijo v čakalno vrsto opravil, in jih izvajati.

Dobra novica je torej, da to deluje in se brez težav širi. Na žalost pa je to precej zapleteno tudi za našo preprosto vadnico.

Celuloid

Zahvaljujoč Ruby Gem ekosistema, je večina zapletenosti večnitnosti lepo vključena v številne preproste Ruby Gems, ki so že na voljo.

Odličen primer je Celluloid, eden mojih najljubših rubinastih draguljev. Celluloid framework je preprost in čist način za izvajanje sočasnih sistemov, ki temeljijo na igralcih, v Rubyju. Celuloid omogoča ljudem, da gradijo sočasne programe iz sočasnih objektov enako enostavno kot zaporedne programe iz zaporednih objektov.

V okviru naše razprave v tej objavi se posebej osredotočam na funkcijo Pools, vendar si naredite uslugo in si jo podrobneje oglejte. Z uporabo Celluloida boste lahko sestavljali večnitne programe Ruby, ne da bi vas skrbele grde težave, kot so mrtve točke, in zdi se vam nepomembno uporabljati druge bolj dovršene funkcije, kot so Futures in Promises.

Evo, kako preprosta večnitna različica našega poštnega programa uporablja Celluloid:

require './lib/mailer' require 'benchmark' require 'celluloid' class MailWorker include Celluloid def send_email(id) Mailer.deliver do from 'eki_#{id}@eqbalq.com' to 'jill_#{id}@example.com' subject 'Threading and Forking (#{id})' body 'Some content' end end end mailer_pool = MailWorker.pool(size: 10) 10_000.times do |i| mailer_pool.async.send_email(i) end

Čist, enostaven, prilagodljiv in robusten. Kaj še lahko zahtevate?

Ozadja

Seveda bi lahko uporabili še eno potencialno izvedljivo alternativo, odvisno od vaših operativnih zahtev in omejitev delovna mesta v ozadju . Obstajajo številni Ruby Gems, ki podpirajo obdelavo v ozadju (tj. Shranjevanje opravil v čakalno vrsto in kasnejša obdelava, ne da bi blokirali trenutno nit). Pomembni primeri vključujejo Sidekiq , Blaginja , Zakasnjeno delo , in Beanstalkd .

Za to objavo bom uporabil Sidekiq in Redis (odprtokodni predpomnilnik in shramba ključ-vrednost).

Najprej namestimo Redis in ga zaženimo lokalno:

brew install redis redis-server /usr/local/etc/redis.conf

Z zagnanim lokalnim primerkom Redisa si oglejmo različico našega vzorčnega programa za pošiljanje (mail_worker.rb) z uporabo Sidekiq:

require_relative '../lib/mailer' require 'sidekiq' class MailWorker include Sidekiq::Worker def perform(id) Mailer.deliver do from 'eki_#{id}@eqbalq.com' to 'jill_#{id}@example.com' subject 'Threading and Forking (#{id})' body 'Some content' end end end

Sidekiq lahko sprožimo s mail_worker.rb mapa:

sidekiq -r ./mail_worker.rb

In potem od IRB :

⇒ irb >> require_relative 'mail_worker' => true >> 100.times 2014-12-20T02:42:30Z 46549 TID-ouh10w8gw INFO: Sidekiq client with redis options {} => 100

Izredno preprosto. In se lahko enostavno poveča s samo spreminjanjem števila delavcev.

Druga možnost je uporaba Prikriti udarec , ena mojih najljubših knjižnic za asinhrono obdelavo RoR. Izvedba z uporabo Sucker Punch bo zelo podobna. Vključiti bomo morali samo SuckerPunch::Job namesto Sidekiq::Worker in MailWorker.new.async.perform() prej MailWorker.perform_async().

Zaključek

Visoke sočasnosti ni mogoče doseči samo v Ruby , vendar je tudi preprostejši, kot si morda mislite.

Eden od izvedljivih pristopov je preprosto oblikovanje tekočega procesa, da pomnožite njegovo procesorsko moč. Druga tehnika je izkoristiti večnitnost. Čeprav so niti lažje od procesov in zahtevajo manj režijskih stroškov, lahko vseeno zmanjka virov, če hkrati zaženete preveč niti. Na neki točki se vam bo morda zdelo potrebno uporabiti področje niti. Na srečo je veliko zapletenosti večnitnosti olajšano z izkoriščanjem katerega koli od razpoložljivih draguljev, kot sta Celluloid in njegov model Actor.

Drug način za obdelavo zamudnih procesov je uporaba obdelave v ozadju. Obstaja veliko knjižnic in storitev, ki vam omogočajo izvajanje opravil v ozadju v vaših aplikacijah. Nekatera priljubljena orodja vključujejo okvire opravil, ki jih podpira baza podatkov, in čakalne vrste sporočil.

Forking, rezanje navojev in obdelava ozadja so vse izvedljive alternative. Odločitev, katero uporabiti, je odvisna od narave vaše aplikacije, vašega operativnega okolja in zahtev. Upajmo, da je ta vadnica uporaben uvod v razpoložljive možnosti.

Potreba po hitrosti: retrospektiva ApeeScape JavaScript Coding Challenge

Back-End

Potreba po hitrosti: retrospektiva ApeeScape JavaScript Coding Challenge
Najboljši čas za objavo na TikTok za rekordne oglede v letu 2021

Najboljši čas za objavo na TikTok za rekordne oglede v letu 2021

Objava

Priljubljene Objave
Pregled iPhone SE 2020: Se ga splača kupiti za fotografiranje?
Pregled iPhone SE 2020: Se ga splača kupiti za fotografiranje?
Kako narediti posnetek zaslona na iPhoneu: 5 načinov za zajem zaslona
Kako narediti posnetek zaslona na iPhoneu: 5 načinov za zajem zaslona
Kako urejati videoposnetke v iPhonu s fotografijami in iMovie
Kako urejati videoposnetke v iPhonu s fotografijami in iMovie
Kako nastaviti arhitekturo mikro storitev v Ruby: Vodnik po korakih
Kako nastaviti arhitekturo mikro storitev v Ruby: Vodnik po korakih
Cordova Frameworks: Ionic vs. Framework7
Cordova Frameworks: Ionic vs. Framework7
 
Kako snemati družinsko fotografijo doma samo z vašim iPhoneom
Kako snemati družinsko fotografijo doma samo z vašim iPhoneom
Stalni podatki na straneh, ki se ponovno naložijo: piškotki, IndexedDB in vse, kar je vmes
Stalni podatki na straneh, ki se ponovno naložijo: piškotki, IndexedDB in vse, kar je vmes
Razbijanje procesa oblikovalskega razmišljanja
Razbijanje procesa oblikovalskega razmišljanja
Vodnik za oddaljene delavce, kako ostati zdrav
Vodnik za oddaljene delavce, kako ostati zdrav
Kako implementirati popoln dizajn uporabniškega vmesnika za iOS
Kako implementirati popoln dizajn uporabniškega vmesnika za iOS
Kategorije
Podatkovne Vede In Zbirke PodatkovKpi In AnalitikaStreljanjeProces In OrodjaInženirski ManagementOdpravljanje težavDrugoŽivljenjski Cikel IzdelkaMobilno OblikovanjeIzdelki In Ekipe

© 2023 | Vse Pravice Pridržane

socialgekon.com