← Назад на главную

Блокировки с использованием мьютексов в Go

Когда несколько горутин работают с общими данными, которые могут изменяться, возникает риск возникновения состояния гонки. В состоянии гонки несколько горутин как бы соревнуются за доступ к информации. Проблемы возникают, когда они одновременно пытаются изменить эти данные. Одна горутина может лишь частично изменить значение, прежде чем другая попытается его использовать, что приводит к непредсказуемым результатам.

Один из наиболее простых способов предотвратить состояние гонки – предоставить каждой горутине возможность заблокировать ресурс перед его использованием и разблокировать после завершения операций. Все остальные горутины, столкнувшиеся с заблокированным ресурсом, будут вынуждены ждать, пока блокировка не будет снята, прежде чем они смогут сами получить доступ к ресурсу. Для блокировки и разблокировки используется функция sync.Mutex.

Встроенный пакет sync определяет интерфейс sync.Locker, а также несколько реализаций блокировок. Это предоставляет все необходимые инструменты для работы с блокировками.

Пример программы с состоянием гонки

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

package main

import (
"bufio"
"fmt"
"os"
"strings"
"sync"
)

func main() {
var wg sync.WaitGroup
w := newWords()
for _, f := range os.Args[1:] {
wg.Add(1)
go func(file string) {
if err := tallyWords(file, w); err != nil {
fmt.Println(err.Error())
}
wg.Done()
}(f)
}
wg.Wait()
fmt.Println("Слова, которые встречаются более одного раза:")
for word, count := range w.found {
if count > 1 {
fmt.Printf("%s: %d\n", word, count)
}
}
}

type words struct {
found map[string]int
}

func newWords() *words {
return &words{found: map[string]int{}}
}

func (w *words) add(word string, n int) {
count, ok := w.found[word]
if !ok {
w.found[word] = n
return
}
w.found[word] = count + n
}

func tallyWords(filename string, dict *words) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
word := strings.ToLower(scanner.Text())
dict.add(word, 1)
}

return scanner.Err()
}
При запуске программы с одним файлом получаем результат:

Изображение

Однако при передаче нескольких файлов, программа может завершиться с ошибкой:

Изображение

Проблема заключается в функции words.add. Несколько горутин одновременно обращаются к одному и тому же участку памяти, а именно к w.found типа map. Это приводит к состоянию гонки и повреждению данных.

Go предоставляет встроенные средства обнаружения состояния гонки

Инструменты Go, такие как go run и go test, при запуске с флагом --race, включают механизм обнаружения состояния гонки. Этот механизм замедляет выполнение, но может быть полезен для выявления состояний гонки в процессе разработки.

Использование блокировок

Единственным простым решением является установка блокировки перед изменением данных и снятие блокировки после изменения. Для этого необходимо внести небольшие изменения в код:

package main

import (
"bufio"
"fmt"
"os"
"strings"
"sync"
)

func main() {
var wg sync.WaitGroup
w := newWords()
for _, f := range os.Args[1:] {
wg.Add(1)
go func(file string) {
if err := tallyWords(file, w); err != nil {
fmt.Println(err.Error())
}
wg.Done()
}(f)
}
wg.Wait()
fmt.Println("Слова, которые встречаются более одного раза:")
w.Lock()
for word, count := range w.found {
if count > 1 {
fmt.Printf("%s: %d\n", word, count)
}
}
w.Unlock()
}

type words struct {
sync.Mutex
found map[string]int
}

func newWords() *words {
return &words{found: map[string]int{}}
}

func (w *words) add(word string, n int) {
w.Lock()
defer w.Unlock()

count, ok := w.found[word]
if !ok {
w.found[word] = n
return
}
w.found[word] = count + n
}

func tallyWords(filename string, dict *words) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
word := strings.ToLower(scanner.Text())
dict.add(word, 1)
}

return scanner.Err()
}
В этой версии структура words включает анонимное поле sync.Mutex, что позволяет использовать методы words.Lock и words.Unlock. Метод add блокирует карту перед ее изменением, а затем разблокирует ее. Это предотвращает одновременное изменение карты несколькими горутинами.

Важно отметить, что блокировки эффективны только в том случае, если все попытки доступа к данным управляются одной и той же блокировкой. Если операции с одними данными осуществляются с блокировкой, а с другими – нет, состояние гонки все еще возможно.

Иногда может потребоваться разрешить одновременное выполнение нескольких операций чтения, но запретить любые операции записи. Для этого можно использовать функцию sync.RWLock. Пакет sync предоставляет еще ряд полезных инструментов для координации горутин.

Спасибо за ваше время и внимание! Ваша поддержка очень важна для меня! Если вам понравилась статья, пожалуйста, поставьте лайк этой статье на моем канале Дзен

Подпишитесь на мой Телеграм-канал, чтобы быть в курсе новых статей.

Удачи!