#19.1 golang Горутины и работа с ними - асинхронного выполнение, многопоточность

Ранее мы упоминали горутины — это легковесные потоки выполнения в Go, работающие поверх системных потоков (threads).

Они позволяют запускать функции параллельно (асинхронно). Давайте посмотрим как это делается в коде

Асинхронное выполнение кода

Ты можешь запустить функцию в горутине, добавив перед её вызовом ключевое слово go:


import (
	"fmt"
	"time"
)

// обычная функция
func say(comment string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(comment, "-интерация", i)
	}
}

func main() {
	// работает параллельно функции main в горутине
	go say("Асинхронный вызов")
	// работает внутри горутины main
	say("Синхронный вызов")
}

- после запуска получим распечатку вроде:

Асинхронный вызов -интерация 0
Синхронный вызов -интерация 0
Синхронный вызов -интерация 1
Асинхронный вызов -интерация 1
Синхронный вызов -интерация 2
Асинхронный вызов -интерация 2
Асинхронный вызов -интерация 3
Синхронный вызов -интерация 3
Синхронный вызов -интерация 4

- из-за параллельности выводы (вызовы println() основной и вручную запущенной горутин перемешаются.

Уточним:

  1. Любая функция перед, которой написано клочевое слово go запускается как горутина
  2. функция main() же всегда запускает главную горутину.

Непредсказуемость выполнения - состояние гонки

Горутины паралельно работают в одном адресном пространстве поэтому для корректного доступа к общим ресурсам требуется явная синхронизация.
Без неё может возникнуть гонка данных (race condition). Такая ситуация показана в примере ниже:

package main

import (
	"fmt"
	"time"
)

func incIt(a *int) {
	*a += 1 // инкрементируем
	fmt.Print(*a, "_")
}

func deincIt(a *int) {
	*a -= 1 // декрементируем
	fmt.Print(*a, "_")
}

func main() {
	a := 1
	for i := 0; i < 3; i++ {
		go incIt(&a)
		go incIt(&a)
		go incIt(&a)
		go deincIt(&a)
		go deincIt(&a)
		go deincIt(&a)
		// Ждем завершения горутин
		time.Sleep(500 * time.Millisecond) // полсекунды должно хватить
		fmt.Println()                      // переносим строчку
	}
}


-- здесь мы три раза инкементируем и три раза декрементируем значение (передаваемое "по ссылке" - через указатель), из-за паралельности выполнения мы не можем предсказать на самом деле ни то в каком именно порядке будут проходить изменения значения переменной a, ни финальное её состояние.

Распечатка этой программы, если её запустить будет выглядть +- случайным образом, но главное что строки будут отличаться одна от другой:

0_1_0_1_-1_0_
0_0_0_1_1_1_
0_1_2_3_2_1_

- это происходит как раз из-за состояния гонки - мы не знаем какой именно поток быстрее доберется до цели (в данном случае до значения переменной "а")

Завершение фоновых горутин, после выхода из main()

Выше в коде main() мы использовали sleep() чтобы дождаться завершения фоновых горутин, которые иначе могли бы просто не успеить выполнится.

Итоги

  • Многопоточность в Go реализована через горутины.
  • Доступ нескольких горутин к общим ресурсам в отсутствие сихронизации приведет к гонке данных.

Что дальше

Далее в уроках рассмотрим вопрос синхронизации работы горутин

Key Words for FKN + antitotal forum (CS VSU):

vedro-compota's picture

переписать иллюстрацию состояния гонки - чтобы финальные результаты разнились

_____________
матфак вгу и остальная классика =)