diff --git a/lectures/05-concurrency/lectures.slide b/lectures/05-concurrency/lectures.slide new file mode 100644 index 0000000..4e5d3c2 --- /dev/null +++ b/lectures/05-concurrency/lectures.slide @@ -0,0 +1,298 @@ +Concurrency with Shared Memory +Лекция 5 + +Фёдор Короткий + +* Happens Before + +- Одно событие в программе может _произойти_раньше_ (_happens_before_) другого события. + +- Внутри одной горутины `a`:=`1` _happens_before_ `a`+=`1`. + + a := 1 + a += 1 + +- Посылка значения из канал _happens_before_ получения этого значения из канала. + +- Два события A и B _происходят_одновременно_ (_are_concurrent_), если нельзя сказать что одно случилось раньше другого. + +* Race Condition + +- _Race_Condition_ - ситуация, когда код работает некорректно в зависимости от порядка выполнения нескольких горутин. + +* Example Bank + + // Package bank implements a bank with only one account. + package bank + + var balance int + + func Deposit(amount int) { balance = balance + amount } + + func Balance() int { return balance } + +Работает корректно в одной горутине. + + // Alice: + go func() { + bank.Deposit(200) + fmt.Println("=", bank.Balance()) + }() + + // Bob + go bank.Deposit(100) + +Ошибка в этом примере называется _data_race_. + +* Data Race breaks memory safety + + var x []int + go func() { x = make([]int, 10) }() + go func() { x = make([]int, 1000000) }() + x[999999] = 1 // NOTE: undefined behavior; memory corruption possible! + +Программа с _data_race_ перестаёт вести себя так, как написано в коде. + +Иногда люди говорят _"это_безопасный_рейс"_. *Не*бывыет*безопасных*data-race-ов* + +* Data Race + +*Data*race* случается, когда несколько горутин одновременно работают с переменной, и хотябы одна из них пишет в эту переменную. + +Как защититься от _data_race_? + +1. Не писать в переменную. +2. Не работать с переменной из нескольких горутин. +3. Не работать с переменной одновременно. (mutex) + +* Не писать в переменую + + var icons = make(map[string]image.Image) + func loadIcon(name string) image.Image + + // NOTE: not concurrency-safe! + func Icon(name string) image.Image { + icon, ok := icons[name] + if !ok { + icon = loadIcon(name) + icons[name] = icon + } + return icon + } + +Правильно: + + var icons = map[string]image.Image{ + "spades.png": loadIcon("spades.png"), + "hearts.png": loadIcon("hearts.png"), + "diamonds.png": loadIcon("diamonds.png"), + "clubs.png": loadIcon("clubs.png"), + } + + func Icon(name string) image.Image { return icons[name] } + +* Не работать с переменной из нескольких горутин + + // Package bank provides a concurrency-safe bank with one account. + package bank + + var deposits = make(chan int) // send amount to deposit + var balances = make(chan int) // receive balance + + func Deposit(amount int) { deposits <- amount } + func Balance() int { return <-balances } + + func teller() { + var balance int // balance is confined to teller goroutine + for { + select { + case amount := <-deposits: + balance += amount + case balances <- balance: + } + } + } + + func init() { + go teller() // start the monitor goroutine + } + +* sync.Mutex + + import "sync" + + var ( + mu sync.Mutex // guards balance + balance int + ) + + func Deposit(amount int) { + mu.Lock() + balance = balance + amount + mu.Unlock() + } + + func Balance() int { + mu.Lock() + defer mu.Unlock() + b := balance + return b + } + +* sync.RWMutex + + var mu sync.RWMutex + var balance int + + func Balance() int { + mu.RLock() // readers lock + defer mu.RUnlock() + return balance + } + +* Memory Order + + var x, y int + go func() { + x = 1 // A1 + fmt.Print("y:", y, " ") // A2 + }() + go func() { + y = 1 // B1 + fmt.Print("x:", x, " ") // B2 + }() + +Можем ожидать + + y:0 x:1 + x:0 y:1 + x:1 y:1 + y:1 x:1 + +Но реально может произойти + + x:0 y:0 + y:0 x:0 + +* Ленивая инициализация sync.Once. + + var icons map[string]image.Image + + func loadIcons() { + icons = map[string]image.Image{ + "spades.png": loadIcon("spades.png") + } + } + + // NOTE: not concurrency-safe! + func Icon(name string) image.Image { + if icons == nil { + loadIcons() // one-time initialization + } + return icons[name] + } + +* Ленивая инициализация sync.Once. + + var mu sync.Mutex // guards icons + var icons map[string]image.Image + + // Concurrency-safe. + func Icon(name string) image.Image { + mu.Lock() + defer mu.Unlock() + if icons == nil { + loadIcons() + } + return icons[name] + } + +* Ленивая инициализация sync.Once. + + var loadIconsOnce sync.Once + var icons map[string]image.Image + + // Concurrency-safe. + func Icon(name string) image.Image { + loadIconsOnce.Do(loadIcons) + return icons[name] + } + +* Concurrent cache + +.play memo1/memo.go /type Memo/,/OMIT/ + +* Concurrent cache + +.play memo2/memo.go /type Memo/,/OMIT/ + +* Concurrent cache + +.play memo3/memo.go /func \(memo/,/OMIT/ + +* Concurrent cache + +.play memo4/memo.go /type entry/,/OMIT/ + +* sync.Map + +Что происходит, когда go совершает type cast из interface{}? + +- Приведение к конкретному типу требует одной проверки на `==` + + var v interface{} + x, ok := v.(int) + +- Приведение к интерфейсу требует (в общем случае) сравнивать method set. + + var v interface{} + x, ok := v.(io.Reader) + + // Конкретный тип v должен иметь метод Read([]byte) (int, error) + +- Для каждой пары `(concrete`type,`interface`type)` нужно знать, корректно ли такое приведение. + + var cache map[typeConversion]conversionResult + +* sync.Map + + var cache map[typeConversion]conversionResult + +Как защитить cache? `sync.Mutex`? Кеш из раннего примера? + +Какой паттерн нагрузки? + +- Запись в начале программы. +- Потом только чтения. +- Никогда не удаляем ключи. +- Не страшно сделать вычисление несколько раз. + +Чего хотим? + +- Скорость как у `map` без лока. + +* sync.Map + + var cache sync.Map + + func convertType(from, to typ) *conversionResult { + key := typeConversion{from: from, to: to} + res, ok := cache.Load(key) + if ok { + return res.(*conversionResult) + } + + res = doConversion(from, to) + cache.Store(key, res) + return res.(*conversionResult) + } + +- `sync.Map` хранит две `map` внутри. `clean` и `dirty`. +- Обращение к `clean` всегда происходит без лока. +- Обращение к `dirty` требует лока. +- Периодически `dirty` повышается до `clean`. + +* Как сделать sync.Map без двойного вычисления? + +.play synconce/map.go /type result/,/OMIT/ + diff --git a/lectures/05-concurrency/memo1/memo.go b/lectures/05-concurrency/memo1/memo.go new file mode 100644 index 0000000..bda41fd --- /dev/null +++ b/lectures/05-concurrency/memo1/memo.go @@ -0,0 +1,36 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 272. + +//!+ + +// Package memo provides a concurrency-unsafe +// memoization of a function of type Func. +package memo + +// A Memo caches the results of calling a Func. +type Memo struct { + f Func + cache map[string]result +} + +// Func is the type of the function to memoize. +type Func func(key string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +// NOTE: not concurrency-safe! +func (memo *Memo) Get(key string) (interface{}, error) { + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + return res.value, res.err +} + +// OMIT diff --git a/lectures/05-concurrency/memo1/memo_test.go b/lectures/05-concurrency/memo1/memo_test.go new file mode 100644 index 0000000..1b1c9af --- /dev/null +++ b/lectures/05-concurrency/memo1/memo_test.go @@ -0,0 +1,67 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo1" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +// NOTE: not concurrency-safe! Test fails. +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} + +/* +//!+output +$ go test -v gopl.io/ch9/memo1 +=== RUN Test +https://golang.org, 175.026418ms, 7537 bytes +https://godoc.org, 172.686825ms, 6878 bytes +https://play.golang.org, 115.762377ms, 5767 bytes +http://gopl.io, 749.887242ms, 2856 bytes + +https://golang.org, 721ns, 7537 bytes +https://godoc.org, 152ns, 6878 bytes +https://play.golang.org, 205ns, 5767 bytes +http://gopl.io, 326ns, 2856 bytes +--- PASS: Test (1.21s) +PASS +ok gopl.io/ch9/memo1 1.257s +//!-output +*/ + +/* +//!+race +$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1 +=== RUN TestConcurrent +... +WARNING: DATA RACE +Write by goroutine 36: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 + ... + +Previous write by goroutine 35: + runtime.mapassign1() + ~/go/src/runtime/hashmap.go:411 +0x0 + gopl.io/ch9/memo1.(*Memo).Get() + ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 +... +Found 1 data race(s) +FAIL gopl.io/ch9/memo1 2.393s +//!-race +*/ diff --git a/lectures/05-concurrency/memo2/memo.go b/lectures/05-concurrency/memo2/memo.go new file mode 100644 index 0000000..8a43f52 --- /dev/null +++ b/lectures/05-concurrency/memo2/memo.go @@ -0,0 +1,44 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 275. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Concurrent requests are serialized by a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +//!+ + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +// Get is concurrency-safe. +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + memo.mu.Unlock() + return res.value, res.err +} + +// OMIT diff --git a/lectures/05-concurrency/memo2/memo_test.go b/lectures/05-concurrency/memo2/memo_test.go new file mode 100644 index 0000000..bd4dc7b --- /dev/null +++ b/lectures/05-concurrency/memo2/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo2" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/lectures/05-concurrency/memo3/memo.go b/lectures/05-concurrency/memo3/memo.go new file mode 100644 index 0000000..a4a4450 --- /dev/null +++ b/lectures/05-concurrency/memo3/memo.go @@ -0,0 +1,46 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// type Func. Requests for different keys run concurrently. +// Concurrent requests for the same key result in duplicate work. +package memo + +import "sync" + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]result +} + +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]result)} +} + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + res, ok := memo.cache[key] + memo.mu.Unlock() + if !ok { + res.value, res.err = memo.f(key) + + // Between the two critical sections, several goroutines + // may race to compute f(key) and update the map. + memo.mu.Lock() + memo.cache[key] = res + memo.mu.Unlock() + } + return res.value, res.err +} + +// OMIT diff --git a/lectures/05-concurrency/memo3/memo_test.go b/lectures/05-concurrency/memo3/memo_test.go new file mode 100644 index 0000000..19fc5f0 --- /dev/null +++ b/lectures/05-concurrency/memo3/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo3" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/lectures/05-concurrency/memo4/memo.go b/lectures/05-concurrency/memo4/memo.go new file mode 100644 index 0000000..a01bc89 --- /dev/null +++ b/lectures/05-concurrency/memo4/memo.go @@ -0,0 +1,59 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +// See page 276. + +// Package memo provides a concurrency-safe memoization a function of +// a function. Requests for different keys proceed in parallel. +// Concurrent requests for the same key block until the first completes. +// This implementation uses a Mutex. +package memo + +import "sync" + +// Func is the type of the function to memoize. +type Func func(string) (interface{}, error) + +type result struct { + value interface{} + err error +} + +//!+ + +func New(f Func) *Memo { + return &Memo{f: f, cache: make(map[string]*entry)} +} + +type Memo struct { + f Func + mu sync.Mutex // guards cache + cache map[string]*entry +} + +type entry struct { + res result + ready chan struct{} // closed when res is ready +} + +func (memo *Memo) Get(key string) (value interface{}, err error) { + memo.mu.Lock() + e := memo.cache[key] + if e == nil { + // This is the first request for this key. + // This goroutine becomes responsible for computing + // the value and broadcasting the ready condition. + e = &entry{ready: make(chan struct{})} + memo.cache[key] = e + memo.mu.Unlock() + e.res.value, e.res.err = memo.f(key) + close(e.ready) // broadcast ready condition + } else { + // This is a repeat request for this key. + memo.mu.Unlock() + <-e.ready // wait for ready condition + } + return e.res.value, e.res.err +} + +// OMIT diff --git a/lectures/05-concurrency/memo4/memo_test.go b/lectures/05-concurrency/memo4/memo_test.go new file mode 100644 index 0000000..55cdc12 --- /dev/null +++ b/lectures/05-concurrency/memo4/memo_test.go @@ -0,0 +1,23 @@ +// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ + +package memo_test + +import ( + "testing" + + "gopl.io/ch9/memo4" + "gopl.io/ch9/memotest" +) + +var httpGetBody = memotest.HTTPGetBody + +func Test(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Sequential(t, m) +} + +func TestConcurrent(t *testing.T) { + m := memo.New(httpGetBody) + memotest.Concurrent(t, m) +} diff --git a/lectures/05-concurrency/synconce/map.go b/lectures/05-concurrency/synconce/map.go new file mode 100644 index 0000000..afb2fc4 --- /dev/null +++ b/lectures/05-concurrency/synconce/map.go @@ -0,0 +1,31 @@ +package synconce + +import "sync" + +var cache sync.Map + +type result struct{} + +func do(key string) *result { return new(result) } + +type entry struct { + res *result + sync.Once +} + +func get(key string) *result { + myEntry := &entry{} + + old, loaded := cache.LoadOrStore(key, myEntry) + if loaded { + myEntry = old.(*entry) + } + + myEntry.Do(func() { + myEntry.res = do(key) + }) + + return myEntry.res +} + +// OMIT