Сав код за ово поглавље можете пронаћи овде
Желимо да направимо бројач који је сигуран за истовремену употребу.
Почећемо са несигурним бројачем и верификовати његово понашање у окружењу са једним навојем.
Затим ћемо вежбати да је то несигурност помоћу вишеструких програма покушавајући да је употребимо путем теста и поправимо.
Желимо да нам АПИ пружи метод за повећање бројача, а затим и за преузимање његове вредности.
func TestCounter(t *testing.T) {
t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
counter := Counter{}
counter.Inc()
counter.Inc()
counter.Inc()
if counter.Value() != 3 {
t.Errorf("got %d, want %d", counter.Value(), 3)
}
})
}
./sync_test.go:9:14: undefined: Counter
Хајде да дефинишемо Counter
.
type Counter struct {
}
Покушајте поново и не успе са следећим
./sync_test.go:14:10: counter.Inc undefined (type Counter has no field or method Inc)
./sync_test.go:18:13: counter.Value undefined (type Counter has no field or method Value)
Дакле, да бисмо коначно извршили пробу, можемо дефинисати те методе
func (c *Counter) Inc() {
}
func (c *Counter) Value() int {
return 0
}
Сада би требало да се покрене и не успе
=== RUN TestCounter
=== RUN TestCounter/incrementing_the_counter_3_times_leaves_it_at_3
--- FAIL: TestCounter (0.00s)
--- FAIL: TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 (0.00s)
sync_test.go:27: got 0, want 3
Ово би требало бити тривијално за стручњаке компаније Го попут нас. Морамо задржати неко стање бројача у нашем типу података, а затим га повећавати на сваком позиву Inc
type Counter struct {
value int
}
func (c *Counter) Inc() {
c.value++
}
func (c *Counter) Value() int {
return c.value
}
Нема много тога за рефакторирање, али с обзиром на то да ћемо написати више тестова око Counter
, написаћемо малу функцију тврдње assertCount
тако да тест чита мало јасније.
t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
counter := Counter{}
counter.Inc()
counter.Inc()
counter.Inc()
assertCounter(t, counter, 3)
})
func assertCounter(t testing.TB, got Counter, want int) {
t.Helper()
if got.Value() != want {
t.Errorf("got %d, want %d", got.Value(), want)
}
}
То је било довољно лако, али сада имамо захтев да мора бити безбедно за употребу у истовременом окружењу. Да бисмо то вежбали, мораћемо да напишемо неуспели тест.
t.Run("it runs safely concurrently", func(t *testing.T) {
wantedCount := 1000
counter := Counter{}
var wg sync.WaitGroup
wg.Add(wantedCount)
for i := 0; i < wantedCount; i++ {
go func() {
counter.Inc()
wg.Done()
}()
}
wg.Wait()
assertCounter(t, counter, wantedCount)
})
Ово ће се провући кроз нашу wantedCount
и активирати програм за позивање counter.Inc()
.
Користимо sync.WaitGroup
који је погодан начин синхронизације истовремених процеса.
WaitGroup
чека да се збирка гороутина заврши. Главни позивни програм позива додати да бисте поставили број програма који се чека. Тада се свака од програма покрене и по завршетку позове Готово. Истовремено, сачекајте да се користи за блокирање док се све гороутине не заврше.
Чекајући да wg.Wait()
заврши пре него што изнесемо своје тврдње, можемо бити сигурни да су све наше гороутине покушале да Inc`` Counter
.
=== RUN TestCounter/it_runs_safely_in_a_concurrent_envionment
--- FAIL: TestCounter (0.00s)
--- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s)
sync_test.go:26: got 939, want 1000
FAIL
Тест ће вероватно пасти са другим бројем, али без обзира на то показује да не функционише када вишеструки програмерски програми истовремено покушавају да мутирају вредност бројача.
Једноставно решење је додавање браве у наш Counter
, Mutex
Мутек је брава за међусобно искључивање. Нулта вредност за Мутек је откључани мутек.
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
То значи да ће сваки гороутин који зове Inc
стећи браву на Counter
ако је први. Сви остали програми ће морати да сачекају да се Unlock
пре него што добију приступ.
Ако сада поново покренете тест, он би сада требало да прође, јер сваки гороутин мора да сачека свој ред пре него што изврши промену.
Можда ћете видети овакве примере
type Counter struct {
sync.Mutex
value int
}
Може се тврдити да код може учинити мало елегантнијим.
func (c *Counter) Inc() {
c.Lock()
defer c.Unlock()
c.value++
}
Ово изгледа лепо, али иако је програмирање изузетно субјективна дисциплина, ово је лоше и погрешно.
Понекад људи забораве да уграђивање типова значи да методе тог типа постају део јавног интерфејса; а то често нећете желети. Имајте на уму да бисмо требали бити врло опрезни са нашим јавним АПИ-има, тренутак када нешто објавимо је тренутак када се други код може повезати са тим. Увек желимо да избегнемо непотребно спајање.
Излагање „закључавања“ (Lock
) и „откључавања“ (Unlock
) у најбољем је случају збуњујуће, али у најгорем случају потенцијално веома штетно за ваш софтвер ако позиоци вашег типа почну да позивају ове методе.
Ово се чини као заиста лоша идеја
Наш тест пролази, али наш код је и даље помало опасан
Ако покренете go vet
на свом коду, требало би да добијете грешку попут следеће
sync/v2/sync_test.go:16: call of assertCounter copies lock value: v1.Counter contains sync.Mutex
sync/v2/sync_test.go:39: assertCounter passes lock by value: v1.Counter contains sync.Mutex
Поглед у документацију sync.Mutex
нам каже зашто
Мутек се не сме копирати након прве употребе.
Када проследимо свој Counter
(по вредности) у assertCounter
, он ће покушати да створи копију мутека.
Да бисмо то решили, уместо тога треба да проследимо показивач на наш Counter
, па променимо потпис assertCounter
func assertCounter(t testing.TB, got *Counter, want int)
Наши тестови се више неће компајлирати, јер покушавамо да уђемо у Counter
уместо у *Counter
. Да бих то решио, више волим да креирам конструктор који читаоцима вашег АПИ-ја показује да би било боље да сами не иницијализујете тип.
func NewCounter() *Counter {
return &Counter{}
}
Користите ову функцију у тестовима приликом иницијализације Counter
.
Обрадили смо неколико ствари из пакета за синхронизацију
Mutex
нам омогућава додавање брава у наше податкеWaitgroup
је средство чекања да гороутине заврше посао
Претходно смо обрађивали програме у првом поглављу о паралелности који нам омогућавају да напишемо сигуран истовремени код, па зашто бисте користили браве? Го вики има страницу посвећену овој теми; Mutex или Channel
Уобичајена грешка за почетнике Го је прекомерна употреба канала и програма само зато што је то могуће и / или зато што је забавно. Не плашите се да користите синц.Мутек ако то најбоље одговара вашем проблему. Го је прагматичан у томе што вам омогућава да користите алате који најбоље решавају ваш проблем, а не да вас приморавају на један стил кода.
Парафразирајући:
- Користите канале приликом преношења власништва над подацима
- Користите мутексеве за управљање стањем
Не заборавите да користите го вет у својим скриптама за изградњу, јер вас може упозорити на неке суптилне грешке у вашем коду пре него што погодију ваше сиромашне кориснике.
- Размислите о ефекту који уграђивање има на ваш јавни АПИ.
- Да ли заиста желите да изложите ове методе и да људи повежу свој код са њима?
- Што се тиче мутекса, ово би могло бити потенцијално погубно на врло непредвидљиве и чудне начине, замислите да неки подли код откључа мутекс кад то не би требало да буде; ово би проузроковало неке врло чудне грешке којима ће бити тешко ући у траг.