Визуальное объяснение Go Concurrency – sync.RWMutex

Я опубликовал блог, раскрывающий sync.Mutex. В продолжение сегодняшнего дня мы рассмотрим его родного брата: sync.RWMutex.



Сценарий

Представьте, что у нас есть 4 суслика и 1 телевизор. Каждый может смотреть телевизор вместе, но если кто-то хочет переключиться на другой канал, ему/ей должен принадлежать только телевизор. Это означает, что никто больше не пытается переключить канал или смотреть текущую передачу по телевизору.

Сначала телевизор выключен. Приходит Партье и включает его. Поскольку он вносит изменения в состояние телевизора, ему необходим эксклюзивный доступ к телевизору. Поскольку он в настоящее время доступен (разблокирован), он может сразу же стать его владельцем.

Пока Партье держит телевизор, Кандье хочет переключиться на другое шоу. Она просит эксклюзивный доступ, позвонив tv.Lock(). Однако ей придется подождать, пока Партье выпустит его.

Приходит Стрингер. Он просто хочет посмотреть текущее шоу и звонит tv.RLock(). Поскольку Кендьер все еще держит телевизор, Стрингеру придется подождать. Мгновение спустя приходит Пловец. Он также хочет посмотреть текущее шоу, не переключая канал. Следовательно, он присоединяется к очереди вместе со Стрингером.

Кандье отпускает телевизор. Стрингеру и Пловцу разрешено смотреть телевизор одновременно. Партье возвращается, на этот раз он просто хочет посмотреть телевизор. Ему разрешено войти вместе со Стрингером и Пловцом.

Пловец говорит: Новости — это скучно. Он хочет перейти на другой канал. RLock, который он сейчас держит, не позволяет ему вносить какие-либо изменения. Чтобы внести изменения, ему нужно сначала tv.RUnlock(), затем tv.Lock(). Но Пловец и Партье все еще смотрят, телевизор остается в состоянии RLocked, заставляя Пловца ждать.

Партье уходит, телевизор все еще находится в состоянии RLocked, поскольку Стрингер все еще там. Кандиер возвращается. На этот раз она хочет только посмотреть телевизор. Несмотря на то, что телевизор находится в состоянии RLocked, Кандиеру придется встать в очередь. Это отличается от ситуации, когда Партье хочет посмотреть текущее шоу со Стрингером и Пловцом минуту назад, потому что Свиммер пытается внести изменения в телевизор до возвращения Кандье.

Стрингер отпускает телевизор. Поскольку никто его не смотрит, телевизор переходит в состояние «Разблокировано», что позволяет Пловцу заблокировать его при смене канала.

После смены канала Пловец отпускает телевизор, позволяя Кандье смотреть телевизор с помощью RLock. Как видите, Кандье хотел посмотреть телевизор, когда шоу было «Новости», но теперь идет шоу «Спорт». Она даже не знает (и не заботится) об этом изменении. Некоторое время она смотрит спортивное шоу, а затем отключает телевизор. После того, как куча людей приходит и уходит, окончательное состояние телевизора — «Разблокировано» и показывает «Спорт».

Покажи мне свой код!

Использовать sync.RWMutex довольно просто. По соглашению мы обычно помещаем sync.RWMutex и защищаемую им переменную рядом друг с другом или внутри одной структуры.

package main

import (
  "log"
  "sync"
  "time"
)

type TV struct {
  mu   sync.RWMutex
  show string
}

func main() {
  tv := &TV{show: "Off"}
  go changeTVChannel("Partier", "Drama", tv)
  go changeTVChannel("Candier", "News", tv)
  go watchTV("Stringer", tv)
  go watchTV("Swimmer", tv)
  go watchTV("Partier", tv)
  go changeTVChannel("Swimmer", "Sport", tv)
  go watchTV("Candier", tv)

  time.Sleep(5 * time.Second) // For brevity, better use sync.WaitGroup
  log.Println("Main: Done, shutting down")
}

func watchTV(name string, tv *TV) {
  log.Printf("%v: I want to watch the TV\n", name)
  tv.mu.RLock()
  log.Printf("%v: I'm watching the TV, show: %v\n", name, tv.show)
  time.Sleep(500 * time.Millisecond)
  log.Printf("%v: I don't want to watch any more\n", name)
  tv.mu.RUnlock()
}

func changeTVChannel(name string, show string, tv *TV) {
  log.Printf("%v: I want to change the channel to %v\n", name, show)
  tv.mu.Lock()
  lastShow := tv.show
  tv.show = show
  time.Sleep(200 * time.Millisecond)
  log.Printf("%v: Changed channel from %v to %v\n", name, lastShow, show)
  tv.mu.Unlock()
}

РЛоккер()

В sync.RWMutex есть 4 основных метода:

  • Lock() и Unlock() для написания эксклюзива
  • RLock() и RUnlock() для чтения

Названия методов Lock()/Unlock() могут показаться вам немного запутанными. Разве WLock()/WUnlock() не намного яснее? Ответ прост, аналогично sync.Mutex, sync.RWMutex также реализует sync.Locker, и эти 2 метода определены в таком интерфейсе. Мы даже можем заменить sync.Mutex на sync.RWMutex во всех случаях использования. Поскольку их реализации различны, производительность может различаться, но функционально они ведут себя одинаково.

type MyStruct struct {
  mu sync.Locker
  val int
}

// This works
myStruct := &MyStruct{mu: sync.Mutext{}}

// This also works and behaves the same
// Though, doing so is questionable
myStruct := &MyStruct{mu: sync.RWMutext{}}

sync.RWMutex также включает в себя метод RLocker(). Этот метод полезен, когда мы хотим гарантировать, что горутины чтения НИКОГДА не будут иметь шанса ошибочно получить блокировку записи.

// RLocker returns a Locker interface that implements
// the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker {
  return (*rlocker)(rw)
}

type rlocker RWMutex

func (r *rlocker) Lock()   { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }

Если эта статья оказалась для вас полезной, пожалуйста, мотивируйте меня одним хлопком. Вы также можете ознакомиться с другими моими статьями по адресу https://medium.com/@briannqc и связаться со мной в LinkedIn. Большое спасибо за чтение!

Стеккадемический

Спасибо, что дочитали до конца. Прежде чем уйти:

  • Пожалуйста, рассмотрите возможность аплодировать и следовать автору! 👏
  • Подпишитесь на нас в Twitter(X), LinkedIn и YouTube.
  • Посетите Stackademic.com, чтобы узнать больше о том, как мы демократизируем бесплатное образование в области программирования во всем мире.