Синхронізація в Go: використання спільних даних

Привіт, мене звати Ярослав. Працюю в компанії Evrius, три роки розробляю на Go, а раніше писав на PHP.

Помітив, що коли на співбесіді з Go питають про синхронізацію, то переважно запитання звучить: «Як розпаралелити задачу?». Про це я писав раніше. Але на співбесіді питають про одне, а в проєкті — інше, там значно більше випадків, коли дані читаються з багатьох горутин, а оновлюють в одній. Тоді краще використовувати оптимальні структури sync.RWMutex та atomic.Value. Про це й буде стаття. Тут ви знайдете приклади коду, помилок, тести, бенчмарки.

Матеріал буде цікавий спеціалістам, які збираються перекваліфікуватись на Go або вже мають досвід з цієї мовою та хочуть краще структурувати свої знання.

Власне, я згадав про PHP, бо маю багато знайомих PHP-розробників і інколи відповідаю на запитання «Як перекваліфікуватись з PHP на Go». Також інколи чую, як PHP-розробникам зі знанням Go, рекрутери пропонують розглянути вакансії Golang Team Lead. Якщо вам буде цікаво почитати про те, як перекваліфікуватись з PHP на Go, дайте знати про це в коментарях чи напишіть мені в LinkedIn, щоб я розумів актуальність питання і мав мотивацію взятись за статтю з прикладами коду та порівнянням.

Безпечне читання даних без синхронізації

Коли в коді просто читаємо завчасно завантажені дані без оновлень з багатьох горутин, то синхронізація зайва. Штучний приклад, коли багато горутин читають одні дані:

package main

import (
	"fmt"
)

func main() {
	var values = map[byte]int{
		'A': 1,
		'B': 2,
		'C': 3,
	}

	var keys = []byte{'A', 'B', 'C'}

	// panic: too many concurrent operations on a single file or socket (max 1048575)
	for try := 1048575; try > 0; try-- {
		go func() {
			for i, key := range keys {
				// safe goroutine read data without any sync
				var value = values[key]

				fmt.Printf("index = %d, key = %c, value = %d\n", i, key, value)
			}
		}()
	}
}

Я запустив цей приклад з параметром race (для перевірки на data race): go run main.go -race.

Якщо ж у першій горутині будемо читати з мапи, а в другій — писати в мапу, то отримаємо помилку fatal error: concurrent map read and map write. Про цю помилку також написано в офіційному блозі Go maps in action:

Maps are not safe for concurrent use: it’s not defined what happens when you read and write to them simultaneously.

Розглянемо її детальніше.

Fatal error: concurrent map read and map write

Якщо взяти попередній приклад і додати одну горутину, яка буде писати в мапу, то отримаємо помилку, навіть якщо будемо оновлювати вже наявні ключі.

// BAD CODE WITH ERROR EXAMPLE
package main

import (
	"fmt"
)

func main() {
	var values = map[byte]int{
		'A': 1,
		'B': 2,
		'C': 3,
	}

	var keys = []byte{'A', 'B', 'C'}

	go func() {
		for {
			for _, key := range keys {
				// UNSAFE, fatal error: concurrent map read and map write
				values[key] = values[key] + 1
			}
		}
	}()

	// panic: too many concurrent operations on a single file or socket (max 1048575)
	for try := 1048575; try > 0; try-- {
		go func() {
			for i, key := range keys {
				// UNSAFE, fatal error: concurrent map read and map write
				var value = values[key]

				fmt.Printf("index = %d, key = %c, value = %d\n", i, key, value)
			}
		}()
	}
}
go run main.go -race

Після запуску програма виведе в термінал очікувані повідомлення ~ index = 0, key = A, value = 850 N разів, а потім завершиться помилкою:

fatal error: concurrent map read and map write

Повна заміна мапи без синхронізації

Знову досліджуємо попередній приклад і замість оновлення мапи будемо робити її повну заміну. Перевіримо, чи це безпечно.

// BAD CODE WITH UNSAFE EXAMPLE
package main

import (
	"fmt"
)

func main() {
	var values = map[byte]int{
		'A': 1,
		'B': 2,
		'C': 3,
	}

	var keys = []byte{'A', 'B', 'C'}

	go func() {
		for {
			var replaceValues = make(map[byte]int, len(values))

			for _, key := range keys {
				replaceValues[key] = values[key] + 1
			}

			// UNSAFE replace on some GOARCH, for example arm(32)
			values = replaceValues
		}
	}()

	// panic: too many concurrent operations on a single file or socket (max 1048575)
	for try := 1048575; try > 0; try-- {
		go func() {
			for i, key := range keys {
				// UNSAFE read on some GOARCH, for example arm(32)
				var value = values[key]

				fmt.Printf("index = %d, key = %c, value = %d\n", i, key, value)
			}
		}()
	}
}
go run main.go -race

Після запуску жодних помилок у терміналі, отже, повна заміна мапи виконалась успішно на моєму комп’ютері.

go env
GOARCH="amd64"
GOHOSTARCH="amd64"

На архітектурі amd64 — повна заміна мапи (вказівник розміром 8 байтів, або 64 біти) є атомарною операцією (безпечною), але на інших архітектурах, таких як 32-бітна arm, будуть помилки. Тому треба писати код, який буде безпечний і на інших архітектурах. Для цього потрібно правильно використовувати синхронізації: sync.Mutex, sync.RWMutex та atomic.Value, які гарантують потокобезпечність.

Універсальний sync.Mutex

Завдання sync.Mutex — надати ексклюзивний доступ до даних за допомогою двох методів Lock() та Unlock(). Візьмемо попередні приклади і зробимо їх потокобезпечними, використовуючи sync.Mutex. Приклад, в якому була помилка fatal error: concurrent map read and map write з sync.Mutex, буде саме таким:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var values = map[byte]int{
		'A': 1,
		'B': 2,
		'C': 3,
	}

	var keys = []byte{'A', 'B', 'C'}

	var mu = new(sync.Mutex)

	go func() {
		for {
			for _, key := range keys {
				mu.Lock()
				values[key] = values[key] + 1
				mu.Unlock()
			}
		}
	}()

	// panic: too many concurrent operations on a single file or socket (max 1048575)
	for try := 1048575; try > 0; try-- {
		go func() {
			for i, key := range keys {
				mu.Lock()
				var value = values[key]
				mu.Unlock()

				fmt.Printf("index = %d, key = %c, value = %d\n", i, key, value)
			}
		}()
	}
}
go run main.go -race

Бачимо успішне завершення.

Але правильніше буде винести дані і мютекс в одну структуру, яка буде відповідати за доступ до даних. Об’єднати в одну структуру — це стандартне рішення, бо більше зрозуміло, які маніпуляції з даними відбуваються.

package main

import (
	"fmt"
	"sync"
)

type SyncCounter struct {
	data map[byte]int
	mu   sync.Mutex
}

func NewSyncCounter(data map[byte]int) *SyncCounter {
	return &SyncCounter{
		data: data,
	}
}

func (c *SyncCounter) Increment(key byte) {
	c.mu.Lock()
	c.data[key] += 1
	c.mu.Unlock()
}

func (c *SyncCounter) Get(key byte) int {
	c.mu.Lock()
	var value = c.data[key]
	c.mu.Unlock()

	return value
}

func main() {
	var values = NewSyncCounter(map[byte]int{
		'A': 1,
		'B': 2,
		'C': 3,
	})

	var keys = []byte{'A', 'B', 'C'}

	go func() {
		for {
			for _, key := range keys {
				values.Increment(key)
			}
		}
	}()

	// panic: too many concurrent operations on a single file or socket (max 1048575)
	for try := 1048575; try > 0; try-- {
		go func() {
			for i, key := range keys {
				var value = values.Get(key)

				fmt.Printf("index = %d, key = %c, value = %d\n", i, key, value)
			}
		}()
	}
}

На початку знайомства з Go універсального sync.Mutex та каналів вистачить для написання коду, але якщо захочете замінити на sync.RWMutex чи atomic.Value, то краще пишіть тести і запускайте з параметром race, щоб перевірити наявність помилок і можливе погіршення швидкодії після оптимізації.

Повна заміна даних з sync.Mutex, sync.RWMutex та atomic.Value

Далі зосередимось тільки на структурах з даними та потокобезпечним доступом. Коли повністю замінюємо дані, достатньо обгорнути мютексом тільки заміну і отримання вказівника:

import "sync"

type CityOnlineMutexMap struct {
	data map[string]uint32
	mu   sync.Mutex
}

func NewCityOnlineMutexMap(data map[string]uint32) *CityOnlineMutexMap {
	return &CityOnlineMutexMap{
		data: data,
	}
}

func (c *CityOnlineMutexMap) Update(data map[string]uint32) {
	c.mu.Lock()
	c.data = data
	c.mu.Unlock()
}

func (c *CityOnlineMutexMap) Get(cityName string) uint32 {
	c.mu.Lock()
	var data = c.data
	c.mu.Unlock()

	return data[cityName]
}

У цьому прикладі пошук в мапі за ключем я виніс за мютекс, бо це безпечно. Коли дані частіше читаються, ніж пишуться, то можемо використати більш оптимальний для таких дій sync.RWMutex:

import "sync"

type CityOnlineRWMutexMap struct {
	data map[string]uint32
	mu   sync.RWMutex
}

func NewCityOnlineRWMutexMap(data map[string]uint32) *CityOnlineRWMutexMap {
	return &CityOnlineRWMutexMap{
		data: data,
	}
}

func (c *CityOnlineRWMutexMap) Update(data map[string]uint32) {
	c.mu.Lock()
	c.data = data
	c.mu.Unlock()
}

func (c *CityOnlineRWMutexMap) Get(cityName string) uint32 {
	c.mu.RLock()
	var data = c.data
	c.mu.RUnlock()

	return data[cityName]
}

У цьому прикладі заміна буде відбуватись довше, бо під капотом RWMutex.Lock() викликається звичайний Mutex.Lock() та додаткові перевірки. Але читання через RWMutex.RLock швидше. Тести будуть далі.

Яка різниця між Mutex та RWMutex — одне зі стандартних питань на співбесіді. Якщо у вас є налаштований локально Go і IDE, то можете зайти в реалізацію RWMutex та почитати коментарі, які пояснюють, як працює RWMutex:

// RLock
// A writer is pending, wait for it.

// Lock
// Announce to readers there is a pending writer.
// Wait for active readers.
А найефективніший варіант повної заміни даних через atomic.Value:
import (
	"sync/atomic"
)

type CityOnlineAtomicMap struct {
	data atomic.Value
}

func NewCityOnlineAtomicMap(data map[string]uint32) *CityOnlineAtomicMap {
	var result = new(CityOnlineAtomicMap)

	result.Update(data)

	return result
}

func (c *CityOnlineAtomicMap) Update(data map[string]uint32) {
	c.data.Store(data)
}

func (c *CityOnlineAtomicMap) Get(cityName string) uint32 {
	var data = c.data.Load().(map[string]uint32)

	return data[cityName]
}

А тепер напишемо тест, щоб порівняти, яка структура найкраща для повної заміни даних:

package main

import (
	"sync"
	"testing"
	"time"
)

type cityOnlineMap interface {
	Update(data map[string]uint32)
	Get(cityName string) uint32
}

func BenchmarkCityOnlineMutexMap(b *testing.B) {
	benchmarkCityOnlineMap(b, NewCityOnlineMutexMap(getCityOnlineMap()))
}

func BenchmarkCityOnlineRWMutexMap(b *testing.B) {
	benchmarkCityOnlineMap(b, NewCityOnlineRWMutexMap(getCityOnlineMap()))
}

func BenchmarkCityOnlineAtomicMap(b *testing.B) {
	benchmarkCityOnlineMap(b, NewCityOnlineAtomicMap(getCityOnlineMap()))
}

func benchmarkCityOnlineMap(b *testing.B, data cityOnlineMap) {
	b.Helper()

	var once = new(sync.Once)

	var cities = []string{"kyiv", "kharkiv", "lviv", "dnipro", "odessa"}

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		var isWriter = false

		once.Do(func() {
			isWriter = true
		})

		if isWriter {
			for pb.Next() {
				data.Update(getCityOnlineMap())

				// read much more often than it is written
				time.Sleep(time.Microsecond)
			}
		} else {
			for pb.Next() {
				for _, cityName := range cities {
					_ = data.Get(cityName)
				}
			}
		}
	})
}

func getCityOnlineMap() map[string]uint32 {
	var now = uint32(time.Now().Unix())

	return map[string]uint32{
		"kyiv":    now,
		"kharkiv": now,
		"lviv":    now,
		"dnipro":  now,
		"odessa":  now,
	}
}
go test ./... -v -bench=. -benchmem
Назва тесту Середній час ітерації Виділення пам’яті
BenchmarkCityOnlineMutexMap 410 ns/op 4 B/op 0 allocs/op
BenchmarkCityOnlineRWMutexMap 241 ns/op 0 B/op 0 allocs/op
BenchmarkCityOnlineAtomicMap 10.4 ns/op 0 B/op 0 allocs/op

Якщо в задачі повна заміна даних, то краще використовувати atomic.Value як найефективнішу структуру (для цілочисельних даних є atomic.StoreUint64, atomic.StoreUint32, atomic.StoreInt64 та atomic.StoreInt32).

Екзотичні варіанти синхронізації через канали

Перший екзотичний варіант, який бачив у реальному проєкті:

// BAD CODE EXAMPLE, DON'T COPY-PASTE
type CityOnlineChanMutexMap struct {
	data      map[string]uint32
	chanMutex chan struct{}
}

func NewCityOnlineChanMutexMap(data map[string]uint32) *CityOnlineChanMutexMap {
	return &CityOnlineChanMutexMap{
		data:      data,
		chanMutex: make(chan struct{}, 1),
	}
}

func (c *CityOnlineChanMutexMap) Update(data map[string]uint32) {
	c.chanMutex <- struct{}{}
	c.data = data
	_ = <-c.chanMutex
}

func (c *CityOnlineChanMutexMap) Get(cityName string) uint32 {
	c.chanMutex <- struct{}{}
	var result = c.data[cityName]
	_ = <-c.chanMutex

	return result
}

Під капотом каналів мютекси, і варіант вище поки найповільніший. У проєкті переписав його на sync.Mutex.

Другий екзотичний варіант помітив на просторах інтернету:

// BAD CODE EXAMPLE, DON'T COPY-PASTE
type cityOnlineRequest struct {
	cityName string
	online   chan uint32
}

type CityOnlineChanReactorMap struct {
	data        map[string]uint32
	requestChan chan cityOnlineRequest
	dataChan    chan map[string]uint32
}

func NewCityOnlineChanReactorMap(data map[string]uint32) *CityOnlineChanReactorMap {
	var result = &CityOnlineChanReactorMap{
		data:        data,
		requestChan: make(chan cityOnlineRequest),
		dataChan:    make(chan map[string]uint32),
	}

	go result.run()

	return result
}

func (c *CityOnlineChanReactorMap) Update(data map[string]uint32) {
	c.dataChan <- data
}

func (c *CityOnlineChanReactorMap) Get(cityName string) uint32 {
	var request = cityOnlineRequest{
		cityName: cityName,
		online:   make(chan uint32),
	}

	c.requestChan <- request

	return <-request.online
}

func (c *CityOnlineChanReactorMap) run() {
	for {
		select {
		case request := <-c.requestChan:
			request.online <- c.data[request.cityName]
		case data := <-c.dataChan:
			c.data = data
		}
	}
}

Цей варіант ще повільніший.

Назва тесту Середній час ітерації Виділення пам’яті
BenchmarkCityOnlineMutexMap 410 ns/op 4 B/op 0 allocs/op
BenchmarkCityOnlineRWMutexMap 241 ns/op 0 B/op 0 allocs/op
BenchmarkCityOnlineAtomicMap 10.4 ns/op 0 B/op 0 allocs/op
BenchmarkCityOnlineChanMutexMap 2037 ns/op 60 B/op 0 allocs/op
BenchmarkCityOnlineChanReactorMap 3740 ns/op 467 B/op 4 allocs/op

Якщо на вашому проєкті є щось схоже, можете відправити мені анонімно — і я додам в коментарях.

Повернення гетера

У попередніх прикладах розглядали взаємодію з даними всередині структури. Але якщо нам знадобиться отримати значення одразу для багатьох ключів, то робити синхронізацію на кожен ключ буде повільно. Якщо ж повернемо всю мапу, буде складніше розуміти, що далі відбувається з даними. Тож повернемо інтерфейс CityOnlineGetter:

import (
	"sync/atomic"
)

type CityOnlineGetter interface {
	Get(cityName string) uint32
}

type CityOnlineMap struct {
	data map[string]uint32
}

func (c *CityOnlineMap) Get(cityName string) uint32 {
	return c.data[cityName]
}

type CityOnlineAtomicMap struct {
	data atomic.Value
}

func NewCityOnlineAtomicMap(data map[string]uint32) *CityOnlineAtomicMap {
	var result = new(CityOnlineAtomicMap)

	result.Update(data)

	return result
}

func (c *CityOnlineAtomicMap) Update(data map[string]uint32) {
	c.data.Store(data)
}

func (c *CityOnlineAtomicMap) Get(cityName string) uint32 {
	var data = c.data.Load().(map[string]uint32)

	return data[cityName]
}

// BAD CODE
//func (c *CityOnlineAtomicMap) Load() map[string]uint32 {
//	var data = c.data.Load().(map[string]uint32)
//
//	return data
//}

func (c *CityOnlineAtomicMap) Load() CityOnlineGetter {
	var data = c.data.Load().(map[string]uint32)

	return &CityOnlineMap{
		data: data,
	}
}

Помилки, які можуть трапитися під час оновлення даних

Цю помилку, коли мютекс довго блокує виконання, бачив у реальних проєктах. Правильні приклади без помилок вже наведені у цій статті вище.

// BAD CODE EXAMPLE, DON'T COPY-PASTE
import (
	"sync"
	"time"
)

type CityOnline struct {
	CityName string
	Online   uint32
}

type CityOnlineTooLongUpdateMap struct {
	data map[string]uint32
	mu   sync.RWMutex
}

func NewCityOnlineTooLongUpdateMap(data map[string]uint32) *CityOnlineTooLongUpdateMap {
	return &CityOnlineTooLongUpdateMap{
		data: data,
	}
}

func (c *CityOnlineTooLongUpdateMap) UpdateBySlice(items []CityOnline) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// other logic
	// for example API call, emulate by time.Sleep
	time.Sleep(time.Second)

	c.data = make(map[string]uint32, len(items))

	for _, item := range items {
		c.data[item.CityName] = item.Online
	}
}

func (c *CityOnlineTooLongUpdateMap) Get(cityName string) uint32 {
	c.mu.RLock()
	defer c.mu.RUnlock()

	return c.data[cityName]
}

У такому прикладі RLock буде чекати defer c.mu.Unlock(), відповідно читання буде заблоковано на секунду. Те саме стосується звичайного Mutex-а.

atomic.Value та збереження інших типів

Коли в статті розглядаєш тільки мапи, то виникає відчуття, що тільки з мапами atomic.Value і використовують. Що ж, для слайсів і звичайних структур також підходить:

import "sync/atomic"

type CountrySettings struct {
	BlockCIDRs []string `json:"block_cidrs"`
}

type Settings struct {
	Countries map[string]CountrySettings `json:"countries"`
	PackSize  uint32                     `json:"pack_size"`
	Interval  uint32                     `json:"interval"`
	Debug     bool                       `json:"debug"`
}

type SettingsProxy struct {
	data atomic.Value
}

func NewSettingsProxy() *SettingsProxy {
	return &SettingsProxy{}
}

func (s *SettingsProxy) Update(value Settings) {
	s.data.Store(value)
}

func (s *SettingsProxy) Load() (Settings, bool) {
	var result, ok = s.data.Load().(Settings)

	return result, ok
}
import (
	"github.com/stretchr/testify/require"
	"testing"
)

func TestSettingsProxy(t *testing.T) {
	var proxy = NewSettingsProxy()

	{
		var settings, ok = proxy.Load()

		require.Equal(t, Settings{}, settings)
		require.Equal(t, false, ok)
	}

	{
		var expected = Settings{
			Countries: map[string]CountrySettings{
				"UA": {
					BlockCIDRs: nil,
				},
			},
			PackSize: 20000,
			Interval: 60,
			Debug:    true,
		}

		proxy.Update(expected)

		var actual, ok = proxy.Load()
		require.Equal(t, expected, actual)
		require.Equal(t, true, ok)
	}
}

У попередніх прикладах в конструкторі я робив збереження значення в atomic.Value, а тут навмисно пропустив, щоб вказати потребу перевірки при приведенні типу:

func (s *SettingsProxy) Load() (Settings, bool) {
	var result, ok = s.data.Load().(Settings)

	return result, ok
}

Бо інакше буде паніка:

panic: interface conversion: interface {} is nil

Такий варіант теж допустимий:

func (s *SettingsProxy) Update(value *Settings) {
	s.data.Store(value)
}

func (s *SettingsProxy) Load() (*Settings, bool) {
	var result, ok = s.data.Load().(*Settings)

	return result, ok
}

Застереження atomic.Value

Під час спроби зберегти nil чи різні типи отримуємо паніку. Ось тести, які показують таку поведінку:

func TestAtomicSuccessStoreNil(t *testing.T) {
	var atomicValue = new(atomic.Value)

	var value map[uint32]uint32 = nil

	atomicValue.Store(value)
}

func TestAtomicPanicOnStoreNil(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("The code did not panic")
		}
	}()

	var atomicValue = new(atomic.Value)

	// will panic
	atomicValue.Store(nil)
}

func TestAtomicPanicOnStoreNilInterface(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("The code did not panic")
		}
	}()

	var atomicValue = new(atomic.Value)

	var value interface{} = nil

	// will panic
	atomicValue.Store(value)
}

func TestAtomicPanicOnStoreDifferentTypes(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("The code did not panic")
		}
	}()

	var atomicValue = new(atomic.Value)

	{
		var value uint32

		// will success
		atomicValue.Store(value)
	}

	{
		var value uint32 = 1

		// will success
		atomicValue.Store(value)
	}

	{
		var value = ""

		// will panic
		atomicValue.Store(value)
	}
}

Ці застереження написані в коментарях до коду atomic.Value.

atomic.Value та збереження int32, int64, uint32, uint64

Для числових типів можна використовувати й atomic.Value:

import "sync/atomic"

type AtomicValueUint64 struct {
	data atomic.Value
}

func NewAtomicValueUint64(data uint64) *AtomicValueUint64 {
	var result = new(AtomicValueUint64)

	result.Update(data)

	return result
}

func (c *AtomicValueUint64) Update(data uint64) {
	c.data.Store(data)
}

func (c *AtomicValueUint64) Load() uint64 {
	return c.data.Load().(uint64)
}

Але можна простіше:

import "sync/atomic"

type AtomicUint64 struct {
	data uint64
}

func NewAtomicUint64(value uint64) *AtomicUint64 {
	var result = new(AtomicUint64)

	result.Update(value)

	return result
}

func (c *AtomicUint64) Update(value uint64) {
	atomic.StoreUint64(&c.data, value)
}

func (c *AtomicUint64) Load() uint64 {
	return atomic.LoadUint64(&c.data)
}

Є спеціальні функції в пакеті sync/atomic:

func LoadInt32(addr *int32) (val int32) {}
func LoadInt64(addr *int64) (val int64) {}
func LoadUint32(addr *uint32) (val uint32) {}
func LoadUint64(addr *uint64) (val uint64) {}
func StoreInt32(addr *int32, val int32) {}
func StoreInt64(addr *int64, val int64) {}
func StoreUint32(addr *uint32, val uint32) {}
func StoreUint64(addr *uint64, val uint64) {}

Епілог

У статті є повно прикладів, які трохи відрізняються, щоб краще запам’ятати. Бо коли код простий, то сам приклад зрозуміліший за текстовий опис.

Схожі оптимізації з atomic.Value для повної заміни даних знадобляться під час написання бібліотек. У робочому закритому проєкті краще використовуйте RWMutex, бо конвертація типів — це джерело помилок.

А ще під кожний тип даних треба писати окрему обгортку з RWMutex, як було у прикладах, бо в Go відсутні дженерики.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

👍НравитсяПонравилось7
В избранноеВ избранном5
Подписаться на тему «Go»
LinkedIn



Підписуйтесь: Soundcloud | Google Podcast | YouTube


21 комментарий

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

> На архітектурі amd64 — повна заміна мапи (вказівник розміром 8 байтів, або 64 біти) є атомарною операцією (безпечною), але на інших архітектурах, таких як 32-бітна arm, будуть помилки.

Чого це? Що там, що там або один вказівник без інших даних, і тоді на обох заміна атомарна, або ще є якісь дані, і тоді заміна не атомарна.

> c.mu.Lock()
> c.data = data
> c.mu.Unlock()

Навіть не використовуючи Go, я знаю, що краще Unlock() назначати через defer. (Пізніше ви так і зробили, але не пояснили різницю.)

> BenchmarkCityOnlineAtomicMap 10.4 ns/op

А тепер візьміть мапу на 100000 записів, а не на три.

> У такому прикладі RLock буде чекати defer c.mu.Unlock(), відповідно читання буде заблоковано на секунду.

А навіщо ви її так блокуєте? (Навіть для учбового прикладу — дивно.)

> Під час спроби зберегти nil чи різні типи отримуємо паніку.

Чому? При компіляції не типізовано, а при роботі чомусь типи починають контролюватись, і при тому nil не дозволено?

До речі: чому для типу Value є лише Load і Store, і нема хоча б CompareAndSwap?

Автори цього механізму явно вживають занадто тяжкі наркотики.

Вам доступна стаття, можете перевірити що і як працює, а потім вказати на помилки якщо знайдете.

Навіть не використовуючи Go, я знаю, що краще Unlock() назначати через defer. (Пізніше ви так і зробили, але не пояснили різницю.)

Стосовно defer писав статтю Використання defer у Go і там є відповідь на питання відмінності на початку статті. Спробуйте знайти настільки детальну статтю про defer англійською мовою.

А тепер візьміть мапу на 100000 записів, а не на три.

Benchmark-и зробив для порівняння швидкодії, як відношення.

А навіщо ви її так блокуєте? (Навіть для учбового прикладу — дивно.)

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

Чому? При компіляції не типізовано, а при роботі чомусь типи починають контролюватись, і при тому nil не дозволено?

Якщо почнете розробляти на Go то самостійно зможете знайти відповідь, підказка: A Tour of Go: The empty interface

До речі: чому для типу Value є лише Load і Store, і нема хоча б CompareAndSwap?

Тому що у Go поки відсутні дженерики, а CompareAndSwap є для цілочисельних типів, а для інших можна зробити через Mutex

Якщо почнете розробляти на Go то самостійно зможете знайти відповідь, підказка: A Tour of Go: The empty interface

Прочитав, все одно не розумію. Із empty interface ніяк не слідує ні заборона заміни типу, ні необхідність робити це тільки в рантаймі.

Тому що у Go поки відсутні дженерики

atomic.Value, який запамʼятовує тип першого значення, уже працює без усяких дженериків в мові, і рівно таким же чином міг би дозволяти і атомарне порівняння с заміною. Тому виглядає лише як відмазка. Пошукайте іншу відповідь, будь ласка :)

Із заборони заміни типу слідує заборона на empty interface, бо прикладу замінюєш тип nil на string

Стосовно atomic.Value і CompareAndSwap: операції Load і Store відбуваються без блокування і тому писання і читання відбувається швидко, CompareAndSwap буде потребувати Mutex і зробить операції Load і Store блокуючими

Стосовно дженериків і CompareAndSwap, дві структури треба порівняти, це порівняння треба контролювати бо є приватні поля а також типи slice.
ВІдповідно CompareAndSwap накладає на тип додаткове зобов’язання мати метод для порівняння, який можна зробити через інтерфейс або підняти до потрібного інтерфейсу, або через дженерик

Із заборони заміни типу слідує заборона на empty interface, бо прикладу замінюєш тип nil на string

А, тобто в рядянській Росії в Go віз везе коня. Зрозуміло, дякую.
Так чому зробили заборону заміни типу значення, якщо значення може бути довільним?

Стосовно дженериків і CompareAndSwap, дві структури треба порівняти, це порівняння треба контролювати бо є приватні поля а також типи slice.

Якщо atomic.Value може приймати довільне значення, це значить, що в неї кладеться тільки вказівник, реальне значення зберігається десь у іншому місці. Інакше памʼять би розширялась на довільну кількість байтів не змінюючи адреси:)
А значить, можна і порівняння зробити на вказівниках, а не на реальних значеннях. І тоді далі: чому таки дають CAS на unsafe вказівниках, але не на посиланнях безпечного рантайму?

операції Load і Store відбуваються без блокування і тому писання і читання відбувається швидко, CompareAndSwap буде потребувати Mutex і зробить операції Load і Store блокуючими

Це якщо вважати порівняння за повним значенням обʼєкта за посиланням. Але см. вище.

Стосовно дженериків і CompareAndSwap, дві структури треба порівняти, це порівняння треба контролювати бо є приватні поля а також типи slice.

До чого тут дженерики? Процесор дає можливість CAS на 1 чи 2 слова довжиною в адресу. Ніяки дженерики не врятують у випадку структури на 1000 байт, але ніякі дженерики не перешкоджають порівнянню посилань розміром у одну адресу.

Про «помилки на arm32» ви чомусь умовчали. Це починає бути підозрілим.

Про «помилки на arm32» ви чомусь умовчали. Це починає бути підозрілим.
Вам доступна стаття, можете перевірити що і як працює, а потім вказати на помилки якщо знайдете.

Ось відповів ⬆

Так чому зробили заборону заміни типу значення, якщо значення може бути довільним?

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

CompareAndSwap можна зробити через Mutex, це буде зрозуміліше ніж ускладнювати atomic.Value який відмінно працює і якого достатньо для рішення задач

Ось відповів

Це не відповідь. Вкажіть хоча б звідки ви взяли це твердження (не самі ж випробували?) Я стверждую, що про «помилки на arm32» була неправда (або ж автори Go не вміють зовсім нічого писати — але це уже точно за межами достовірного).

Бо приведення типів це одне з джерел помилок,

І саме тому кожний Load супроводжується конверсією типу? Дууже логічно.

а спроба зберігати різні типи це вже помилка в побудові програми.

Ні, це як раз може бути повністю нормальним (і якщо це було б завжди помилкою, в мову не вводили б конверсію типу, чи не так?)

CompareAndSwap можна зробити через Mutex, це буде зрозуміліше ніж ускладнювати atomic.Value який відмінно працює і якого достатньо для рішення задач

За такою логікою і усякі CompareAndSwapInt32, AddInt32 не треба було робити — Mutex завжди зрозуміліший. Не збігається щось у вас.

І ще питання: чому в такому коді:

package main

import "fmt"
import "sync/atomic"

func main() {
        fmt.Println("Hello?")
        var x1 atomic.Value
        var x1s int = 10 // <--
        x1.Store(x1s)
        fmt.Printf("After store 1: %s\n", x1.Load())
        x1.Store(3000000000)
        fmt.Println(x1)
        fmt.Println(x1.Load())
        x1.Store(3000000000000)
        fmt.Println(x1)
        fmt.Println(x1.Load())
}

якщо по стрілці змінити int на int64 — програма падає в паніку?

go is assign thread-safe:

package main

import "fmt"
import "sync/atomic"

func main() {
	fmt.Println("Hello?")
	var x1 atomic.Value
	var x1s int64 = 10 // <--
	x1.Store(x1s)
	fmt.Printf("After store 1: %+v\n", x1.Load())
	// panic: store of inconsistently typed value
	// x1.Store(3000000000)
	// x1.Store(int(3000000000))
	x1.Store(int64(3000000000))
	fmt.Println(x1)
	fmt.Println(x1.Load())
	// panic: store of inconsistently typed value
	// x1.Store(3000000000000)
	// x1.Store(int(3000000000000))
	x1.Store(int64(3000000000000))
	fmt.Println(x1)
	fmt.Println(x1.Load())
}

Ви навіть не спробували зрозуміти мої запитання.

P.S. З документу golang.org/ref/mem

> Don’t be clever.

Буду згадувати це в дискусіях :)

Зрозуміти тонкі деталі реалізації, щоб 1) знати їх, якщо доведеться таки на цій мові писати, 2) піймати якусь важливу думку навіть для своїх звичайних засобів. Тут я бачив такі можливості у сферах 1) типизації, 2) атомарности.

Але я поки що бачу, що ви навіть в основах не розбираєтесь... тому цей тред (і труд) безперспективний.

Дякую за критику.

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

Привет! Спасибо за труд. Так же интересно узнать, как свичнутся из Php дева. Возможно, какой-то роадмап. Верю, все же не я один такой. Заранее спасибо!

Перше що робив на Go це API для Android застосунку, ще до того як почав працювати Junior Go-розробником.

Є стаття Як здобути знання Junior Go і там рекомендують також робити власні проекти, хай навіть щось просте і давно відоме ~ URL shortening service

Питання «Як перекваліфікуватись з PHP на Go» вже як пару років час від часу з’являється в обговореннях на DOU, але зазвичай у відповідях посилання на книжки, відеоуроки та документацію, цього достатньо для перекваліфікації, але є ще аудиторія яка бачила Go і відклала на потім із-за нових конструкцій, які пояснюються десь на 50-100 сторінці книжки з Go і для таких варто написати про особливості Go, пояснити ці нові конструкції, і описати сильні сторони.

Толковый материал, спасибо !

Стаття про повну заміну даних і на це питання вже відповідав раніше, але давай спробуємо ще раз:

1. В документації sync.Map:

The Map type is specialized.
Most code should use a plain Go map

2. В проекті sync.Map зустрічається тільки в одній з vendor бібліотек
3. Спробуй замінити в SafeCounter map на sync.Map

sync.Map додає свої обмеження і відповідно простіше завжди використовувати звичайний map ніж думати: «А чи це та сама ситуація коли я можу використати sync.Map?»

В стандартних бібліотеках Go sync.Map використовується, ось один з прикладів: encoding/json:

var fieldCache sync.Map // map[reflect.Type]structFields

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) structFields {
	if f, ok := fieldCache.Load(t); ok {
		return f.(structFields)
	}
	f, _ := fieldCache.LoadOrStore(t, typeFields(t))
	return f.(structFields)
}

Якщо моєї відповіді буде мало то поділись досвідом і розкажи в яких ситуаціях використовуєш sync.Map і чому

Якась різка реакція, флешбекі з мапамі?
Не пам’ятаю, коли ми стали друзяками, так що «ти» можна лишити собі, біля дзеркала використовувати.

По топіку:
Я не бачу проблеми з використанням sync.Map. Якщо, звичайно, не треба супер-перфоманс в цьому місці.
Зручно використовувати як поле структури, якому не потрібен ініт. І не потрібно перевіряти, чи не нілом є мапа.

GopherCon 2017 An overview of sync.Map:

If cache contention is not the problem,
sync.Map is probably not the solution.

Тут важливо дивитися на «вірогідно». Звичайно, якщо вам не дозволяє релігія, то вам не можна використовувати sync.Map де завгодно.

Але Golang — це про простоту та зручність, тому якщо вам треба сінк мап — то можна використовувати (враховуючи застереження). Догматизм — шлях в нікуди.

Відео GopherCon 2017 An overview of sync.Map містить пояснення де саме варто використовувати sync.Map, з цього пояснення стає зрозуміло чому використовують sync.Map в пакеті encoding/json.

Цитата «If cache contention is not the problem, sync.Map is probably not the solution.» також взята з відео

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