Irbisa · Шпаргалка

sync, атомики и модель памяти

Мьютексы, sync.Once, атомики, happens-before, детектор гонок

Middle-разработчик · 22 вопроса

  1. 01

    Какие примитивы синхронизации даёт пакет sync и для чего нужен каждый из них?

    В sync есть блокировки (Mutex, RWMutex), средства координации (WaitGroup, Once, Cond) и два специальных контейнера (Map, Pool); sync/atomic добавляет операции без блокировок над одиночными значениями.

  2. 02

    Что не так с этим кодом на WaitGroup и как wg.Go из Go 1.25 исключает эту ошибку?

    func fetchAll(ids []int) []Row {    var wg sync.WaitGroup    rows := make([]Row, len(ids))    for i, id := range ids {        go func() {            wg.Add(1)            defer wg.Done()            rows[i] = load(id)        }()    }    wg.Wait()    return rows}

    wg.Add(1) выполняется внутри горутины, поэтому может случиться уже после того, как wg.Wait() увидел нулевой счётчик. Тогда fetchAll возвращает rows, пока горутины ещё пишут в него: результаты теряются, и возникает data race, о которой сообщит -race.

  3. 03

    Почему defer mu.Unlock() сразу после mu.Lock() считается вариантом по умолчанию и когда вместо него стоит снимать блокировку явно?

    defer mu.Unlock() закрывает все выходы: ранний return, тот, который кто-то добавит через месяц, и панику посередине.

    // wrong: the cache-hit path returns with the mutex still heldfunc (c *Cache) Get(k string) ([]byte, error) {    c.mu.Lock()    if v, ok := c.m[k]; ok {        return v, nil      // never unlocked: every later Get blocks forever    }…
  4. 04

    Чем отличаются deadlock, livelock и starvation?

    При deadlock горутины ждут друг друга и ничего не движется; при livelock они продолжают работать, но постоянно сводят на нет прогресс друг друга; при starvation (голодании) система движется, но одна горутина редко или вообще никогда не получает ресурс.

  5. 05

    Incr вызывает Get, удерживая тот же мьютекс. Что произойдёт при выполнении и почему в Go нет реентерабельного мьютекса?

    type Cache struct {    mu sync.Mutex    m  map[string]int}func (c *Cache) Get(k string) int {    c.mu.Lock()    defer c.mu.Unlock()    return c.m[k]} func (c *Cache) Incr(k string) int {    c.mu.Lock()    defer c.mu.Unlock()    c.m[k]++    return c.Get(k)}

    Горутина заблокирует сама себя: sync.Mutex не реентерабелен, поэтому Lock внутри Get ждёт Unlock, который может выполниться только после него. Если больше ничего не работает, рантайм остановится с fatal error: all goroutines are asleep - deadlock!; в сервере просто повиснет каждый вызывающий.

  6. 06

    Найдите баг: после того как один клиент перестал читать, весь чат-хаб замирает, и новые Subscribe висят, как и любой другой Publish.

    type Hub struct {    mu   sync.Mutex    subs map[int]chan string} func (h *Hub) Subscribe(id int) <-chan string {    h.mu.Lock()    defer h.mu.Unlock()    ch := make(chan string)    h.subs[id] = ch    return ch} func (h *Hub) Publish(msg string) {    h.mu.Lock()    defer h.mu.Unlock()    for _, ch := range h.subs {        ch <- msg    }}

    Publish держит mu, пока делает блокирующую отправку в небуферизованный канал, поэтому один подписчик, который не читает, держит мьютекс вечно.

  7. 07

    Как сделать потокобезопасный ленивый синглтон в Go и что гарантирует sync.Once такого, чего не даёт проверка if instance == nil?

    Используйте sync.Once или, начиная с Go 1.21, sync.OnceValue: функция инициализации выполняется ровно один раз для всех вызывающих, и каждый из них возвращается только после того, как она завершилась. Обычная проверка на nil не даёт ни одной из этих гарантий.

    // wrong: two goroutines can both see nil, and the read is a data racevar client *http.Client func Client() *http.Client {    if client == nil {        client = newClient()…
  8. 08

    Чем data race отличается от race condition и почему -race может быть зелёным, пока защищённый мьютексом баланс уходит в минус?

    Гонка данных (data race) возникает, когда две горутины одновременно обращаются к одной памяти, хотя бы одна пишет, и синхронизации нет; race condition означает логическую ошибку, при которой результат зависит от порядка выполнения.

    // wrong: -race is clean, every access is locked, and it is still brokenfunc (a *Account) Withdraw(n int64) error {    if a.Balance() < n {       // RLock ... RUnlock        return ErrInsufficient    }    a.add(-n)                  // Lock ... Unlock: a *different* critical section…
  9. 09

    Почему несинхронизированный флаг остановки типа bool считается багом, даже если «в худшем случае горутина прочитает устаревшее значение», а один байт не может порваться?

    Гонка данных (data race) не сводится к устаревшему значению: компилятор и процессор вправе выдать результат, который никакой последовательностью устаревших чтений не объяснить.

    // wrong: "it is just a bool"; there is no such thing as a benign racevar ready boolvar payload *Report func producer() { payload = build(); ready = true } // two plain stores, nothing orders them…
  10. 10

    Что даёт sync/atomic и когда стоит использовать атомик вместо Mutex?

    Атомик делает одно чтение, запись или read-modify-write одного значения неделимым и видимым другим горутинам, без блокировки.

    var hits atomic.Int64var ready atomic.Boolvar cfg atomic.Pointer[Config] hits.Add(1)                  // counter: read-modify-write in one stepready.Store(true)            // flag: visible to every later Load…
  11. 11

    Этот датчик должен хранить максимальное увиденное значение. Каждое обращение атомарно, -race чист, но максимум иногда оказывается занижен. Почему и как исправить?

    type Gauge struct{ max atomic.Int64 } func (g *Gauge) Observe(n int64) {    if g.max.Load() < n {        g.max.Store(n)    }}

    Атомарна каждая операция, но не пара. Read-modify-write должен быть одним шагом, а это CompareAndSwap в цикле повторов.

  12. 12

    Чем sync.Mutex отличается от sync.RWMutex и почему переход на RWMutex может замедлить map, которую в основном читают?

    Mutex пускает одну горутину за раз; RWMutex пускает много читателей одновременно, а писателям даёт эксклюзивный доступ.

  13. 13

    Как устроен sync.Mutex внутри и что такое его нормальный режим и режим голодания?

    Mutex состоит из двух полей: слова state (биты «захвачен», «разбужен», «голодание» и счётчик ожидающих) и семафора.

  14. 14

    Что такое sync.Map и когда он лучше или хуже обычной map под Mutex?

    sync.Map устроена как конкурентная map, настроенная под два сценария, которые называет её документация: ключи, записанные один раз и читаемые много раз, как кэш, который только растёт, и горутины, работающие с непересекающимися наборами ключей.

    // right use: written once, read forever, e.g. a registry of compiled codecsvar codecs sync.Map // string -> Codec func codecFor(name string) (Codec, error) {    if v, ok := codecs.Load(name); ok {        return v.(Codec), nil  // every read pays a type assertion…
  15. 15

    Как работает race detector и чего он не может найти?

    -race на этапе компиляции инструментирует каждое обращение к памяти, а во время работы отслеживает, какие обращения упорядочены блокировками, каналами и атомиками.

    // The detector sees only what runs, so make the test run the overlap.func TestLimiterConcurrent(t *testing.T) {    l := NewLimiter(100)    var wg sync.WaitGroup    for range 64 {                   // one goroutine would find nothing        wg.Go(func() {…
  16. 16

    Как работает sync.Cond и почему в большинстве Go-проектов вместо него используют канал?

    Cond позволяет горутинам спать, пока не изменится защищённое состояние: Wait атомарно отпускает L и паркует горутину, Signal будит одного ожидающего, Broadcast будит всех, а Wait перед возвратом снова берёт блокировку.

    // before: Cond cannot be selected on, so no timeout and no shutdown wake-upfunc (q *CondQueue) Pop() Job {    q.mu.Lock()    defer q.mu.Unlock()    for len(q.jobs) == 0 {   // `for`, never `if`: the predicate may be false        q.cond.Wait()        // again once Wait has re-taken the lock…
  17. 17

    Найдите баг: health checker, который перебирает Snapshot(), иногда падает с concurrent map iteration and map write.

    type Registry struct {    mu    sync.RWMutex    peers map[string]Peer} func (r *Registry) Add(p Peer) {    r.mu.Lock()    defer r.mu.Unlock()    r.peers[p.ID] = p} func (r *Registry) Snapshot() map[string]Peer {    r.mu.RLock()    defer r.mu.RUnlock()    return r.peers}

    Snapshot возвращает собственную map реестра, а не копию, поэтому блокировка на чтение защищает только оператор return. Вызывающий код перебирает map без всякой блокировки, пока Add пишет в неё, и рантайм падает с concurrent map iteration and map write.

  18. 18

    Как sync.Pool сокращает аллокации и когда он вредит, а не помогает?

    Pool держит на каждый процессор списки свободных объектов, которые вы вернули через Put, так что следующий Get переиспользует объект вместо аллокации; это снижает нагрузку на GC от короткоживущих объектов вроде буферов для кодирования.

    var bufs = sync.Pool{New: func() any { return new(bytes.Buffer) }} func render(w io.Writer, v any) error {    b := bufs.Get().(*bytes.Buffer)    b.Reset()                   // reset on the way IN: Get may return a used one…
  19. 19

    Что означает happens-before в модели памяти Go и какие операции его создают?

    Happens-before обозначает порядок, который гарантирует модель памяти: если запись w происходит до чтения r, то r увидит w или более позднюю запись.

    // Each pair is free of data races: the marked operation is the edge.var ready = make(chan struct{}) func publishByChannel() { cfg = load(); close(ready) } // close ...func readByChannel()    { <-ready; use(cfg) }          // ... before this receive…
  20. 20

    Почему double-checked locking на обычном указателе в Go сломан и как подменять на лету конфиг, который читает каждый запрос?

    **Внешняя проверка cfg == nil означает несинхронизированное чтение указателя, который пишет другая горутина, то есть data race: можно увидеть наполовину собранную структуру.

    // wrong: textbook double-checked locking, a data race in Govar cfg *Configvar mu sync.Mutex func Get() *Config {    if cfg == nil {              // unsynchronised read…
  21. 21

    Вызовы transfer(a, b) и transfer(b, a) виснут навсегда, а остальной сервер продолжает работать. В чём баг и как сделать его невозможным?

    Инверсия порядка блокировок: transfer(x, y) берёт x, затем y, а transfer(y, x) берёт y, затем x, и каждая ждёт блокировку, которую держит другая. Чинится одним фиксированным порядком, которому подчиняется каждый путь захвата.

    // wrong: two lock orders form a cycle; transfer(x, y) and transfer(y, x) hangfunc transfer(a, b *Account, n int64) {    a.mu.Lock()    defer a.mu.Unlock()    b.mu.Lock()    defer b.mu.Unlock()…
  22. 22

    Счётчики по шардам в []struct{ n atomic.Int64 } работают медленнее на операцию с ростом числа ядер, хотя две горутины никогда не трогают один элемент. Почему?

    Ложное разделение (false sharing): соседние счётчики лежат в одной кеш-линии, а когерентность кешей процессора работает по линиям, а не по переменным. Запись в один элемент инвалидирует всю линию в кешах всех остальных ядер, вместе с соседями.

    // wrong: 8 bytes each, so eight of these share one 64-byte cache linetype packed struct{ n atomic.Int64 } // right: one line each. 128, not 64: internal/cpu.CacheLinePadSize is 128// on arm64, and sync.Pool pads its per-P struct to 128 for the same reason.type padded struct {…