Nagu te ilmselt teate, on Ruby'l mitmeid implementatsioone, näiteks MRI, JRuby, Rubinius, Opal, RubyMotion jne, ja igaüks neist võib kasutada erinevat koodi täitmise mustrit. See artikkel keskendub kolmele esimesele neist ja võrdleb MRI
Nagu te ilmselt teate, on Ruby'l mitmeid implementatsioone, näiteks MRI, JRuby, Rubinius, Opal, RubyMotion jne, ja igaüks neist võib kasutada erinevat mustrit. kood täitmine. Selles artiklis keskendutakse kolmele esimesele neist ja võrreldakse MRI (praegu kõige populaarsem implementatsioon) JRuby ja Rubiniusega, käivitades mõned näidisskriptid, mis peaksid hindama forki ja keermestamise sobivust erinevates olukordades, näiteks protsessori intensiivsete algoritmide töötlemisel, failide kopeerimisel jne.Enne kui hakkate "õppima tehes", peate üle vaatama mõned põhiterminid.
suhtleb teistega protsessidevahelise kommunikatsiooni (IPC) kanalite, näiteks sõnumijärjekordade, failide, sokkide jne kaudu.
on olemas ka siis, kui vanemprotsess lõpeb
on POSIX-kõne - töötab peamiselt Unixi platvormidel.
Teema
on "ainult" täitmiskontekst, mis töötab protsessis.
jagab kogu mälu teistega (vaikimisi kasutab see vähem mälu kui kahvel)
suhtleb teistega jagatud mäluobjektide kaudu
sureb protsessiga
toob kaasa tüüpilised mitmikeeramisprobleemid, nagu nälgimine, ummikseisud jne.
On palju vahendeid, mis kasutavad forke ja niite, mida kasutatakse igapäevaselt, nt Unicorn (forks) ja Puma (niidid) rakendusserverite tasandil, Resque (forks) ja Sidekiq (niidid) taustatööde tasandil jne.
Järgnevas tabelis on esitatud hargnemise ja lõimimise tugi peamistes Ruby implementatsioonides.
Ruby rakendamine
Forking
Keermestamine
MRI
Jah
Jah (piiratud GILiga**)
JRuby
–
Jah
Rubinius
Jah
Jah
Veel kaks maagilist sõna tulevad selles teemas tagasi nagu bumerang - paralleelsus ja samaaegsus - peame neid veidi selgitama. Esiteks ei saa neid mõisteid kasutada omavahel vahetatavalt. Lühidalt - paralleelsusest saame rääkida siis, kui kahte või enamat ülesannet töödeldakse täpselt samal ajal. Samaaegsus toimub siis, kui kahte või enamat ülesannet töödeldakse üksteisega kattuvas ajavahemikus (mitte tingimata samal ajal). Jah, see on lai seletus, kuid piisavalt hea, et aidata teil erinevust märgata ja ülejäänud artiklist aru saada.
Järgnevas tabelis on esitatud paralleelsuse ja samaaegsuse toetus.
Ruby rakendamine
Paralleelsus (hargnemiste kaudu)
Paralleelsus (niitide kaudu)
Samaaegsus
MRI
Jah
Ei
Jah
JRuby
–
Jah
Jah
Rubinius
Jah
Jah (alates versioonist 2.X)
Jah
Sellega on teooria lõppenud - vaatame seda praktikas!
Eraldi mälu omamine ei pruugi põhjustada sama palju mälu tarbimist kui vanemprotsess. On olemas mõned mälu optimeerimise tehnikad. Üks neist on Copy on Write (CoW), mis võimaldab vanemprotsessil jagada eraldatud mälu lapsprotsessiga ilma seda kopeerimata. CoW puhul on lisamälu vaja ainult juhul, kui lapsprotsess muudab jagatud mälu. Ruby kontekstis ei ole kõik implementatsioonid CoW-sõbralikud, näiteks MRI toetab seda täielikult alates versioonist 2.X. Enne seda versiooni tarbis iga fork sama palju mälu kui vanemprotsess.
MRT üks suurimaid eeliseid/ puudusi (kustutage ebasobiv alternatiiv) on GILi (Global Interpreter Lock) kasutamine. Lühidalt öeldes vastutab see mehhanism niitide täitmise sünkroniseerimise eest, mis tähendab, et korraga saab teostada ainult ühte niiti. Aga oota... Kas see tähendab, et MRI-s pole mõtet niite üldse kasutada? Vastus tuleb GIL-i sisemuse mõistmisega... või vähemalt selles artiklis olevate koodinäidete vaatamisega.
Katsejuhtum
Selleks, et tutvustada, kuidas forking ja threading Ruby implementatsioonides toimib, lõin lihtsa klassi nimega Test ja mõned teised sellest pärinevamad. Igal klassil on erinev ülesanne, mida töödelda. Vaikimisi töötab iga ülesanne neli korda tsüklis. Samuti töötab iga ülesanne kolme tüüpi koodi täitmise vastu: järjestikune, hargnemisega ja niidiga. Lisaks, Benchmark.bmbm käivitab koodiploki kaks korda - esimest korda, et käivitada ja käivitada töökeskkond, teist korda, et mõõta. Kõik käesolevas artiklis esitatud tulemused saadi teise käivituse käigus. Loomulikult on isegi bmbm meetod ei garanteeri täiuslikku isolatsiooni, kuid erinevused mitme koodijooksu vahel on tähtsusetud.
nõuda "võrdlusalus"
klass Test
AMOUNT = 4
def run
Benchmark.bmbm do |b|
b.report("sequential") { sequential }
b.report("forking") { forking }
b.report("threading") { threading }
end
end
private
def sequential
AMOUNT.times { perform }
end
def forking
AMOUNT.times do
fork do
perform
end
end
Process.waitall
rescue NotImplementedError => e
# fork meetod ei ole JRuby's saadaval
puts e
end
def threading
threads = []
AMOUNT.times do
threads << Thread.new do
perform
end
end
threads.map(&:join)
end
def perform
raise "not implemented"
end
end
Koormuse test
Käivitab arvutusi tsüklis, et tekitada suurt protsessorikoormust.
class LoadTest < Test
def perform
1000.times { 1000.times { 2**3**4 } }
end
end
Käivitame selle...
LoadTest.new.run
...ja kontrollige tulemusi
MRI
JRuby
Rubinius
järjestikune
1.862928
2.089000
1.918873
kahvel
0.945018
–
1.178322
keermestamine
1.913982
1.107000
1.213315
Nagu näete, on järjestikuste sõitude tulemused sarnased. Loomulikult on lahenduste vahel väike erinevus, kuid see on tingitud valitud meetodite aluseks olevast rakendusest erinevates interpretaatorites.
Forking on antud näite puhul märkimisväärne jõudluse kasv (kood töötab peaaegu kaks korda kiiremini).
Keermestamine annab samasuguseid tulemusi nagu forking, kuid ainult JRuby ja Rubiniuse puhul. Proovi käivitamine niitidega MRI-l kulutab veidi rohkem aega kui järjestikune meetod. Sellel on vähemalt kaks põhjust. Esiteks, GIL sunnib järjestikuseid niite täitma, mistõttu täiuslikus maailmas peaks täitmisaeg olema sama, mis järjestikuse käivitamise puhul, kuid GILi operatsioonide (niitide vahel ümberlülitamine jne) puhul tekib ka ajakadu. Teiseks on vaja ka mõningaid üldkulusid niitide loomiseks.
See näide ei anna meile vastust küsimusele, milline on kasutussõnade mõte MRT-s. Vaatame teist.
Snooze test
Käivitab une meetodi.
class SnoozeTest < Test
def perform
sleep 1
end
end
Siin on tulemused
MRI
JRuby
Rubinius
järjestikune
4.004620
4.006000
4.003186
kahvel
1.022066
–
1.028381
keermestamine
1.001548
1.004000
1.003642
Nagu näete, annavad mõlemad rakendused sarnaseid tulemusi mitte ainult järjestikuste ja hargnevate käivituste, vaid ka niitide puhul. Miks on siis MRI-l samasugune jõudluse kasv kui JRuby-l ja Rubiniusel? Vastus peitub implementatsioonis magada.
MRT magada meetod on rakendatud rb_thread_wait_for C-funktsioon, mis kasutab teist funktsiooni nimega native_sleep. Vaatame selle rakendamist (kood on lihtsustatud, algne rakendamine on leitav siin):
static void
native_sleep(rb_thread_t *th, struct timeval *timeout_tv)
{
...
GVL_UNLOCK_BEGIN();
{
// tee siin mõned asjad
}
GVL_UNLOCK_END();
thread_debug("native_sleep donen");
}
Selle funktsiooni tähtsus seisneb selles, et lisaks range Ruby konteksti kasutamisele lülitub see ka süsteemi konteksti, et seal mõningaid operatsioone teha. Sellises olukorras ei ole Ruby protsessil midagi teha... Hea näide aja raiskamisest? Mitte päris, sest seal on GIL ütlus: "Selles niidis pole midagi teha? Vahetame teise ja tuleme mõne aja pärast siia tagasi". Seda võiks teha GILi lahti ja lukustades GILi koos GVL_UNLOCK_BEGIN() ja GVL_UNLOCK_END() funktsioonid.
Olukord muutub selgeks, kuid magada meetod on harva kasulik. Me vajame rohkem reaalseid näiteid.
Faili allalaadimise test
Käivitab protsessi, mis laeb alla ja salvestab faili.
nõuda "net/http"
class DownloadFileTest < Test
def perform
Net::HTTP.get("upload.wikimedia.org", "/wikipedia/commons/thumb/7/7/73/Ruby_logo.svg/2000px-Ruby_logo.svg.png")
end
end
Järgmisi tulemusi ei ole vaja kommenteerida. Need on üsna sarnased eespool esitatud näite tulemustega.
1.003642
JRuby
Rubinius
järjestikune
0.327980
0.334000
0.329353
kahvel
0.104766
–
0.121054
keermestamine
0.085789
0.094000
0.088490
Teine hea näide võib olla faili kopeerimine või mõni muu I/O-operatsioon.
Järeldused
Rubinius toetab täielikult nii hargnemist kui ka lõimimist (alates versioonist 2.X, kui GIL eemaldati). Teie kood võib olla samaaegne ja töötada paralleelselt.
JRuby teeb head tööd niitidega, kuid ei toeta üldse hargnemist. Paralleelsust ja samaaegsust saaks saavutada niitidega.
MRI toetab hargnemist, kuid lõimimine on piiratud GILi olemasolu tõttu. Samaaegsus on võimalik saavutada niitide abil, kuid ainult siis, kui jooksev kood väljub Ruby interpretaatori kontekstist (nt IO operatsioonid, tuumafunktsioonid). Paralleelsust ei ole võimalik saavutada.