Въведение

Тъй като нашите Go програми стават все по-сложни, може да срещнем ситуации, в които трябва да работим със споделени данни в множество goroutines. Това може да доведе до условия на надпревара в данните, при които различни goroutines се опитват да получат достъп и да променят едни и същи данни едновременно, което води до непредвидимо поведение и потенциално неправилни резултати. За да избегнем подобни проблеми, ние използваме mutexes в Go за безопасен контрол на достъпа до споделени ресурси.

В тази статия ще демонстрираме как да използвате mutex за едновременно управление на карта от броячи. Ще гарантираме, че броячите се актуализират правилно и ще избегнем всякакви условия на състезание. Така че, нека се потопим!

Разбиране на мютексите

Mutexes, съкращение от „Взаимно изключване,“ са примитиви за синхронизиране, които защитават споделените ресурси от едновременен достъп. В Go един мютекс е представен от типа sync.Mutex. Когато goroutine придобие mutex чрез извикване на Lock(), тя получава изключителен достъп до споделения ресурс. Всяка друга goroutine, която се опитва да придобие същия мютекс, докато е заключен, ще бъде блокирана, докато mutex не бъде освободен от първата goroutine, използваща Unlock().

Разбиране на Кодекса

Първо, нека анализираме кода, с който ще работим. Дефинирахме структура Container, която съдържа карта на броячи и sync.Mutex за синхронизиране на достъпа до тази карта.

package main

import (
    "fmt"
    "sync"
)

type Container struct {
    mu       sync.Mutex
    counters map[string]int
}

func (c *Container) inc(name string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.counters[name]++
}

func main() {
    c := Container{
        counters: map[string]int{"a": 0, "b": 0},
    }

    var wg sync.WaitGroup

    doIncrement := func(name string, n int) {
        for i := 0; i < n; i++ {
            c.inc(name)
        }
        wg.Done()
    }

    wg.Add(3)
    go doIncrement("a", 10000)
    go doIncrement("a", 10000)
    go doIncrement("b", 10000)

    wg.Wait()
    fmt.Println(c.counters)
}

Обяснение

  1. Ние дефинираме структура Container, съдържаща две полета: mu, sync.Mutex и counters, карта на броячи на низове.
  2. Методът inc на структурата Container се използва за увеличаване на брояча с даденото име. Преди достъп до counters картата, заключваме мютекса с помощта на c.mu.Lock(). Това гарантира, че само една goroutine може да има достъп до картата counters в даден момент.
  3. Използваме оператор defer, за да отключим мютекса в края на метода inc. Това гарантира, че мютексът ще бъде освободен, дори ако възникне паника по време на изпълнението на метода.
  4. Във функцията main създаваме Container екземпляр c с първоначални броячи, зададени на 0 за ключове 'a' и 'b'.
  5. Ние дефинираме функция doIncrement, която увеличава даден брояч ("a" или "b") определен брой пъти (n). Той работи в goroutine и използва метода inc на структурата Container за извършване на увеличенията.
  6. Създаваме три goroutines за едновременно увеличаване на броячите. Двама от тях увеличават брояча с име "a" по 10 000 пъти всеки, а един увеличава брояча с име "b" 10 000 пъти.
  7. Използваме sync.WaitGroup, за да изчакаме всички goroutines да завършат, преди да отпечатаме крайните стойности на броячите.
  8. Когато стартирате програмата, ще забележите, че броячите се актуализират, както се очаква, без никакви проблеми с данни или паралелност. Резултатът ще бъде карта, съдържаща крайните стойности на броячите:
map[a:20000 b:10000]

Използване на sync.RWMutex за достъп за четене/запис

Стандартната библиотека на Go предоставя разширение на основния мьютекс: sync.RWMutex. За разлика от традиционния mutex, RWMutex позволява на множество goroutines да придобият заключване за четене едновременно, стига нито една goroutine да не поддържа заключване за запис. Тази функция е особено полезна, когато ресурсът се чете по-често, отколкото се пише.

Нека да видим пример как да използвате RWMutex:

package main

import (
 "fmt"
 "sync"
 "time"
)

type DataStore struct {
 mu    sync.RWMutex
 data  map[string]string
}

func (ds *DataStore) ReadData(key string) string {
 ds.mu.RLock()
 defer ds.mu.RUnlock()
 return ds.data[key]
}

func (ds *DataStore) WriteData(key, value string) {
 ds.mu.Lock()
 defer ds.mu.Unlock()
 ds.data[key] = value
}

func main() {
 ds := DataStore{data: make(map[string]string)}

 go func() {
  for i := 0; i < 5; i++ {
   ds.WriteData(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
   time.Sleep(time.Millisecond * 500)
  }
 }()

 for i := 0; i < 5; i++ {
  key := fmt.Sprintf("key%d", i)
  fmt.Printf("Read: %s = %s\n", key, ds.ReadData(key))
  time.Sleep(time.Millisecond * 200)
 }
}

Избягване на безизходица с TryLock

Често срещана клопка при използване на мутекси е блокиране, което се случва, когато goroutines са блокирани и чакат една друга да освободят ключалки за неопределено време. Техниката TryLock ни позволява да се опитаме да получим заключване, без да блокираме goroutine. Ако заключването не е налично, goroutine може да избере да извърши алтернативни действия или да опита отново по-късно.

package main

import (
 "fmt"
 "sync"
 "time"
)

type Resource struct {
 mu sync.Mutex
}

func (r *Resource) DoSomething() {
 if r.mu.TryLock() {
  defer r.mu.Unlock()
  // Perform the critical section here
  fmt.Println("Resource acquired.")
 } else {
  // Handle the case when the resource is unavailable
  fmt.Println("Resource is busy, retry later.")
 }
}

func main() {
 resource := Resource{}

 go func() {
  resource.DoSomething()
 }()

 time.Sleep(time.Millisecond * 200)
 resource.DoSomething()
}

Рекурсивен Mutex за Reentrant Locks

В някои сценарии може да е необходимо горограмата да получи отново същата ключалка, която вече притежава. Това е известно като повторно влизане или рекурсивно заключване. Докато основният sync.Mutex не позволява това, типът sync.Mutex от пакета 'sync' поддържа заключване при повторно влизане.

package main

import (
 "fmt"
 "sync"
)

type RecLockResource struct {
 mu sync.Mutex
}

func (r *RecLockResource) Outer() {
 r.mu.Lock()
 defer r.mu.Unlock()

 fmt.Println("Outer lock acquired.")
 r.Inner()
}

func (r *RecLockResource) Inner() {
 r.mu.Lock()
 defer r.mu.Unlock()

 fmt.Println("Inner lock acquired.")
}

func main() {
 resource := RecLockResource{}
 resource.Outer()
}

Предимства на Mutexes

  1. Безопасност на нишките: Мутексите предпазват споделените ресурси от едновременен достъп, като елиминират условията за надпревара в данните и гарантират целостта на данните.
  2. Синхронизация: Мутексите позволяват синхронизация между goroutines, което им позволява да се редуват при достъп до споделени данни.
  3. Прости и ефикасни: Вградените мутекси на Go са лесни за използване и ефикасно внедрени, което ги прави популярен избор за едновременни задачи по програмиране.

Заключение

В тази статия проучихме как да управляваме едновременното състояние с помощта на mutexes в Go. Успешно синхронизирахме достъпа до карта на броячи в множество goroutines, гарантирайки целостта на данните и предотвратявайки условия на състезание. Проучихме различни техники за подобряване на ефективността и гъвкавостта на паралелните програми. Разбирането на sync.RWMutex, избягването на блокирания с TryLock и внедряването на заключвания за повторно влизане отварят нови възможности за управление на споделени ресурси.

Очаквайте следващата ни статия, където ще продължим нашето изследване на едновременното програмиране в Go. Ще демонстрираме как да постигнем една и съща задача за управление на състоянието с помощта на goroutines и канали, което е друг мощен механизъм, осигурен от паралелния модел на Go.

Приятно кодиране с Go и не забравяйте винаги да пишете код, безопасен за нишки, когато работите с едновременни приложения!

Честито кодиране!