Shanraq.org Shanraq.org
Горутина мен арна: вебке не үшін керек
IT

Go тілі: нөлден өз блогыңызға дейін 42-сабақ (барлығы 50)

Горутина мен арна: вебке не үшін керек

Go курсының қырық екінші сабағы. Сервер HTTP сабағынан бері конкурентті, әрі мұны сіз таңдаған жоқсыз: әр сұраныс — өз горутинасы. Құлыпсыз санауыш алдымен бес жүзден бір оқылымды жоғалтатыны, сосын бүкіл серверді құлататыны және go test -race мұны алдын ала табатыны өлшенді.

Не үшін керек

Горутина туралы әдетте бір нәрсені жылдамдату керек мысалмен айтады. Веб үшін бұл қате бастама: сіздің серверіңіз әлдеқашан конкурентті, әрі ол сіздің қатысуыңызсыз солай болған.

http.Server әр сұранысты бөлек горутинада іске қосады. Бір оқырман жауап күтіп тұрғанда, екіншісі әлдеқашан қызмет алып жатыр. Бұл әрі ыңғайлы, әрі қауіпті: өңдеушілер бірге қозғайтынның бәрін олар бір мезгілде қозғайды.

Блог бәрін дерекқорда сақтап тұрғанда бақытсыздық жоқ еді: кім жазатынын SQLite өзі шешеді. Жадта айнымалы ашсаңыз болды — оқылым санауышы, кеш, кез келген нәрсе — жарыс пайда болады.

Бірден тұтас

Жаңа қалта, go mod init sabaq39:

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
)

// views оқылымды санайды. Өңдеуші әр сұранысқа өз горутинасында орындалады,
// демек бұл айнымалыны бір мезгілде қозғайды.
type views struct {
	mu sync.Mutex
	n  map[string]int
}

func newViews() *views { return &views{n: map[string]int{}} }

// Құлыпсыз Add — былай жазуға болмайды, неге екені төменде көрінеді.
func (v *views) AddUnsafe(slug string) { v.n[slug]++ }

// Құлыппен Add: ішке бір мезгілде біреуді ғана кіргізеді.
func (v *views) Add(slug string) {
	v.mu.Lock()
	defer v.mu.Unlock()
	v.n[slug]++
}

func (v *views) Count(slug string) int {
	v.mu.Lock()
	defer v.mu.Unlock()
	return v.n[slug]
}

func handler(v *views, safe bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if safe {
			v.Add("dala")
		} else {
			v.AddUnsafe("dala")
		}
		fmt.Fprint(w, "ok")
	})
}

// hammer бір мезгілде жүздеген сұраныс жіберіп, бәрі бітуін күтеді.
func hammer(h http.Handler, n int) {
	srv := httptest.NewServer(h)
	defer srv.Close()

	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			resp, err := http.Get(srv.URL)
			if err == nil {
				resp.Body.Close()
			}
		}()
	}
	wg.Wait()
}

func main() {
	v := newViews()
	hammer(handler(v, true), 500)
	fmt.Println("құлыппен:   ", v.Count("dala"), "500-ден")

	u := newViews()
	hammer(handler(u, false), 500)
	fmt.Println("құлыпсыз:   ", u.Count("dala"), "500-ден")
}

Іске қосамыз — әртүрлі нәтиже аламыз:

құлыппен:    500 500-ден
құлыпсыз:    499 500-ден
құлыппен:    500 500-ден
fatal error: concurrent map writes

goroutine 5411 [running]:

Бір бағдарлама: бір іске қосқанда бес жүзден бір оқылым жоғалтты, екіншісінде өзін түгел құлатты.

Талдау

Жарыс: кейде өтірік айтатын, кейде құлайтын бағдарлама

AddUnsafe v.n[slug]++ істейді. Бір әрекет сияқты көрінеді, ал шын мәнінде үшеу: оқу, қосу, жазу.

Екі горутина бір мәнді оқып үлгереді, әрқайсысы өз бірлігін қосады, әрқайсысы жазады — сонда бір оқылым жоғалады. 499 500-ден осылай шығады.

Картамен одан да жаман. map-қа бір мезгілде жазу — жай ғана қате сан емес: Go мұны әдейі тексеріп, бағдарламаны fatal error: concurrent map writes деп құлатады. Бұл recover-мен ұстауға болатын паника емес, процестің лезде өлуі. Сіздің серверіңіз бәріне жауап беруін қояды.

Елестетіп көріңіз. Қағаз журналдағы бір жолды екі адам түзетіп жатыр. Әрқайсысы «17» деп оқыды, әрқайсысы сызып, «18» деп жазды. Келуші екеу еді, ал біреуі жазылды.

Ең жаманы — үшінші нәтиже, бірінші шығудағы: бағдарлама дұрыс жұмыс істеді. Жарыс сұраныс бойынша қайталанбайды. Ол жүктемені күтеді, яғни ноутбугіңізде емес, жұмыстағы серверде атады.

-race: көрінбейтінді табады

Сондықтан жарысты көзбен іздемейді. Оны Go іздейді:

құлыппен:    500 500-ден
құлыпсыз:    490 500-ден
==================
WARNING: DATA RACE
Read at 0x00c000dd2780 by goroutine 3668:
  runtime.mapdelete_faststr()
      /usr/local/go/src/internal/runtime/maps/runtime_faststr_swiss.go:396 +0x8c
  main.(*views).AddUnsafe()
      main.go:20 +0x58
  main.main.handler.func2()
      main.go:40 +0x68
  net/http.HandlerFunc.ServeHTTP()
      /usr/local/go/src/net/http/server.go:2294 +0x48
  net/http.serverHandler.ServeHTTP()

Үзіндідегі жолдар файл атына дейін қысқартылды.

go run -race . және go test -race ./... жарыс детекторын қосады. Ол жадты кім, қашан қозғайтынын бақылап, қақтығыстың екі жағын да файл аты мен жол нөмірімен басып шығарады — мұнда main.go:20, дәл сол v.n[slug]++.

Детектор бұл жолы бәрі сәйкес келсе де жарысты табады. Ол бағдарламаны бірнеше есе баяулатады, сондықтан оны ұрыста қосулы ұстамайды — оны тестте іске қосады, әрі бұл әдетте кодты жіберер алдындағы тексерудегі бір жол.

Ереже қарапайым: ортақ күйі бар кез келген бағдарлама үшін тестте -race міндетті. Вебте ондай күй әрқашан бар.

Құлып: sync.Mutex және defer Unlock

Мұны құлып емдейді:

func (v *views) Add(slug string) {
	v.mu.Lock()
	defer v.mu.Unlock()
	v.n[slug]++
}

Lock ішке бір горутинаны кіргізеді; қалғандары Unlock болғанша осы жолда күтеді. Мұндағы defer — әшекей емес: онсыз кез келген ерте return немесе паника құлыпты мәңгі жабық қалдырады, әрі сервер қатып қалады.

Мьютексті ол қорғайтын нәрсенің қасына қояды — сол құрылымның өрісі етіп. Сонда оның нақты нені күзететіні көрінеді, әрі оны есте ұстаудың қажеті жоқ.

Есте ұстайтыны: құлып астында ең азын істейді. Құлыпты ұстап тұрып дерекқорға немесе желіге бару — жылдам серверді кезекке айналдырудың сенімді тәсілі.

Арна: фондық жұмыс кезегі

Мьютекс — ортақ айнымалы туралы. Арна — жұмысты бір горутинадан екіншісіне беру туралы, әрі вебте ол басқа нәрсе үшін керек: оқырман күтуге тиіс емес нәрсені күтпеуі үшін.

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// Фондық жұмыс кезегі. Жүз тапсырмалық буфер: өңдеуші тапсырманы салып,
	// оқырманға бірден жауап береді, оның бітуін күтпейді.
	jobs := make(chan string, 100)

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		// Арна бойынша цикл арна жабылғанда өзі аяқталады.
		for slug := range jobs {
			time.Sleep(50 * time.Millisecond) // телеграмға жіберу сияқты
			fmt.Println("  жіберілді:", slug)
		}
	}()

	start := time.Now()
	for _, slug := range []string{"dala", "salem", "shanyraq"} {
		jobs <- slug
		fmt.Printf("оқырманға %s ішінде жауап бердік\n", time.Since(start).Round(time.Millisecond))
	}

	// Арнаны жабу — «тапсырма бітті» деу. Онсыз горутинадағы цикл мәңгі
	// күтер еді, ал Wait ешқашан күтіп жетпес еді.
	close(jobs)
	wg.Wait()
	fmt.Println("бәрі бітті:", time.Since(start).Round(10*time.Millisecond))
}
оқырманға 0s ішінде жауап бердік
оқырманға 0s ішінде жауап бердік
оқырманға 0s ішінде жауап бердік
  жіберілді: dala
  жіберілді: salem
  жіберілді: shanyraq
бәрі бітті: 150ms

Оқырманға үш рет лезде жауап берілді, ал жүз елу миллисекундтық жұмыс кейін бітті. Блогта телеграмға пост осылай жіберіледі, хат жөнелтіледі, кеш тазаланады — нәтижесі оқырманға керек емес нәрсенің бәрі.

Жүз тапсырмалық буфер — сән емес, шешім: кезек толса, jobs <- slug бөгеледі, әрі өңдеуші күте бастайды. Кейде бұл дұрыс (жоғалтқаннан күткен артық), кейде дұрыс емес, сонда default-ы бар select жазып, тапсырманы журналға жазумен бірге тастайды.

WaitGroup және жабылған арна

Соңындағы екі жол көрінгеннен маңыздырақ.

close(jobs) «тапсырма бұдан былай болмайды» дейді. for slug := range jobs циклі тек жабылғаннан кейін өзі аяқталады; онсыз горутина мәңгі күтер еді.

wg.Wait() горутина жұмысын бітіргенше main-ді ұстап тұрады. Онсыз бағдарлама аяқталып, барлық горутинаны өзімен бірге алып кетеді — бітпеген жұмысымен қоса. Айтпақшы, бұл жиі қойылатын сұрақтың жауабы: горутина өз бағдарламасынан аспайды.

Не істемейді

Әр нәрсеге бір горутина. Өңдеушідегі go doSomething() тегін көрінеді, бірақ мұндай әр горутина өз бетінше өмір сүреді: оны ешкім күтпейді, оның паникасы бүкіл серверді құлатады, ал сұраныс көбейгенде олар мыңдап пайда болады. Фондық жұмысты жоғарыдағыдай арна арқылы бір-екі жұмысшыға береді.

Горутинадағы паниканы орамамен ұстау. Қате туралы сабақтағы recoverPanic тек өзі тұрған горутинаны қорғайды. Фондық горутинадағы паника процесті түгел өлтіреді, сондықтан recover-ді оның ішіне қояды.

Горутинаны жылдамдық туралы деп ойлау. Олар күту туралы. Бір сұраныс дерекқорды күтіп тұрғанда, екіншісі есептейді — бүкіл пайда сол. Сандарды олар жылдамырақ қоспайды.

Сабақ картасы

Сабақ картасы: құлып астындағы ортақ жад, арна арқылы жұмыс

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Жарыс кәдімгі қатеден неге қауіптірек?
  2. Lock қасында defer не үшін керек?
  3. main аяқталса, фондық горутинаға не болады?

Тапсырма

Міндетті. Блогта жадтағы оқылым санауышын ашыңыз: мақала өңдеушісі оны өсіреді, «блог туралы» беті көрсетеді. Алдымен құлыпсыз — әрі есепті көру үшін go test -race ./... іске қосыңыз. Сосын құлыппен жауып, есептің жоғалғанына көз жеткізіңіз.

Мұның бәрі step-27 ішінде жасалған — өзіңіз жазғаннан кейін салыстырыңыз.

Қалауыңызша.

  • Мьютексті sync.RWMutex-ке ауыстырып, жиі оқығанда RLock не беретінін түсіндіріңіз.
  • Журналға жазуды арна арқылы бір жұмысшыға беріп, жауаптардың жылдамдағанын тексеріңіз.
  • Жұмысшы мысалынан wg.Wait()-ті алып тастап, қанша тапсырма бітіп үлгеретініне қараңыз.

Блогта бұл қайда тұрады

Блог жадтағы алғашқы ортақ күйді алады — әрі онымен бірге тесттерді -race-пен іске қосу әдетін.

Қарыздар. Санауыш қайта қосқанға дейін өмір сүреді: оны сақтау үшін дерекқор керек. Жұмысшы бізде біреу әрі дерекқордағы кезегі жоқ — процесс құласа, тапсырмалар жоғалады. Ал оқырман қойындыны жапқанда жұмысты тоқтата алатын context — келесі сабақ.

Жауаптар

Жауаптарды көрсету
  1. Себебі ол сұраныс бойынша қайталанбайды: сол бағдарлама бірде дұрыс жұмыс істейді, бірде дерек жоғалтады, бірде fatal error-мен құлайды. Кәдімгі қате бірдей сынады, ал жарыс жүктемені күтеді — яғни жұмыстағы серверді.
  2. Функциядан кез келген жолмен шыққанда, соның ішінде ерте return мен паникада, құлып ашылуы үшін. Ұмытылған Unlock бір горутинаны емес, оның артынан келетіндердің бәрін тоқтатады.
  3. Ол бағдарламамен бірге жоғалады, жұмысын бітірмей. Горутина өз бағдарламасынан аспайды, сондықтан оны күтеді — мысалы, WaitGroup арқылы.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.