Interview
Concurrency
Какие есть проблемы конкурентности?
- Data Race
- Race Condition
Как возникает проблема Data Race?
Гонка данных (data race), которая происходит, когда две или более горутины одновременно обращаются к одной и той же ячейке памяти и по крайней мере одна из них выполняет запись в эту ячейку.
Пример Data Race?
package main
import (
"fmt"
"time"
)
func main() {
i := 0
go func() {
i++
}()
go func() {
i++
}()
time.After(1 * time.Second)
fmt.Println(i)
}
Как решить проблему Data Race?
Варианты предотвращения Data Race с помощью трех синхронизирующих подходов:
- использование атомарных операций (пакет
sync/atomic
) - защита критических секций с помощью мьютексов (
sync.Mutex
) - использование communication (связи) и channels (каналов) для обеспечения того, чтобы переменная обновлялась только одной горутиной.
Обязательно ли приложение, свободное от Data Race, означает детерминированный результат?
Не всегда. Может быть зависимость от порядка выполнения.
Как возникает проблема Race Condition?
Race condition (состояние гонки) возникает, когда поведение зависит от последовательности или времени выполнения событий, которые невозможно контролировать.
Пример Race Condition?
package main
import (
"fmt"
"sync"
"time"
)
func main() {
i := 0
mutex := sync.Mutex{}
go func() {
mutex.Lock()
defer mutex.Unlock()
i = 2
}()
go func() {
mutex.Lock()
defer mutex.Unlock()
i = 1
}()
time.After(1 * time.Second)
mutex.Lock()
fmt.Println(i)
mutex.Unlock()
}
Как решить проблему Race Condition?
Эту проблему можно решить с помощью каналов. Координация и оркестровка также могут гарантировать, что к определенному разделу будет обращаться только одна горутина.
Что такое Go memory model?
Go memory model (Модель памяти Go) — это спецификация, определяющая условия, при которых чтение из переменной в одной горутине может гарантированно произойти только после записи в ту же переменную другой горутиной. Другими словами, она предоставляет определенные гарантии, о которых разработчики должны помнить, чтобы избежать гонки данных и обеспечить детерминированный результат.
Какие гарантии дает Go memory model? Какие happens-before есть?
В рамках одной горутины нет возможности несинхронизированного доступа. То, что одно действие происходит до (happens-before) другого, гарантируется порядком, заданным программой.
При работе с несколькими горутинами помните о некоторых из этих гарантий. Рассмотрим эти гарантии (некоторые из них скопированы из модели памяти Go):
- Создание горутины happens-before выполнения этой горутины. Следовательно, чтение переменной, а затем запуск новой горутины, которая производит запись в эту переменную, не приводит к гонке данных.
- Выход из горутины не обязательно happens-before наступления какого-либо события.
- Отправка по каналу happens-before завершения соответствующего приема из этого канала. С помощью транзитивности мы можем обеспечить то, что доступ к переменным будет синхронизирован и, следовательно, не будет гонки данных.
- Закрытие канала happens-before получения замыкания.
- Прием из небуферизованного канала happens-before завершения отправки по этому каналу.
От чего зависит количество goroutines в контексте конкурентных приложений?
От типа рабочей нагрузки.
Какие бывают типы рабочей нагрузки?
- CPU-bound
- I/O-bound
- memory-bound
Какие особенности работы при CPU-bound?
Если рабочая нагрузка — типа CPU-bound, то рекомендуется полагаться на GOMAXPROCS
— переменную,
которая устанавливает количество потоков ОС, выделенных для выполнения горутин.
По умолчанию это значение равно количеству логических процессоров.
Какие особенности работы при I/O-bound?
Если рабочая нагрузка — типа I/O-bound, то ответ зависит от внешней системы. С каким количеством конкурентных обращений сможет справиться система, если мы хотим максимизировать ее пропускную способность?
Worker-pooling pattern?
package main
import (
"io"
"sync"
"sync/atomic"
)
func read(r io.Reader) (int, error) {
var count int64
wg := sync.WaitGroup{}
var n = 10
ch := make(chan []byte, n)
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
for b := range ch {
v := task(b)
atomic.AddInt64(&count, int64(v))
}
}()
}
for {
b := make([]byte, 1024)
ch <- b
}
close(ch)
wg.Wait()
return int(count), nil
}
func task(b []byte) int {
return len(b)
}