Samanaikaisuuden soveltaminen asyncilla
Tässä osiossa sovellamme asyncia joihinkin samoihin samanaikaisuushaasteisiin, joita käsittelimme säikeillä luvussa 16. Koska käsittelimme siellä jo monia keskeisiä ideoita, keskitymme tässä osiossa siihen, mikä eroaa säikeiden ja futurejen välillä.
Monissa tapauksissa asyncilla työskentelyn API:t ovat hyvin samankaltaisia kuin säikeillä työskentelyn API:t. Toisissa tapauksissa ne ovat hyvin erilaisia. Vaikka API:t näyttäisivät samankaltaisilta säikeiden ja asyncin välillä, niillä on usein erilainen käyttäytyminen — ja niillä on lähes aina erilaiset suorituskykyominaisuudet.
Uuden tehtävän luominen spawn_task:illa
Ensimmäinen operaatio, jota käsittelimme ”Uuden säikeen luominen spawn:illa” -osiossa luvussa 16, oli laskeminen kahdella erillisellä säikeellä. Tehdään sama asyncilla. trpl-crate tarjoaa spawn_task-funktion, joka näyttää hyvin samankaltaiselta kuin thread::spawn-API, ja sleep-funktion, joka on async-versio thread::sleep-API:sta. Voimme käyttää näitä yhdessä laskuesimerkin toteuttamiseen, kuten listauksessa 17-6.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-06/src/main.rs:all}}
}
Lähtökohtanamme asetamme main-funktion trpl::block_on:illa, jotta ylätason funktiomme voi olla async.
Huom: Tästä eteenpäin luvussa jokainen esimerkki sisältää täsmälleen saman käärintäkoodin
trpl::block_on:illamain:issa, joten ohitamme sen usein samalla tavalla kuinmain:in. Muista sisällyttää se koodiisi!
Sitten kirjoitamme kaksi silmukkaa kyseisen lohkon sisään, joista kummassakin on trpl::sleep-kutsu, joka odottaa puoli sekuntia (500 millisekuntia) ennen seuraavan viestin lähettämistä. Sijoitamme yhden silmukan trpl::spawn_task:n runkoon ja toisen ylätason for-silmukkaan. Lisäämme myös await:in sleep-kutsujen jälkeen.
Tämä koodi käyttäytyy samankaltaisesti kuin säikeisiin perustuva toteutus — mukaan lukien se, että saatat nähdä viestien ilmestyvän eri järjestyksessä omassa terminaalissasi, kun suoritat sen:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
Tämä versio pysähtyy heti, kun pää-async-lohkon rungossa oleva for-silmukka päättyy, koska spawn_task:n luoma tehtävä sammutetaan, kun main-funktio päättyy. Jos haluat sen suorittuvan aina tehtävän valmistumiseen asti, tarvitset join-kahvan odottamaan ensimmäisen tehtävän valmistumista. Säikeillä käytimme join-metodia ”estääksemme” säikeen valmistumiseen asti. Listauksessa 17-7 voimme käyttää await:ia samaan tarkoitukseen, koska tehtäväkahva itsessään on future. Sen Output-tyyppi on Result, joten puramme sen myös await:in jälkeen.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-07/src/main.rs:handle}}
}
await:in käyttö join-kahvan kanssa tehtävän suorittamiseksi loppuunTämä päivitetty versio suorittaa, kunnes molemmat silmukat päättyvät:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
Tähän mennessä näyttää siltä, että async ja säikeet antavat samankaltaiset tulokset, vain eri syntaksilla: await:in käyttö join-kutsun sijaan join-kahvalla ja sleep-kutsujen odottaminen.
Suurempi ero on se, että emme tarvinneet luoda toista käyttöjärjestelmän säiettä tähän. Itse asiassa emme edes tarvitse luoda tehtävää tässä. Koska async-lohkot käännetään nimettömiksi futureiksi, voimme sijoittaa kummankin silmukan async-lohkoon ja antaa ajoympäristön suorittaa molemmat loppuun trpl::join-funktiolla.
”Kaikkien säikeiden valmistumisen odottaminen” -osiossa luvussa 16 näytimme, miten join-metodia käytetään std::thread::spawn:in palauttaman JoinHandle-tyypin kanssa. trpl::join-funktio on samankaltainen, mutta futureille. Kun annat sille kaksi futurea, se tuottaa yhden uuden futuren, jonka tulos on monikko kummankin välittämäsi futuren tuloksista, kun ne molemmat ovat valmiita. Näin listauksessa 17-8 käytämme trpl::join:ia odottamaan sekä fut1:n että fut2:n valmistumista. Emme odota fut1:tä ja fut2:tä, vaan trpl::join:n tuottamaa uutta futurea. Jätämme tuloksen huomiotta, koska se on vain monikko, joka sisältää kaksi yksikköarvoa.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-08/src/main.rs:join}}
}
trpl::join:in käyttö kahden nimettömän futuren odottamiseenKun suoritamme tämän, näemme molempien futurejen suorittuvan loppuun:
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
Nyt näet täsmälleen saman järjestyksen joka kerta, mikä on hyvin erilaista kuin säikeillä ja trpl::spawn_task:illa listauksessa 17-7. Tämä johtuu siitä, että trpl::join-funktio on reilu: se tarkistaa kummankin futuren yhtä usein, vuorotellen niitä, eikä koskaan anna toisen edetä, jos toinen on valmis. Säikeillä käyttöjärjestelmä päättää, mitä säiettä tarkistaa ja kuinka kauan antaa sen suorittaa. Async-Rustissa ajoympäristö päättää, mitä tehtävää tarkistaa. (Käytännössä yksityiskohdat monimutkaistuvat, koska async-ajoympäristö voi käyttää käyttöjärjestelmän säikeitä taustalla osana samanaikaisuuden hallintaa, joten reiluuden takaaminen voi olla ajoympäristölle enemmän työtä — mutta se on silti mahdollista!) Ajoympäristöjen ei tarvitse taata reiluutta millekään operaatiolle, ja ne tarjoavat usein eri API:ja, joiden avulla voit valita, haluatko reiluutta.
Kokeile joitakin näistä variaatioista futurejen odottamisessa ja katso, mitä ne tekevät:
- Poista async-lohko kummankin tai molempien silmukoiden ympäriltä.
- Odota kumpaakin async-lohkoa heti sen määrittelyn jälkeen.
- Kääri vain ensimmäinen silmukka async-lohkoon ja odota tuloksena olevaa futurea toisen silmukan rungon jälkeen.
Lisähaasteena katso, osaatko päätellä, mikä tuloste on kussakin tapauksessa ennen koodin suorittamista!
Datan lähettäminen kahden tehtävän välillä viestinvälityksellä
Datan jakaminen futurejen välillä on myös tuttua: käytämme jälleen viestinvälitystä, mutta tällä kertaa async-versioita tyypeistä ja funktioista. Kuljemme hieman eri polkua kuin ”Datan siirtäminen säikeiden välillä viestinvälityksellä” -osiossa luvussa 16 havainnollistaaksemme keskeisiä eroja säikeisiin ja futureihin perustuvan samanaikaisuuden välillä. Listauksessa 17-9 aloitamme vain yhdellä async-lohkolla — emme luo erillistä tehtävää kuten loimme erillisen säikeen.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-09/src/main.rs:channel}}
}
tx:lle ja rx:lleTässä käytämme trpl::channel:ia, async-version monituottaja-yksittäiskuluttaja-kanava-API:sta, jota käytimme säikeillä luvussa 16. Async-versio API:sta eroaa vain vähän säikeisiin perustuvasta versiosta: se käyttää muuttuvaa eikä muuttumatonta vastaanottajaa rx:ää, ja sen recv-metodi tuottaa futuren, jota meidän täytyy odottaa, sen sijaan että se tuottaisi arvon suoraan. Nyt voimme lähettää viestejä lähettäjältä vastaanottajalle. Huomaa, että emme tarvitse luoda erillistä säiettä tai edes tehtävää; meidän täytyy vain odottaa rx.recv-kutsua.
Synkroninen Receiver::recv-metodi std::mpsc::channel:issa estää, kunnes se vastaanottaa viestin. trpl::Receiver::recv-metodi ei estä, koska se on async. Sen sijaan että estäisi, se palauttaa ohjauksen ajoympäristölle, kunnes viesti vastaanotetaan tai kanavan lähetyspuoli sulkeutuu. Sitä vastoin emme odota send-kutsua, koska se ei estä. Sen ei tarvitse, koska kanava, johon lähetämme, on rajoittamaton.
Huom: Koska kaikki tämä async-koodi suoritetaan async-lohkossa
trpl::block_on-kutsussa, kaikki sen sisällä voi välttää estämisen. Koodi sen ulkopuolella kuitenkin estyy, kunnesblock_on-funktio palaa. Siinä on kokotrpl::block_on-funktion idea: sen avulla voit valita, missä estät jonkin async-koodin joukon, ja siten missä siirryt synkronisen ja asynkronisen koodin välillä.
Huomaa tästä esimerkistä kaksi asiaa. Ensinnäkin viesti saapuu heti. Toiseksi, vaikka käytämme tässä futurea, samanaikaisuutta ei vielä ole. Kaikki listauksessa tapahtuu peräkkäin, aivan kuten ilman futureja.
Käsitellään ensimmäinen osa lähettämällä sarja viestejä ja nukkumalla niiden välissä, kuten listauksessa 17-10.
{{#rustdoc_include ../listings/ch17-async-await/listing-17-10/src/main.rs:many-messages}}
await:illa jokaisen viestin välissäViestien lähettämisen lisäksi meidän täytyy vastaanottaa ne. Tässä tapauksessa, koska tiedämme kuinka monta viestiä on tulossa, voisimme tehdä sen käsin kutsumalla rx.recv().await neljä kertaa. Oikeassa maailmassa odotamme kuitenkin yleensä tuntemattoman määrän viestejä, joten meidän täytyy odottaa, kunnes päätämme, ettei viestejä enää tule.
Listauksessa 16-10 käytimme for-silmukkaa käsittelemään kaikki synkronisesta kanavasta vastaanotetut kohteet. Rustilla ei kuitenkaan ole vielä tapaa käyttää for-silmukkaa asynkronisesti tuotetun kohteiden sarjan kanssa, joten meidän täytyy käyttää silmukkaa, jota emme ole vielä nähneet: while let -ehdosilmukkaa. Tämä on silmukkaversio if let -rakenteesta, jonka näimme ”Tiivis ohjausvirta if let:illä ja let...else:llä” -osiossa luvussa 6. Silmukka jatkaa suorittamista niin kauan kuin sen määrittelemä kuvio vastaa edelleen arvoa.
rx.recv-kutsu tuottaa futuren, jota odotamme. Ajoympäristö keskeyttää futuren, kunnes se on valmis. Kun viesti saapuu, future ratkeaa Some(message)-arvoksi niin monta kertaa kuin viestejä saapuu. Kun kanava sulkeutuu — riippumatta siitä, ovatko mitkään viestit saapuneet — future ratkeaa sen sijaan None:ksi ilmaisemaan, ettei arvoja ole enempää ja että meidän pitäisi lopettaa pollaus — eli lopettaa odottaminen.
while let -silmukka yhdistää kaiken tämän. Jos rx.recv().await:in tulos on Some(message), saamme käyttöön viestin ja voimme käyttää sitä silmukan rungossa, aivan kuten if let:illä. Jos tulos on None, silmukka päättyy. Joka kerta kun silmukka suoritetaan loppuun, se osuu odotuspisteeseen uudelleen, joten ajoympäristö keskeyttää sen uudelleen, kunnes toinen viesti saapuu.
Koodi lähettää ja vastaanottaa nyt onnistuneesti kaikki viestit. Valitettavasti on vielä pari ongelmaa. Ensinnäkin viestit eivät saavu puolen sekunnin välein. Ne saapuvat kaikki kerralla 2 sekunnin (2 000 millisekunnin) kuluttua ohjelman käynnistymisestä. Toiseksi tämä ohjelma ei myöskään koskaan päätty! Sen sijaan se odottaa ikuisesti uusia viestejä. Sinun täytyy sammuttaa se painamalla ctrl-C.
Yhden async-lohkon sisällä oleva koodi suoritetaan lineaarisesti
Aloitetaan tutkimalla, miksi viestit saapuvat kaikki kerralla koko viiveen jälkeen sen sijaan, että ne saapuisivat viivein välein. Tietyssä async-lohkossa await-avainsanojen esiintymisjärjestys koodissa on myös järjestys, jossa ne suoritetaan ohjelman käydessä.
Listauksessa 17-10 on vain yksi async-lohko, joten kaikki sen sisällä suoritetaan lineaarisesti. Samanaikaisuutta ei vieläkään ole. Kaikki tx.send-kutsut tapahtuvat vuorotellen kaikkien trpl::sleep-kutsujen ja niihin liittyvien odotuspisteiden kanssa. Vasta sitten while let -silmukka pääsee käymään läpi mitään recv-kutsujen odotuspisteitä.
Saadaksemme haluamamme käyttäytymisen, jossa nukkumisviive tapahtuu jokaisen viestin välissä, meidän täytyy sijoittaa tx- ja rx-operaatiot omiin async-lohkoihinsa, kuten listauksessa 17-11. Sitten ajoympäristö voi suorittaa kummankin erikseen käyttämällä trpl::join:ia, aivan kuten listauksessa 17-8. Taas odotamme trpl::join:in kutsumisen tulosta, emme yksittäisiä futureja. Jos odottaisimme yksittäisiä futureja peräkkäin, päätyisimme takaisin peräkkäiseen kulkuun — juuri sitä, mitä yritämme välttää.
{{#rustdoc_include ../listings/ch17-async-await/listing-17-11/src/main.rs:futures}}
send- ja recv-operaatioiden erottaminen omiin async-lohkoihinsa ja näiden lohkojen futurejen odottaminenListauksen 17-11 päivitetyllä koodilla viestit tulostetaan 500 millisekunnin välein sen sijaan, että ne tulostuisivat kaikki kerralla 2 sekunnin kuluttua.
Omistajuuden siirtäminen async-lohkoon
Ohjelma ei kuitenkaan vieläkään päätty, koska while let -silmukan ja trpl::join:in vuorovaikutus:
trpl::join:in palauttama future valmistuu vasta, kun molemmat sille välitetyt futuret ovat valmiita.tx_fut-future valmistuu, kun se on nukkunut viimeisen viestin lähettämisen jälkeenvals:issa.rx_fut-future ei valmistu, ennen kuinwhile let-silmukka päättyy.while let-silmukka ei päätty, ennen kuinrx.recv:n odottaminen tuottaaNone:n.rx.recv:n odottaminen palauttaaNone:n vain, kun kanavan toinen pää on suljettu.- Kanava sulkeutuu vain, jos kutsumme
rx.close:a tai kun lähettäjäpuolitxpudotetaan. - Emme kutsu
rx.close:a missään, eikätxpudotu ennen kuintrpl::block_on:ille välitetty ulompi async-lohko päättyy. - Lohko ei voi päättyä, koska se on estynyt
trpl::join:in valmistumiseen, mikä vie meidät takaisin tämän listan alkuun.
Tällä hetkellä viestejä lähettävä async-lohko vain lainaa tx:ää, koska viestin lähettäminen ei vaadi omistajuutta, mutta jos voisimme siirtää tx:n kyseiseen async-lohkoon, se pudotettaisiin, kun lohko päättyy. ”Viittausten kaappaaminen tai omistajuuden siirtäminen” -osiossa luvussa 13 opit käyttämään move-avainsanaa sulkeumien kanssa, ja kuten käsiteltiin ”move-sulkeumien käyttö säikeiden kanssa” -osiossa luvussa 16, meidän täytyy usein siirtää data sulkeumiin säikeillä työskennellessä. Sama perusdynamiikka pätee async-lohkoihin, joten move-avainsana toimii async-lohkojen kanssa samalla tavalla kuin sulkeumien kanssa.
Listauksessa 17-12 muutamme viestien lähettämiseen käytetyn lohkon muodosta async muotoon async move.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-12/src/main.rs:with-move}}
}
Kun suoritamme tämän version koodista, se sammuttuu siististi viimeisen viestin lähettämisen ja vastaanottamisen jälkeen. Seuraavaksi katsotaan, mitä pitäisi muuttaa lähettääksemme dataa useammasta kuin yhdestä futuresta.
Usean futuren yhdistäminen join!-makrolla
Tämä async-kanava on myös monituottaja-kanava, joten voimme kutsua clone:a tx:lle, jos haluamme lähettää viestejä useista futureista, kuten listauksessa 17-13.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}}
}
Ensin kloonaamme tx:n luoden tx1:n ensimmäisen async-lohkon ulkopuolelle. Siirrämme tx1:n kyseiseen lohkoon kuten aiemmin tx:n kanssa. Sitten myöhemmin siirrämme alkuperäisen tx:n uuteen async-lohkoon, jossa lähetämme lisää viestejä hieman hitaammalla viiveellä. Sijoitamme tämän uuden async-lohkon vastaanottavaan async-lohkoon jälkeen, mutta se voisi olla yhtä hyvin ennen sitä. Ratkaisevaa on järjestys, jossa futureja odotetaan, ei järjestys, jossa ne luodaan.
Molempien viestejä lähettävien async-lohkojen täytyy olla async move -lohkoja, jotta sekä tx että tx1 pudotetaan, kun lohkot päättyvät. Muuten päädymme takaisin samaan loputtomaan silmukkaan, jossa aloitimme.
Lopuksi vaihdamme trpl::join:ista trpl::join!:iin käsitelläksemme lisäfuturen: join!-makro odottaa mielivaltaisen määrän futureja, joiden määrä tiedetään kääntöaikana. Käsittelemme tuntemattoman määrän futureja myöhemmin tässä luvussa.
Nyt näemme kaikki viestit molemmista lähettävistä futureista, ja koska lähettävät futuret käyttävät hieman erilaisia viiveitä lähettämisen jälkeen, viestit vastaanotetaan myös näillä eri väleillä:
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
Olemme tutkineet, miten viestinvälitystä käytetään datan lähettämiseen futurejen välillä, miten async-lohkon sisällä oleva koodi suoritetaan peräkkäin, miten omistajuus siirretään async-lohkoon ja miten useita futureja yhdistetään. Seuraavaksi keskustellaan siitä, miten ja miksi kerrotaan ajoympäristölle, että se voi vaihtaa toiseen tehtävään.