Teknologian jakaminen

Go-korutiinien ja kanavien kattavat sovelluskysymykset

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1. Ymmärrä lyhyesti, mitä korutiinit ja kanavat ovat

Mikä on korutiini

Korutiini on käyttäjätason kevyt säie, jolla on itsenäinen pinotila ja joka jakaa ohjelman pinotilan.

Se on yhden säikeen algoritmien avulla toteutettu mikrosäie Monisäikeiseen ohjelmointiin verrattuna sillä on seuraavat edut:

  • Korutiinin kontekstin vaihtamisen päättää käyttäjä ilman järjestelmän ytimen kontekstin vaihtoa, mikä vähentää yleiskustannuksia.
  • Oletusarvoisesti korutiinit ovat täysin suojattuja keskeytyksiä estämiseksi.Ei vaadi atomikäyttölukkoa
  • Yksi säie voi myös saavuttaa korkean samanaikaisuuden, ja jopa yksiytiminen CPU voi tukea kymmeniä tuhansia korutiineja.

mikä on kanava

Kanava on tietorakenne, jota käytetään tiedonsiirtoon korutiinien välillä. Kuten jonossa, toinen pää on lähettäjä ja toinen pää on vastaanottaja. Kanavien avulla voidaan varmistaa tietojen synkronointi ja järjestys.

Kanavat on jaettu puskuroituihin ja puskuroimattomiin kanaviin, jotka ilmoitetaan seuraavasti:

  • Siellä on puskurikanava
intChan := make(chan int,<缓冲容量>)
  • puskuroimaton kanava
intChan := make(chan int)

Ero puskuroitujen ja puskuroimattomien kanavien välillä:

  • Esto: Puskuroimattoman kanavan lähettäjä estää, kunnes data on vastaanotettu, puskuroidun kanavan lähettäjä estää, kunnes puskuri on täynnä, ja vastaanottaja estää, kunnes puskuri ei ole tyhjä.
  • Tietojen synkronointi ja järjestys: Puskuroimattomat kanavat takaavat tietojen synkronoinnin ja puskuroidut putket eivät takaa tietojen synkronointia ja järjestystä.
  • Sovellusskenaariot: Puskuroimattomat kanavat vaativat tiukan synkronoinnin ja puskuroidut kanavat voivat kommunikoida asynkronisesti ja parantaa suorituskykyä.

Puskuroimattomien kanavien toteutuksessa on huomioitava, että kanavan molemmissa päissä on oltava lähettäjä ja vastaanottaja, muuten tapahtuu lukkiutuminen.

2. Korutiinikanavan samanaikainen ohjelmointitapaus

(1) Tulosta kirjaimia ja numeroita vuorotellen

Kysymyksen merkitys: Käytä corutine-channel tulostaaksesi vuorotellen numerot 1-10 ja kirjaimet AJ.

Koodi:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. /*
  7. 无缓冲chanel:需要在写入chanel的时候要保证有另外一个协程在读取chanel。否则会导致写端阻塞,发生死锁
  8. 解决办法:
  9. 避免死锁的发生:
  10. 当i循环到10时,printAlp协程已然结束,所以此时不必再写入alp通道
  11. */
  12. func printNum(wg *sync.WaitGroup, numCh chan struct{}, alpCh chan struct{}) {
  13. defer wg.Done()
  14. for i := 1; i <= 10; i++ {
  15. <-alpCh // 等待字母goroutine发信号
  16. fmt.Print(i, " ")
  17. //避免死锁发生
  18. if i < 10 {
  19. numCh <- struct{}{} // 发信号给字母goroutine
  20. }
  21. if i == 10 {
  22. close(numCh)
  23. }
  24. }
  25. }
  26. func printAlp(wg *sync.WaitGroup, numCh chan struct{}, alpCh chan struct{}) {
  27. defer wg.Done()
  28. for i := 'A'; i <= 'J'; i++ {
  29. <-numCh // 等待数字goroutine发信号
  30. fmt.Printf("%c", i)
  31. alpCh <- struct{}{} // 发信号给数字goroutine
  32. }
  33. close(alpCh)
  34. }
  35. func main() {
  36. numCh := make(chan struct{}) // 用于数字goroutine的信号通道
  37. alpCh := make(chan struct{}) // 用于字母goroutine的信号通道
  38. var wg sync.WaitGroup
  39. wg.Add(2)
  40. go printAlp(&wg, numCh, alpCh)
  41. go printNum(&wg, numCh, alpCh)
  42. // 启动时先给数字goroutine发送一个信号
  43. numCh <- struct{}{}
  44. wg.Wait()
  45. }

Aiheanalyysi:

Kysymys edellyttää meidän tulostavan vuorotellen kirjaimia ja numeroita, joten meidän on varmistettava kahden korutiinin tiukka järjestys, mikä on yhdenmukainen puskuroimattomien kanavien sovellusskenaarion kanssa. Aseta kaksi kanavaa tallentaaksesi numerot ja kirjaimet. Kaksi riviä, jotka tulostavat numeroita ja kirjaimia, toimivat kummankin kanavan lähettäjänä ja vastaanottajana. Tulosta kerran silmukassa ja lähetä signaali kerran muistuttaaksesi toista korutiinia tulostamisesta.

On huomioitava, että kun viimeinen merkki '10' on tulostettu, kirjainten tulostus on päättynyt ja numCh-kanavalla ei ole vastaanotinta. Tällä hetkellä puskuroimattoman kanavan toteutusehdot eivät enää täyty Lähettäjä ja vastaanottaja Muutoin signaalin lähettäminen uudelleen aiheuttaa lukkiutumisen. Joten signaalia ei tarvitse lähettää uudelleen 10. kertaa.

(2) Suunnittele tehtävien ajoitus

Otsikko: Suunnittele tehtävien ajoitus, joka käyttää monikorutiini + kanavaohjelmointimallia toteuttamaan liiketoimintaskenaarioita useiden tehtävien samanaikaista käsittelyä varten, ja ajoitusjärjestyksen on oltava siinä järjestyksessä, jossa tehtävät lisätään.

Koodi:

  1. type scheduler struct {
  2. taskChan chan func()
  3. wt sync.WaitGroup
  4. }
  5. func (td *scheduler) AddTask(task func()) {
  6. td.taskChan <- task
  7. }
  8. func (td *scheduler) Executer() {
  9. defer td.wt.Done()
  10. for {
  11. task, ok := <-td.taskChan
  12. task()
  13. if ok && len(td.taskChan) == 0 {
  14. break
  15. }
  16. }
  17. }
  18. func (td *scheduler) Start() {
  19. td.wt.Add(4)
  20. //假设四个消费者
  21. for i := 0; i < 4; i++ {
  22. go td.Executer()
  23. }
  24. td.wt.Wait()
  25. }
  26. func main() {
  27. sd := scheduler{
  28. taskChan: make(chan func(), 5),
  29. }
  30. go func() {
  31. sd.AddTask(func() {
  32. fmt.Println("任务1")
  33. })
  34. sd.AddTask(func() {
  35. fmt.Println("任务2")
  36. })
  37. sd.AddTask(func() {
  38. fmt.Println("任务3")
  39. })
  40. sd.AddTask(func() {
  41. fmt.Println("任务4")
  42. })
  43. sd.AddTask(func() {
  44. fmt.Println("任务5")
  45. })
  46. sd.AddTask(func() {
  47. fmt.Println("任务6")
  48. })
  49. close(sd.taskChan)
  50. }()
  51. sd.Start()
  52. }

ongelmaanalyysi:

Koska lisätyt tehtävät ovat useita tehtäviä, niitä on enemmän kuin yksi, ja näiden tehtävien suorittaminen edellyttää asynkronista käsittelyä. Puskuroitujen kanavien noudattaminen vaatii parempaa suorituskykyä ja asynkronista käsittelyä.

Sitten meidän on asetettava tehtävä kanavalle, ja useat vastaanottimet voivat ottaa tehtävät kanavalta järjestyksessä ja suorittaa ne.

Yksi huomioitava asia on, että jos lisättyjen tehtävien määrä on suurempi kuin kanavapuskuri, se aiheuttaa eston tehtäviä lisättäessä. Jotta se ei vaikuta kuluttajan normaaliin käynnistykseen, sen on avattava erillinen korutiini tehtävien lisäämistä varten.

Tällä tavalla, kun kuluttaja kuluttaa, estotuottaja herätetään jatkamaan tehtävien lisäämistä.

3. Yhteenveto

Korutiini + kanavaohjelmointimallin tutkittuamme, otsikossa mainitun lisäksi, kannattaa kiinnittää huomiota myös seuraaviin asioihin:

1. Miksi kanava pitäisi sulkea sen jälkeen, kun se on käytetty loppuun. Mitä riskejä sen sulkematta jättäminen aiheuttaa?

  • Välttääksesi umpikujan. Kanavan sulkeminen kertoo myös vastaanottajalle, että lähettäjältä ei ole enää lähetettävää dataa eikä dataa tarvitse odottaa. Vastaanotettuaan kanavan sulkemistiedot, vastaanotin lopettaa tietojen vastaanottamisen, jos kanavaa ei ole suljettu, vastaanotin pysyy lukittuna ja on olemassa lukkiutumisen vaara.
  • Vapauta resurssit ja vältä resurssivuodot. Kanavan sulkemisen jälkeen järjestelmä vapauttaa vastaavat resurssit. Kanavan sulkeminen ajoissa voi välttää resurssien tuhlauksen ja vuodon.

2. Kuinka sulkea kanava kauniisti?

Ensinnäkin kanavien sulkemisen perusperiaate on olla sulkematta suljettuja kanavia. Toiseksi Go-kanavien käytössä on toinen periaate:Älä sulje kanavaa datavastaanottimessa tai jos lähettäjiä on useita.toisin sanoenMeidän tulisi antaa vain kanavan ainoan lähettäjän sulkea tämä kanava.

Epäkohtelias tapa on sulkea kanava poikkeuspalautuksen avulla, mutta tämä on ilmeisesti vastoin yllä olevia periaatteita ja voi aiheuttaa datakilpailun. Toinen tapa on sulkea kanava sync.Once- tai sync.Mutex-toiminnolla, jonka toteutumista ei taata operaatiot ja lähetystoiminnot kanavalla eivät luo datakilpailuja. Molemmilla tavoilla on tiettyjä ongelmia, joten en esittele niitä yksityiskohtaisesti. Tässä on menetelmä kanavan sulamiseen.

Skenaario 1: M vastaanottajaa ja yksi lähettäjä

Yksi helpoimmista tilanteista käsitellä. Kun lähettäjän on lopetettava lähetys, anna sen sulkea kanava. Näin on kahdessa yllä olevassa ohjelmointiesimerkissä.

Skenaario 2: Yksi vastaanottaja ja N lähettäjää

Go-kanavien perusperiaatteiden mukaan voimme sulkea kanavan vain kanavan ainoalta lähettäjältä. Joten tässä tapauksessa emme voi sulkea kanavaa suoraan jonnekin.Mutta voimme antaa vastaanottimen sulkea ylimääräisen signaalikanavan, jotta lähettäjä ei lähetä enempää dataa.

  1. package main
  2. import (
  3. "log"
  4. "sync"
  5. )
  6. func main() {
  7. cosnt N := 5
  8. cosnt Max := 60000
  9. count := 0
  10. dataCh := make(chan int)
  11. stopCh := make(chan bool)
  12. var wt sync.WaitGroup
  13. wt.Add(1)
  14. //发送者
  15. for i := 0; i < N; i++ {
  16. go func() {
  17. for {
  18. select {
  19. case <-stopCh:
  20. return
  21. default:
  22. count += 1
  23. dataCh <- count
  24. }
  25. }
  26. }()
  27. }
  28. //接收者
  29. go func() {
  30. defer wt.Done()
  31. for value := range dataCh {
  32. if value == Max {
  33. // 此唯一的接收者同时也是stopCh通道的
  34. // 唯一发送者。尽管它不能安全地关闭dataCh数
  35. // 据通道,但它可以安全地关闭stopCh通道。
  36. close(stopCh)
  37. return
  38. }
  39. log.Println(value)
  40. }
  41. }()
  42. wt.Wait()
  43. }

Tässä menetelmässä lisäämme ylimääräisen signaalikanavan stopCh, jonka avulla vastaanotin ilmoittaa lähettäjälle, ettei sen tarvitse enää vastaanottaa dataa. Lisäksi tämä menetelmä ei sulje dataCh:tä, kun kanavaa ei enää käytä mikään korutiini, se kerätään vähitellen, riippumatta siitä, onko se suljettu.

Tämän menetelmän tyylikkyys on, että sulkemalla yhden kanavan lopetat toisen kanavan käytön ja suljet siten epäsuorasti toisen kanavan.

Skenaario 3: M vastaanotinta ja N lähettäjää

Emme voi saada joko vastaanottajaa tai lähettäjää sulkemaan datan lähettämiseen käytettyä kanavaa, emmekä voi saada yhtä useista vastaanottimista sulkemaan ylimääräistä signalointikanavaa. Molemmat käytännöt rikkovat kanavan sulkemisperiaatetta.

Voimme kuitenkin esitelläVälivälittäjärooli ja se sulkee lisäsignalointikanavia ilmoittaakseen kaikille vastaanottajille ja lähettäjille työn päättymisestä

Esimerkki koodista:

  1. package main
  2. import (
  3. "log"
  4. "math/rand"
  5. "strconv"
  6. "sync"
  7. )
  8. func main() {
  9. const Max = 100000
  10. const NumReceivers = 10
  11. const NumSenders = 1000
  12. var wt sync.WaitGroup
  13. wt.Add(NumReceivers)
  14. dataCh := make(chan int)
  15. stopCh := make(chan struct{})
  16. // stopCh是一个额外的信号通道。它的发送
  17. // 者为中间调解者。它的接收者为dataCh
  18. // 数据通道的所有的发送者和接收者。
  19. toStop := make(chan string, 1)
  20. // toStop是一个用来通知中间调解者让其
  21. // 关闭信号通道stopCh的第二个信号通道。
  22. // 此第二个信号通道的发送者为dataCh数据
  23. // 通道的所有的发送者和接收者,它的接收者
  24. // 为中间调解者。它必须为一个缓冲通道。
  25. var stoppedBy string
  26. // 中间调解者
  27. go func() {
  28. stoppedBy = <-toStop
  29. close(stopCh)
  30. }()
  31. // 发送者
  32. for i := 0; i < NumSenders; i++ {
  33. go func(id string) {
  34. for {
  35. value := rand.Intn(Max)
  36. if value == 0 {
  37. // 为了防止阻塞,这里使用了一个尝试
  38. // 发送操作来向中间调解者发送信号。
  39. select {
  40. case toStop <- "发送者#" + id:
  41. default:
  42. }
  43. return
  44. }
  45. select {
  46. case <-stopCh:
  47. return
  48. case dataCh <- value:
  49. }
  50. }
  51. }(strconv.Itoa(i))
  52. }
  53. // 接收者
  54. for i := 0; i < NumReceivers; i++ {
  55. go func(id string) {
  56. defer wt.Done()
  57. for {
  58. select {
  59. case <-stopCh:
  60. return
  61. case value := <-dataCh:
  62. if value == Max {
  63. // 为了防止阻塞,这里使用了一个尝试
  64. // 发送操作来向中间调解者发送信号。
  65. select {
  66. case toStop <- "接收者:" + id:
  67. default:
  68. }
  69. return
  70. }
  71. log.Println(value)
  72. }
  73. }
  74. }(strconv.Itoa(i))
  75. }
  76. wt.Wait()
  77. log.Println("被" + stoppedBy + "终止了")
  78. }