Рассмотрим простой и вообщем-то спорный шаблон Singleton. Его “потокобезопасную” версию и общие подходы к решению таких задач.
Классическое назначение: гарантирует, что у типа есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
В объектно-ориентированных языках синглетон эмулирует глобальные переменные.
В Go есть глобальные переменные, которые к тому же могут быть инициализированы в init(). Но синглетон может дать возможность для отложенной(ленивой) инициализация(lazy initialization).
Надо помнить, что синглетон обладает теми же проблема, что и глобальные переменные.
Начнем с простой реализации
Реализуем глобальный счетчик:
package base
type Singleton interface {
AddOne() int
}
type singleton struct {
count int
}
var instance *singleton
func GetInstance() Singleton {
if instance==nil{
instance = new(singleton)
}
return instance
}
func (s *singleton) AddOne() int {
s.count++
return s.count
}
Напишем тесты и убедимся что он работает:
package base
import "testing"
func TestGetInstance(t *testing.T) {
var counter1 Singleton
counter1 = GetInstance()
if counter1 == nil {
t.Fatalf("expected pointer to Singleton after calling GetInstance(), not nil")
}
currentCount := counter1.AddOne()
if currentCount != 1 {
t.Errorf("After calling for the first time to count, the count must be 1 but it is %d\n", currentCount)
}
var counter2, expectedCounter Singleton
expectedCounter = counter1
counter2 = GetInstance()
if counter2 != expectedCounter {
t.Error("Expected same instance in counter2 but it got a different instance")
}
currentCount = counter2.AddOne()
if currentCount != 2 {
t.Errorf("After calling 'AddOne' using the second counter, the current count must be 2 but was %d\n", currentCount)
}
}
Проблема этой реализации становится сразу очевидна, функция GetInstance
и метод AddOne
подвержены ошибкам конкурентного доступа.
GetInstance
Первое, что приходит в голову это просто добавить блокировок:
var instance *singleton
var mu sync.Mutex
func GetInstance() Singleton {
mu.Lock()
defer mu.Unlock()
if instance==nil{
instance = new(singleton)
}
return instance
}
И это неплохо работает. Но создаем мы instance всего один раз, а блокировки срабатывают каждый раз.
Так же можно попробовать некоторые варианты “блокировка с двойной проверкой”, но к счастью в стандартной библиотеке Golang есть sync.Once
. Ему можно передать функцию, которая будет выполнена не более одного раза:
var instance *singleton
var once sync.Once
func GetInstance() Singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
С GetInstance разобрались, осталось самое интересное.
AddOne и другие методы доступа
Начнем с теста который сможет продемонстрировать проблему:
func TestParallel(t *testing.T) {
singleton := GetInstance()
singleton2 := GetInstance()
n := 5000
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
singleton.AddOne()
wg.Done()
}()
wg.Add(1)
go func() {
singleton2.AddOne()
wg.Done()
}()
}
fmt.Printf("Before loop, current count is %d\n", singleton.GetCount())
wg.Wait()
fmt.Printf("Current count is %d\n", singleton.GetCount())
currentCount1 := singleton.GetCount()
currentCount2 := singleton2.GetCount()
if currentCount1 != currentCount2 {
t.Errorf("Counts not match\nCurrentCount1=%d\nCurrentCount2=%d", currentCount1, currentCount2)
}
if currentCount1 != n*2 {
t.Errorf("Counts not match\nCurrentCount1=%d\nN*2=%d", currentCount1, n*2)
}
}
Запускаем цикл на 5000 итераций в которых создается по две горутины, в горутинах вызывается метод AddOne
. По завершению всех горутин мы ожидаем, что наш счетчик будет иметь значение 5000*2=10к
=== RUN TestParallel
Before loop, current count is 9499
Current count is 9524
--- FAIL: TestParallel (0.00s)
main_test.go:68: Counts not match
CurrentCount1=9524
N*2=10000
Сразу по окончанию цикла у нас получилось 9499, это неплохо, к этому моменту еще не все горутины закончили свою работу.
А уже по завершению мы получаем счетчик со значением 9524, а это плохо, куда то потерялось целых 476.
Добавляем -race
и запускаем:
WARNING: DATA RACE
Read at 0x00c04205e130 by goroutine 9:
github.com/germangorelkin/go-patterns/creational/singleton/once.(*singleton).AddOne()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main.go:26 +0x3f
github.com/germangorelkin/go-patterns/creational/singleton/once.TestParallel.func2()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main_test.go:50 +0x45
Previous write at 0x00c04205e130 by goroutine 8:
github.com/germangorelkin/go-patterns/creational/singleton/once.(*singleton).AddOne()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main.go:26 +0x55
github.com/germangorelkin/go-patterns/creational/singleton/once.TestParallel.func1()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main_test.go:45 +0x45
Goroutine 9 (running) created at:
github.com/germangorelkin/go-patterns/creational/singleton/once.TestParallel()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main_test.go:49 +0x178
testing.tRunner()
./testing/testing.go:777 +0x174
Goroutine 8 (finished) created at:
github.com/germangorelkin/go-patterns/creational/singleton/once.TestParallel()
./github.com/germangorelkin/go-patterns/creational/singleton/once/main_test.go:44 +0x11f
testing.tRunner()
./testing/testing.go:777 +0x174
==================
Before loop, current count is 9988
Current count is 9989
--- FAIL: TestParallel (0.60s)
main_test.go:68: Counts not match
CurrentCount1=9989
N*2=10000
testing.go:730: race detected during execution of test
FAIL
Получаем DATA RACE.
Data race(“гонка данных”) осуществляется, когда две горутины одновременно обращаются к одной и той же переменной и по крайней мере одно из обращений представляет собой запись. Подробней можно посмотреть в статье Race condition и Data Race.
Существует минимум три способа избежать этой проблемы:
Первый способ — не изменять переменную. Как не странно, но иногда есть возможность отказаться от записи и использовать уже предустановленный набор данных.
Второй способ — использовать взаимное исключение. Для этой цели служит бинарный семафор, он же мьютекс. В Go его можно реализовать через буферизированный канал. Но, к счастью, в стандартной библиотеки уже есть примитивы синхронизации: sync.Mutex
и sync.RWMutex
.
Внесем корректировки:
import "sync"
type Singleton interface {
AddOne()
GetCount() int
}
type singleton struct {
count int
sync.RWMutex
}
var instance *singleton
var once sync.Once
func GetInstance() Singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
func (s *singleton) AddOne() {
s.Lock()
defer s.Unlock()
s.count++
}
func (s *singleton) GetCount()int {
s.RLock()
defer s.RUnlock()
return s.count
}
go test -race
:
Before loop, current count is 9864
Current count is 10000
PASS
Всё в порядке, всё отлично.
Третий способ — избегать обращения к переменной из нескольких горутин.
Ограничиваем переменную одной горутиной. Другие горутины не могут получить прямой доступ к переменным, они должны использовать каналы для запроса у ограничивающей горутины(“monitor”) получения значения или обновления переменной.
Следуя этому принципам, пробуем реализовать синглетон.
Потребуются три канала:
var addCh chan bool = make(chan bool)
var getCountCh chan chan int = make(chan chan int)
var quitCh chan bool = make(chan bool)
Через канал addCh
будем запрашивать обновление переменной. Канал quitCh
сигнализирует ограничивающей горутине, что нужно завершить работу и освободить ресурсы.
getCountCh
это канал каналов int
. Через него можно будет запрашивать значение переменной, значение будет передано обратно через полученный канал. Возможно, это звучит запутано, но на деле это достаточно удобно и требует небольшого усилия, что бы разобраться.
Функция которая будет выступать ограничителем:
go func() {
for {
select {
case <-addCh:
instance.count++
case ch := <-getCountCh:
ch <- instance.count
case <-quitCh:
return
}
}
}()
Полный код может выглядит таким образом:
import "sync"
var addCh chan bool = make(chan bool)
var getCountCh chan chan int = make(chan chan int)
var quitCh chan bool = make(chan bool)
type Singleton interface {
AddOne()
GetCount() int
Stop()
}
type singleton struct {
count int
}
var instance *singleton
var once sync.Once
func GetInstance() Singleton {
once.Do(func() {
instance = new(singleton)
go func() {
for {
select {
case <-addCh:
instance.count++
case ch := <-getCountCh:
ch <- instance.count
case <-quitCh:
return
}
}
}()
})
return instance
}
func (s *singleton) AddOne() {
addCh <- true
}
func (s *singleton) GetCount() int {
resCh := make(chan int)
defer close(resCh)
getCountCh <- resCh
return <-resCh
}
func (s *singleton) Stop() {
quitCh <- true
close(addCh)
close(getCountCh)
close(quitCh)
instance = nil
}
go test -race
:
Before loop, current count is 9824
Current count is 10000
PASS
Benchmark
func BenchmarkChannelSingletonParallel(b *testing.B) {
singleton := GetInstance()
singleton2 := GetInstance()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
singleton.AddOne()
singleton2.GetCount()
singleton2.AddOne()
singleton.GetCount()
}
})
}
func BenchmarkChannelSingleton(b *testing.B) {
singleton := GetInstance()
singleton2 := GetInstance()
b.ResetTimer()
for i := 0; i < b.N; i++ {
singleton.AddOne()
singleton2.GetCount()
singleton2.AddOne()
singleton.GetCount()
}
}
Дают такой результат на каналы:
BenchmarkChannelSingletonParallel-4 500000 3441 ns/op 192 B/op 2 allocs/op
BenchmarkChannelSingleton-4 300000 4590 ns/op 192 B/op 2 allocs/op
flat flat% sum% cum cum%
650ms 9.37% 9.37% 650ms 9.37% runtime.osyield
470ms 6.77% 16.14% 470ms 6.77% runtime.stdcall1
400ms 5.76% 21.90% 410ms 5.91% runtime.addspecial
400ms 5.76% 27.67% 850ms 12.25% runtime.pcvalue
400ms 5.76% 33.43% 400ms 5.76% runtime.stdcall2
390ms 5.62% 39.05% 390ms 5.62% runtime.procyield
360ms 5.19% 44.24% 450ms 6.48% runtime.step
340ms 4.90% 49.14% 1500ms 21.61% runtime.gentraceback
290ms 4.18% 53.31% 1150ms 16.57% runtime.lock
280ms 4.03% 57.35% 880ms 12.68% runtime.selectgo
И такие на мьютексы:
BenchmarkMutexSingletonParallel-4 3000000 419 ns/op 0 B/op 0 allocs/op
BenchmarkMutexSingleton-4 10000000 207 ns/op 0 B/op 0 allocs/op
flat flat% sum% cum cum%
690ms 13.12% 13.12% 690ms 13.12% runtime.procyield
470ms 8.94% 22.05% 950ms 18.06% runtime.deferreturn
470ms 8.94% 30.99% 470ms 8.94% runtime.freedefer
460ms 8.75% 39.73% 460ms 8.75% runtime.newdefer
330ms 6.27% 46.01% 990ms 18.82% sync.(*Mutex).Lock
310ms 5.89% 51.90% 390ms 7.41% sync.(*RWMutex).RUnlock
260ms 4.94% 56.84% 1260ms 23.95% sync.(*RWMutex).Lock
250ms 4.75% 61.60% 320ms 6.08% sync.(*Mutex).Unlock
230ms 4.37% 65.97% 230ms 4.37% sync.(*RWMutex).RLock
220ms 4.18% 70.15% 720ms 13.69% runtime.deferproc
https://github.com/GermanGorelkin/go-patterns/tree/master/creational/singleton
Комментарии в Telegram-группе!