Kuten luultavasti tiedät, Rubylla on useita toteutuksia, kuten MRI, JRuby, Rubinius, Opal, RubyMotion jne., ja kukin niistä voi käyttää erilaista koodin suoritusmallia. Tässä artikkelissa keskitytään kolmeen ensimmäiseen niistä ja vertaillaan MRI:tä ja
Kuten luultavasti tiedät, Rubylla on muutamia toteutuksia, kuten MRI, JRuby, Rubinius, Opal, RubyMotion jne., ja kukin niistä voi käyttää erilaista mallia ohjeiden koodi teloitus. Tässä artikkelissa keskitytään kolmeen ensimmäiseen ja vertaillaan MRI:tä (tällä hetkellä suosituin toteutus) JRubyyn ja Rubiniukseen ajamalla muutamia esimerkkiskriptejä, joiden tarkoituksena on arvioida haarautumisen ja säikeistyksen soveltuvuutta erilaisissa tilanteissa, kuten prosessori-intensiivisten algoritmien käsittelyssä, tiedostojen kopioinnissa jne.Ennen kuin aloitat "tekemällä oppimisen", sinun on kerrattava muutama perustermi.
kommunikoi muiden kanssa prosessien välisten IPC-kanavien (inter-process communication), kuten viestijonojen, tiedostojen, pistorasioiden jne. välityksellä.
on olemassa, vaikka emoprosessi päättyy
on POSIX-kutsu - toimii pääasiassa Unix-alustoilla.
Lanka
on "vain" suorituskonteksti, joka toimii prosessin sisällä.
jakaa kaiken muistin muiden kanssa (oletusarvoisesti se käyttää vähemmän muistia kuin haarautuminen).
kommunikoi muiden kanssa jaettujen muistiobjektien avulla
kuolee prosessin myötä
aiheuttaa tyypillisiä monisäikeisyysongelmia, kuten nääntymistä, lukkiutumista jne.
On olemassa runsaasti haaroja ja säikeitä käyttäviä työkaluja, joita käytetään päivittäin, esimerkiksi Unicorn (haaroja) ja Puma (säikeitä) sovelluspalvelimien tasolla, Resque (haaroja) ja Sidekiq (säikeitä) taustatehtävien tasolla jne.
Seuraavassa taulukossa esitellään haarautumisen ja säikeistyksen tuki tärkeimmissä Ruby-toteutuksissa.
Ruby-toteutus
Haarukka
Kierteitys
MRI
Kyllä
Kyllä (GIL:n rajoittamana**)
JRuby
–
Kyllä
Rubinius
Kyllä
Kyllä
Kaksi muuta taikasanaa palaa kuin bumerangi tässä aiheessa - rinnakkaisuus ja samanaikaisuus - meidän on selitettävä niitä hieman. Ensinnäkin näitä termejä ei voi käyttää keskenään vaihdellen. Pähkinänkuoressa - voimme puhua rinnakkaisuudesta, kun kahta tai useampaa tehtävää käsitellään täsmälleen samaan aikaan. Rinnakkaisuus tapahtuu, kun kahta tai useampaa tehtävää käsitellään päällekkäisinä ajanjaksoina (ei välttämättä samanaikaisesti). Kyllä, tämä on laaja selitys, mutta riittävän hyvä, jotta huomaat eron ja ymmärrät tämän artikkelin loput.
Seuraavassa taulukossa esitetään rinnakkaisuuden ja samanaikaisuuden tuki.
Ruby-toteutus
Rinnakkaisuus (haarojen kautta)
Rinnakkaisuus (säikeiden avulla)
Samanaikaisuus
MRI
Kyllä
Ei
Kyllä
JRuby
–
Kyllä
Kyllä
Rubinius
Kyllä
Kyllä (versiosta 2.X lähtien)
Kyllä
Teoria loppuu tähän - katsotaanpa sitä käytännössä!
Erillisen muistin käyttö ei välttämättä aiheuta sitä, että se kuluttaa saman verran muistia kuin emoprosessi. On olemassa joitakin muistin optimointitekniikoita. Yksi niistä on Copy on Write (CoW), jonka avulla emoprosessi voi jakaa varattua muistia lapsiprosessin kanssa kopioimatta sitä. CoW:n avulla lisämuistia tarvitaan vain silloin, kun lapsiprosessi muuttaa jaettua muistia. Rubyn osalta kaikki toteutukset eivät ole CoW-ystävällisiä, esimerkiksi MRI tukee sitä täysin versiosta 2.X lähtien. Ennen tätä versiota kukin haarautuminen kulutti yhtä paljon muistia kuin vanhempaprosessi.
Yksi magneettikuvauksen suurimmista eduista/haitoista (poista sopimaton vaihtoehto) on GIL:n (Global Interpreter Lock) käyttö. Lyhyesti sanottuna tämä mekanismi vastaa säikeiden suorituksen synkronoinnista, mikä tarkoittaa, että vain yhtä säiettä voidaan suorittaa kerrallaan. Mutta hetkinen... Tarkoittaako se, että säikeitä ei kannata käyttää MRI:ssä lainkaan? Vastaus tulee, kun ymmärtää GIL:n sisäiset asiat... tai ainakin vilkaisee tämän artikkelin koodinäytteitä.
Testitapaus
Esitelläkseni, miten haarautuminen ja säikeistäminen toimii Rubyn toteutuksissa, loin yksinkertaisen luokan nimeltä Testi ja muutama muu siitä periytyvä. Jokaisella luokalla on erilainen tehtävä. Oletusarvoisesti jokainen tehtävä suoritetaan neljä kertaa silmukassa. Jokainen tehtävä suoritetaan myös kolmea koodin suoritustapaa vastaan: peräkkäin, haarukoiden ja säikeiden kanssa. Lisäksi, Benchmark.bmbm suorittaa koodilohkon kahdesti - ensimmäisen kerran, jotta ajoympäristö saadaan käyttöön ja toimimaan, ja toisen kerran mittausta varten. Kaikki tässä artikkelissa esitetyt tulokset saatiin toisella ajokerralla. Tietenkin myös bmbm menetelmä ei takaa täydellistä eristystä, mutta useiden koodiajojen väliset erot ovat merkityksettömiä.
vaatia "benchmark"
luokka Testi
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-metodi ei ole käytettävissä JRubyssä.
puts e
end
def threading
threads = []
AMOUNT.times do
threads << Thread.new do
perform
end
end
threads.map(&:join)
end
def perform
raise "ei toteutettu"
end
end
Kuormitustesti
Suorittaa laskutoimituksia silmukassa luodakseen suuren prosessorikuorman.
class LoadTest < Testi
def perform
1000.times { 1000.times { 2**3**4 } }
end
end
Käynnistetään se...
LoadTest.new.run
...ja tarkista tulokset
MRI
JRuby
Rubinius
peräkkäiset
1.862928
2.089000
1.918873
haarautuminen
0.945018
–
1.178322
kierteitys
1.913982
1.107000
1.213315
Kuten näet, peräkkäisten ajojen tulokset ovat samanlaisia. Ratkaisujen välillä on tietysti pieni ero, mutta se johtuu eri tulkkien valittujen menetelmien taustalla olevasta toteutuksesta.
Haarautuminen tuo tässä esimerkissä merkittävää suorituskykyhyötyä (koodi toimii lähes kaksi kertaa nopeammin).
Säikeistäminen antaa samanlaisia tuloksia kuin haarautuminen, mutta vain JRubylla ja Rubiniuksella. Näytteen ajaminen säikeiden kanssa MRI:ssä kuluttaa hieman enemmän aikaa kuin peräkkäinen menetelmä. Tähän on ainakin kaksi syytä. Ensinnäkin GIL pakottaa suorittamaan säikeet peräkkäin, joten täydellisessä maailmassa suoritusajan pitäisi olla sama kuin peräkkäisessä suorituksessa, mutta GIL-operaatioihin (säikeiden välillä vaihtaminen jne.) kuluu myös aikaa. Toiseksi säikeiden luomiseen kuluu myös jonkin verran aikaa.
Tämä esimerkki ei anna meille vastausta kysymykseen, mikä on käyttösäikeiden merkitys MRI:ssä. Katsotaanpa toinen esimerkki.
Torkkutesti
Suorittaa lepotilamenetelmän.
class SnoozeTest < Testi
def perform
sleep 1
end
end
Tässä ovat tulokset
MRI
JRuby
Rubinius
peräkkäiset
4.004620
4.006000
4.003186
haarautuminen
1.022066
–
1.028381
kierteitys
1.001548
1.004000
1.003642
Kuten näet, kumpikin toteutus antaa samankaltaisia tuloksia paitsi peräkkäisissä ja haarautuvissa suorituksissa myös säikeistetyissä suorituksissa. Miksi MRI:llä on siis sama suorituskykyhyöty kuin JRubylla ja Rubiniuksella? Vastaus löytyy toteutuksesta nukkua.
Magneettikuvaus nukkua menetelmä on toteutettu rb_thread_wait_for C-funktio, joka käyttää toista funktiota nimeltä native_sleep. Katsotaanpa pikaisesti sen toteutusta (koodia on yksinkertaistettu, alkuperäinen toteutus löytyy osoitteesta täällä):
Tämä toiminto on tärkeä siksi, että sen lisäksi, että se käyttää tiukkaa Ruby-kontekstia, se myös vaihtaa järjestelmäkontekstiin suorittaakseen siellä joitakin toimintoja. Tällaisissa tilanteissa Ruby-prosessilla ei ole mitään tekemistä... Loistava esimerkki ajan tuhlaamisesta? Ei oikeastaan, koska on olemassa GIL:n sanonta: "Ei mitään tekemistä tässä säikeessä? Vaihdetaan toiseen ja palataan tänne hetken kuluttua". Tämä voitaisiin tehdä avaamalla ja lukitsemalla GIL:n lukitus komennolla GVL_UNLOCK_BEGIN() ja GVL_UNLOCK_END() toiminnot.
Tilanne tulee selväksi, mutta nukkua menetelmä on harvoin hyödyllinen. Tarvitsemme enemmän esimerkkejä todellisesta elämästä.
Tiedoston lataustesti
Käynnistää prosessin, joka lataa ja tallentaa tiedoston.
vaativat "net/http"
class DownloadFileTest < Testi
def perform
Net::HTTP.get("upload.wikimedia.org", "/wikipedia/commons/thumb/7/7/73/Ruby_logo.svg/2000px-Ruby_logo.svg.png"))
end
end
Seuraavia tuloksia ei tarvitse kommentoida. Ne ovat melko samanlaisia kuin edellä olevassa esimerkissä.
1.003642
JRuby
Rubinius
peräkkäiset
0.327980
0.334000
0.329353
haarautuminen
0.104766
–
0.121054
kierteitys
0.085789
0.094000
0.088490
Toinen hyvä esimerkki voisi olla tiedoston kopiointiprosessi tai jokin muu I/O-operaatio.
Päätelmät
Rubinius tukee täysin sekä haarautumista että säikeistämistä (versiosta 2.X lähtien, jolloin GIL poistettiin). Koodisi voi olla rinnakkaista ja toimia rinnakkain.
JRuby tekee hyvää työtä säikeiden kanssa, mutta ei tue haarautumista lainkaan. Rinnakkaisuus ja samanaikaisuus voitaisiin saavuttaa säikeillä.
MRI tukee haarautumista, mutta säikeistämistä rajoittaa GIL:n läsnäolo. Rinnakkaisuus voitaisiin saavuttaa säikeiden avulla, mutta vain silloin, kun suoritettava koodi menee Ruby-tulkin kontekstin ulkopuolelle (esim. IO-operaatiot, ytimen toiminnot). Rinnakkaisuutta ei ole mahdollista saavuttaa.