Skip to content

Latest commit

 

History

History
276 lines (188 loc) · 13 KB

sync.md

File metadata and controls

276 lines (188 loc) · 13 KB

Синхронизација

Сав код за ово поглавље можете пронаћи овде

Желимо да направимо бројач који је сигуран за истовремену употребу.

Почећемо са несигурним бројачем и верификовати његово понашање у окружењу са једним навојем.

Затим ћемо вежбати да је то несигурност помоћу вишеструких програма покушавајући да је употребимо путем теста и поправимо.

Прво напиши тест

Желимо да нам АПИ пружи метод за повећање бројача, а затим и за преузимање његове вредности.

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 пре него што добију приступ.

Ако сада поново покренете тест, он би сада требало да прође, јер сваки гороутин мора да сачека свој ред пре него што изврши промену.

Видео сам друге примере где је sync.Mutex уграђен у структуру.

Можда ћете видети овакве примере

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

Уобичајена грешка за почетнике Го је прекомерна употреба канала и програма само зато што је то могуће и / или зато што је забавно. Не плашите се да користите синц.Мутек ако то најбоље одговара вашем проблему. Го је прагматичан у томе што вам омогућава да користите алате који најбоље решавају ваш проблем, а не да вас приморавају на један стил кода.

Парафразирајући:

  • Користите канале приликом преношења власништва над подацима
  • Користите мутексеве за управљање стањем

go vet

Не заборавите да користите го вет у својим скриптама за изградњу, јер вас може упозорити на неке суптилне грешке у вашем коду пре него што погодију ваше сиромашне кориснике.

Не користите уграђивање јер је то погодно

  • Размислите о ефекту који уграђивање има на ваш јавни АПИ.
  • Да ли заиста желите да изложите ове методе и да људи повежу свој код са њима?
  • Што се тиче мутекса, ово би могло бити потенцијално погубно на врло непредвидљиве и чудне начине, замислите да неки подли код откључа мутекс кад то не би требало да буде; ово би проузроковало неке врло чудне грешке којима ће бити тешко ући у траг.