Одной из самых крутых особенностей Go являются горутины, которые позволяют вам очень легко делать что-то одновременно. Однако одна вещь, которая чрезвычайно важна для горутин, которую можно упустить из виду, - это мьютексы. Например, когда более одной горутины пытаются увеличить глобальную переменную, мы можем столкнуться со сценарием, в котором счетчик не точен. Давайте посмотрим на пример:
var count int func updateCount(done chan<- bool) { count++ done <- true } func main() { done := make(chan bool) for i := 0; i < 1000; i++ { go updateCount(done) } for i := 0; i < 1000; i++ { <-done } fmt.Println(count) }
Теперь, несмотря на то, что все горутины завершили инкрементирование, когда мы печатаем count, мы можем увидеть что-то вроде 990, 986, 987 и т. д. и т. д., даже если мы увеличили переменную в 1000 раз, начиная с 0. Так как же нам это исправить? тогда? Мы должны использовать мьютексы. Мьютексы позволяют нашим параллельным программам получать доступ к переменной, но не одновременно. Им приходится ждать своей очереди, теперь вы можете подумать, что это глупо, в конце концов, почему бы просто не использовать обычные функции вместо горутин? Ну, в других, более реалистичных примерах, мы по-прежнему будем делать другие вещи одновременно, например, у нас может быть куча горутин, делающих http-запросы и добавляющих ответы в массив. Вот обновленная версия нашей программы с использованием мьютексов:
var count int var mu sync.Mutex func updateCount(done chan<- bool) { mu.Lock() count++ mu.Unlock() done <- true } func main() { done := make(chan bool) for i := 0; i < 1000; i++ { go updateCount(done) } for i := 0; i < 1000; i++ { <-done } fmt.Println(count) }
В этом примере счетчик всегда будет 1000 при печати, потому что мы использовали мьютексы, чтобы убедиться, что он обновляется правильно. Давайте возьмем предыдущий пример http-запросов и используем мьютексы для сбора множества ответов с разных сайтов:
var responses []*http.Response var mu sync.Mutex func fetch(url string, done chan<- bool) { response, err := http.Get(url) if err != nil { fmt.Printf("error: making request to site '%s': %v\n", url, err) } else { mu.Lock() responses = append(responses, response) mu.Unlock() } done <- true } func main() { sites := []string{ /* sites */ } done := make(chan bool) for _, v := range sites { go fetch(v, done) } for i := 0; i < len(sites); i++ { <-done } var getTitle func(n *html.Node) getTitle = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "title" { fmt.Println(n.FirstChild.Data) } for c := n.FirstChild; c != nil; c = c.NextSibling { getTitle(c) } } for _, v := range responses { doc, err := html.Parse(v.Body) if err != nil { fmt.Printf("error: parsing website body: %v\n", err) continue } fmt.Printf("%s - ", v.Request.URL) getTitle(doc) } }
Самая важная часть в приведенном выше коде — это функция выборки. Как видите, он блокирует мьютекс «mu» перед добавлением тела ответа в глобальную переменную ответов. В противном случае, если два запроса завершатся почти одновременно, они могут быть неправильно добавлены в наш массив ответов.
Подводя итог, мьютексы важны в параллельных программах, чтобы гарантировать, что переменная может быть доступна только по одной, а не одновременно, чтобы предотвратить состояние гонки при обновлении этой переменной, где она не обновляется правильно.