×Закрыть

Горутинова спокуса. Приклади використання горутин, де вони насправді зайві

Це компактна збірка прикладів використання горутин, де вони насправді зайві.

Передісторія або просто додайте «go»

На Golang просто розробляти і також розпаралелити збереження статистики, збереження онлайну або схожі задачі, додавши ключове слово go.
Така простота допомагає зосередитись на бізнес-логіці але інколи призводить до бездумного додання go перед задачами які можна розпаралелити.

Приклад перший, запуск горутини в кінці горутини

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func endLongtimeAction(now int64) {
	time.Sleep(time.Microsecond)

	// some logic
	log.Printf("endLongtimeAction %d", now)
}

func longtimeAction(now int64) {
	time.Sleep(time.Microsecond)

	// some logic
	log.Printf("longtimeAction    %d", now)

	// useless start goroutine
	go endLongtimeAction(now)
}

func hello(w http.ResponseWriter, r *http.Request) {
	// useful start goroutine
	go longtimeAction(time.Now().UnixNano())

	fmt.Fprintf(w, "hello\n")
}

func main() {
	http.HandleFunc("/hello", hello)

	log.Printf("start server on port 8090")

	http.ListenAndServe(":8090", nil)
}

в цьому прикладі горутина перед викликом endLongtimeAction зайва

Приклад другий, використання горутини для простої дії

Є схожий на попередній приклад hello в якому можна розпаралелити AddView:

func hello(w http.ResponseWriter, r *http.Request) {
	go AddView(fontThemeID(), country(r))

	fmt.Fprintf(w, "hello\n")
}
AddView просте збереження в буфер.
import "sync"

type FontThemeKey struct {
	FontThemeID uint8
	Country     string
}

type FontThemeValue struct {
	ViewCount     uint32
	RegisterCount uint32
}

var (
	fontThemeBuffer = make(map[FontThemeKey]FontThemeValue)
	fontThemeMutex  sync.Mutex
)

func AddView(fontThemeID uint8, country string) {
	var key = FontThemeKey{
		FontThemeID: fontThemeID,
		Country:     country,
	}

	fontThemeMutex.Lock()
	defer fontThemeMutex.Unlock()

	var value = fontThemeBuffer[key]
	value.ViewCount += 1
	fontThemeBuffer[key] = value
}

func Flush() error {
	return store(swap())
}

func swap() map[FontThemeKey]FontThemeValue {
	fontThemeMutex.Lock()
	defer fontThemeMutex.Unlock()

	var result = fontThemeBuffer
	fontThemeBuffer = make(map[FontThemeKey]FontThemeValue, len(result))
	return result
}

func store(map[FontThemeKey]FontThemeValue) error {
	// store to stats

	return nil
}

тепер зробимо простий тест щоб перевірити чи пришвидшує go швидкодію на простому прикладі:


import (
	"sync/atomic"
	"testing"
)

const (
	fontThemeCount = 15
)

var (
	fixtureCountries     = []string{"C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8"}
	fixtureCountryLength = uint32(len(fixtureCountries))
)

func reset() {
	fontThemeBuffer = make(map[FontThemeKey]FontThemeValue)
}

func BenchmarkAddView(b *testing.B) {
	reset()

	var index uint32

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			var i = atomic.AddUint32(&index, 1)

			AddView(uint8(i%fontThemeCount), fixtureCountries[i%fixtureCountryLength])
		}
	})
}

func BenchmarkAddViewExtraGoroutine(b *testing.B) {
	reset()

	var index uint32

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			var i = atomic.AddUint32(&index, 1)

			// useless goroutine
			go AddView(uint8(i%fontThemeCount), fixtureCountries[i%fixtureCountryLength])
		}
	})
}
результати тесту:
Назва тесту Кількість ітерацій Середній час ітерації Виділення пам’яті
AddView 6413756 185 ns/op 0 B/op
AddViewExtraGoroutine 2628880 403 ns/op 58 B/op
що показує що зайва горутина лише сповільнила виконання

Приклад третій, горутина за шаблоном

На сторінці продукту треба відобразити пару блоків, перший «рекомендації або схожі товари» та другий «разом з цим купують».
Отримання цих блоків можна легко розпаралелити.
func ExtraGoodCodes(goodCode uint32) ([]uint32, []uint32) {
	var wg = new(sync.WaitGroup)

	var (
		recommendations []uint32
		supplies        []uint32
	)

	wg.Add(1)
	go func() {
		recommendations = getRecommendationGoods(goodCode)

		wg.Done()
	}()

	wg.Add(1)
	go func() {
		supplies = getSupplyGoods(goodCode)

		wg.Done()
	}()

	wg.Wait()

	return recommendations, supplies
}
І хоч логіка правильна, але одну горутину можна прибрати:

func ExtraGoodCodes(goodCode uint32) ([]uint32, []uint32) {
	var wg = new(sync.WaitGroup)

	var (
		recommendations []uint32
		supplies        []uint32
	)

	wg.Add(1)
	go func() {
		recommendations = getRecommendationGoods(goodCode)

		wg.Done()
	}()

	supplies = getSupplyGoods(goodCode)

	wg.Wait()

	return recommendations, supplies
}

Епілог

Всі ці приклади зустрічав під час розробки, тому в коментарях буде цікаво дізнатись які з наведених зустрічали ви.
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Да, кстати, чуть не забыл. wg.Done() нужно обязательно через defer ставить в начале перед залоченым кодом, как и любую другую разблокировку, иначе в случае паники всё повиснет в дидлоке, новичкам это важно знать.

Зустрічав цю пораду багаторазово, але в такому випадку panic просто припиняє виконання всієї програми

Ось приклад який дійсно відновлює:

go func(i int) {
	defer wg.Done()
	defer func() {
		recover()
	}()

	panicAfterN(i, after)
}(i)

приклад зі статті використання Defer у Go

Паника может выше ловиться, например на сервере, так что когда один вызов хендлера валится, остальные работают. И что выходит: хендлер падает в панику, лок остаётся, и повторные вызовы того же хендлера ожидают разблокировку, которой не будет. Можно waitgroup/mutex объявить локально, тогда на первый взгляд дидлока не будет, но на самом деле появятся зависшие горутины, и утечка памяти. Ну и кроме паники может быть просто где-то в теле функции return до разблокировки.

Зараз в Golang зловити паніку можна лише в поточній горутині.
А коли є лише defer, бо так пишуть-кажуть інші, без recover, то такий код виглядає безпечно і припиняє програму після паніки.

Це добре описав в блоці «Recover тільки для поточної горутини» в статті.

Где горутины не нужны — это ситуативно. Ну например, для 1-го случая, горутина в конце будет всё-же нужна, если longtimeAction будет вызываться с ожиданием результата. Ещё не помешает рассмотреть, в каких случаях горутины нужны. Например, чтоб распараллелить код в хендлерах сервера, и сократить задержку между запросом и ответом.

Де горутини потрібні вже добре описано в книжках з Golang.

Стараюсь писати про те що практично зустрічаю в роботі, детальні інструкції які зможуть використовувати інші спеціалісти, або ж писати про помилки які робив сам та помічав у колег.

Отличный материал, большое спасибо !

Только вчера хотел описать работу Горутин для новичков. Статья шикарная.

Подписаться на комментарии