> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/osnovy-go/sync.md).

# Sync

[**Весь код для этой главы вы можете найти здесь**](https://github.com/quii/learn-go-with-tests/tree/main/sync)

Мы хотим создать счетчик, который безопасно использовать в конкурентной среде.

Мы начнем с небезопасного счетчика и убедимся, что его поведение работает в однопоточной среде.

Затем мы продемонстрируем его небезопасность, используя несколько горутин, пытающихся использовать счетчик через тест, и исправим это.

## Сначала напишите тест

Мы хотим, чтобы наш API предоставлял нам метод для инкрементирования счетчика, а затем получения его значения.

```go
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`.

```go
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)
```

Итак, чтобы наконец запустить тест, мы можем определить эти методы:

```go
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
```

## Напишите достаточно кода, чтобы он прошел

Это должно быть тривиально для таких экспертов Go, как мы. Нам нужно хранить некоторое состояние для счетчика в нашем типе данных, а затем инкрементировать его при каждом вызове `Inc`.

```go
type Counter struct {
	value int
}

func (c *Counter) Inc() {
	c.value++
}

func (c *Counter) Value() int {
	return c.value
}
```

## Рефакторинг

Особого рефакторинга здесь нет, но, учитывая, что мы собираемся написать больше тестов для `Counter`, мы напишем небольшую функцию утверждения `assertCount`, чтобы тест читался немного яснее.

```go
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)
})
```

```go
func assertCounter(t testing.TB, got Counter, want int) {
	t.Helper()
	if got.Value() != want {
		t.Errorf("got %d, want %d", got.Value(), want)
	}
}
```

## Следующие шаги

Это было достаточно легко, но теперь у нас есть требование, что его использование должно быть безопасным в конкурентной среде. Нам нужно будет написать тест, который завершится ошибкой, чтобы проверить это.

## Сначала напишите тест

```go
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`](https://golang.org/pkg/sync/#WaitGroup), что является удобным способом синхронизации конкурентных процессов.

> WaitGroup ожидает завершения выполнения набора горутин. Основная горутина вызывает Add, чтобы установить количество горутин для ожидания. Затем каждая из горутин запускается и вызывает Done по завершении. В то же время Wait может использоваться для блокировки до тех пор, пока все горутины не завершатся.

Ожидая завершения `wg.Wait()` перед выполнением наших утверждений, мы можем быть уверены, что все наши горутины попытались `Inc` `Counter`.

## Попробуйте запустить тест

```
=== RUN   TestCounter/it_runs_safely_concurrently
--- FAIL: TestCounter (0.00s)
    --- FAIL: TestCounter/it_runs_safely_concurrently (0.00s)
    	sync_test.go:26: got 939, want 1000
FAIL
```

Тест *вероятно* завершится с другим числом, но, тем не менее, он демонстрирует, что это не работает, когда несколько горутин пытаются одновременно изменить значение счетчика.

### Почему это происходит?

`c.value++` выглядит как единая, неделимая операция, но это не так. Это сокращение для чего-то более похожего на:

```go
tmp := c.value // 1. read
tmp = tmp + 1  // 2. increment
c.value = tmp  // 3. write
```

Каждый из этих трех шагов является отдельной операцией, и Go-рантайм может свободно переключаться между горутинами в любой момент между ними. Если две горутины вызывают `Inc` примерно в одно и то же время, их шаги могут чередоваться, например:

```
goroutine A: reads c.value (0)
goroutine B: reads c.value (0)
goroutine A: increments its copy to 1
goroutine B: increments its copy to 1
goroutine A: writes c.value = 1
goroutine B: writes c.value = 1
```

Обе горутины вызвали `Inc` по одному разу, поэтому мы хотели бы, чтобы `c.value` в итоге стало `2`, но оно становится `1`. Одно из инкрементирований было незаметно потеряно, потому что обе горутины прочитали одно и то же начальное значение, прежде чем какая-либо из них записала свой результат обратно. Это называется *состоянием гонки* (race condition), и когда тысяча горутин одновременно соревнуются за чтение, инкрементирование и запись, неудивительно, что некоторые из этих инкрементирований теряются.

## Напишите достаточно кода, чтобы он прошел

Простое решение — добавить блокировку в наш `Counter`, гарантируя, что только одна горутина может инкрементировать счетчик за раз. [`Mutex`](https://golang.org/pkg/sync/#Mutex) Go предоставляет такую блокировку:

> Mutex — это блокировка взаимного исключения. Нулевое значение для Mutex — это разблокированный мьютекс.

```go
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` встраивается в структуру.

Вы можете увидеть такие примеры:

```go
type Counter struct {
	sync.Mutex
	value int
}
```

Можно утверждать, что это может сделать код немного более элегантным.

```go
func (c *Counter) Inc() {
	c.Lock()
	defer c.Unlock()
	c.value++
}
```

Это *выглядит* красиво, но, хотя программирование — это очень субъективная дисциплина, это **плохо и неправильно**.

Иногда люди забывают, что встраивание типов означает, что методы этого типа становятся *частью публичного интерфейса*; и вы часто не захотите этого. Помните, что мы должны быть очень осторожны с нашими публичными API, момент, когда мы делаем что-то публичным, это момент, когда другой код может привязаться к этому. Мы всегда хотим избежать ненужной связанности.

Предоставление `Lock` и `Unlock` в лучшем случае сбивает с толку, а в худшем случае может быть очень вредным для вашего программного обеспечения, если вызывающие стороны вашего типа начнут вызывать эти методы.

![Показано, как пользователь этого API может ошибочно изменить состояние блокировки](https://i.imgur.com/SWYNpwm.png)

*Это кажется очень плохой идеей*

## Копирование мьютексов

Наш тест проходит, но наш код всё ещё немного опасен.

Если вы запустите `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`](https://golang.org/pkg/sync/#Mutex), мы узнаем почему:

> Mutex не должен быть скопирован после первого использования.

Когда мы передаем наш `Counter` (по значению) в `assertCounter`, он попытается создать копию мьютекса.

Чтобы решить эту проблему, мы должны вместо этого передать указатель на наш `Counter`, поэтому измените сигнатуру `assertCounter`:

```go
func assertCounter(t testing.TB, got *Counter, want int)
```

Наши тесты больше не будут компилироваться, потому что мы пытаемся передать `Counter`, а не `*Counter`. Чтобы решить это, я предпочитаю создать конструктор, который показывает читателям вашего API, что лучше не инициализировать тип самостоятельно.

```go
func NewCounter() *Counter {
	return &Counter{}
}
```

Используйте эту функцию в своих тестах при инициализации `Counter`.

## Альтернатива: sync/atomic

`Mutex` — это инструмент общего назначения — он может защищать любое количество полей и любые инварианты между ними, если вы не забываете вызывать `Lock`/`Unlock` при каждом доступе. Но наш `Counter` настолько прост, насколько это возможно для общего состояния: одно целое число, инкрементируемое из нескольких горутин. Именно для таких случаев пакет [`sync/atomic`](https://pkg.go.dev/sync/atomic) предоставляет типы, такие как [`atomic.Int64`](https://pkg.go.dev/sync/atomic#Int64), которые дают безопасный конкурентный доступ к одному значению без отдельной блокировки вообще:

```go
type Counter struct {
	value atomic.Int64
}

func NewCounter() *Counter {
	return &Counter{}
}

func (c *Counter) Inc() {
	c.value.Add(1)
}

func (c *Counter) Value() int64 {
	return c.value.Load()
}
```

Нет `Mutex`, нет `Lock`/`Unlock`, и всё равно безопасно вызывать `Inc` из любого количества горутин одновременно — тот же тест, что мы написали ранее, проходит без изменений. Под капотом это использует низкоуровневые инструкции ЦП, чтобы сделать само инкрементирование атомарным, что обычно быстрее, чем `Mutex`, для таких простых случаев. `atomic.Int64` (и его аналоги, такие как `atomic.Int32` и `atomic.Bool`) также не могут быть использованы неправильно, как это могли бы делать вызовы функций в стиле `atomic.AddInt64(&x, 1)` в старых версиях Go — вы не можете забыть передать указатель и не можете случайно прочитать поле, не пройдя через `Load`.

Используйте типы `sync/atomic`, когда защищаете одно значение; используйте `Mutex`, когда вам нужно поддерживать согласованность нескольких полей или какого-либо инварианта между ними.

## Подведение итогов

Мы рассмотрели несколько аспектов из [пакета sync](https://golang.org/pkg/sync/):

* `Mutex` позволяет нам добавлять блокировки к нашим данным.
* `WaitGroup` — это средство ожидания завершения задач горутин.
* `sync/atomic` предлагает безопасный, не требующий блокировок доступ к отдельным значениям, и его стоит использовать, когда полноценный `Mutex` был бы избыточен.

### Когда использовать блокировки вместо каналов и горутин?

[Ранее мы рассмотрели горутины в первой главе о конкурентности](/lgwt/osnovy-go/concurrency.md), которые позволяют нам писать безопасный конкурентный код, так почему же вы должны использовать блокировки? [На вики Go есть страница, посвященная этой теме; Mutex или Канал](https://go.dev/wiki/MutexOrChannel).

> Распространенная ошибка новичков в Go — чрезмерное использование каналов и горутин просто потому, что это возможно, и/или потому, что это весело. Не бойтесь использовать sync.Mutex, если он лучше всего подходит для вашей проблемы. Go прагматичен, позволяя вам использовать инструменты, которые лучше всего решают вашу проблему, и не навязывая один стиль кода.

Перефразируя:

* **Используйте каналы при передаче владения данными.**
* **Используйте мьютексы для управления состоянием.**

### go vet

Не забывайте использовать go vet в своих скриптах сборки, так как он может предупредить вас о некоторых тонких ошибках в вашем коде до того, как они достигнут ваших несчастных пользователей.

### Не используйте встраивание из-за удобства

* Подумайте о влиянии встраивания на ваш публичный API.
* Вы *действительно* хотите раскрывать эти методы и позволять людям связывать с ними свой код?
* Что касается мьютексов, это может быть потенциально катастрофическим очень непредсказуемым и странным образом. Представьте себе недобросовестный код, разблокирующий мьютекс, когда он не должен этого делать; это вызовет очень странные ошибки, которые будет трудно отследить.

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/osnovy-go/sync.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
