Skip to content

Latest commit

 

History

History
242 lines (171 loc) · 13.2 KB

File metadata and controls

242 lines (171 loc) · 13.2 KB

Многопоточность

Go называют C 21 века. Я думаю, этому есть две причины: во-первых, Go - простой язык, во-вторых, многопоточность сегодня является горячей темой, а Go поддерживает многопоточность на уровне языка.

Горутины

Горутины и многопоточность встроены в ядро Go. Они подобны потокам, но работают по-другому. Больше дюжины горутин будут иметь в своей основе только 5-6 потоков. Go также дает в Ваше распоряжение полную поддержку расшаривания памяти в горутинах. Одна горутина обычно использует 4~5 килобайт памяти стека. Поэтому нетрудно запустить тысячи горутин на одном компьютере. Горутины более эффективны, более удобны в использовании и менее ресурсоемки, чем системные потоки.

Горутины в Go запускаются на менеджере потоков во время выполнения. Чтобы создать новую горутину, которая на низлежащем уровне является функцией, используется ключевое слово go, ( main() - это горутина ).

go hello(a, b, c)

Давайте посмотрим на пример:

package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 5; i++ {
    	runtime.Gosched()
    	fmt.Println(s)
	}
}

func main() {
	go say("Мир") // создает новую горутину
	say("Привет") // текущая горутина
}

Вывод:

Привет
Мир
Привет
Мир
Привет
Мир
Привет
Мир
Привет

Мы видим, что реализовывать многопоточность в Go с помощью ключевого слова go очень легко. В примере выше две горутины разделяют память, но лучше следовать следующему рецепту: не используйте разделяемые данные для коммуникаций, а используйте коммуникации для того, чтобы разделять данные.

runtime.Gosched() говорит процессору, что нужно исполнить другие горутины и вернуться затем назад.

Для того, чтобы запустить все горутины, планировщик использует только один поток, что означает, что он один реализует многопоточность. Если для задействования преимущества параллельных процессов Вам надо использовать больше ядер процессора, Вам нужно вызвать runtime.GOMAXPROCS(n), чтобы установить количество ядер, которые Вы хотите использовать. Если n<1, эта команда ничего не меняет. В будущем эта функция может быть убрана, больше деталей о параллельных процессах и многопоточности смотрите в этой статье.

Каналы

Горутины выполняются в одном и том же адресном пространстве, поэтому, когда Вам нужен доступ к разделяемой памяти, Вам нужно организовывать синхронизацию. Как осуществлять коммуникации между различными горутинами? Для этого Go использует очень хороший механизм коммуникаций, называемый канал. Канал - это как двусторонняя "труба" в шеллах Unix: используйте канал, чтобы посылать и принимать данные. Единственный тип данных, используемый в каналах - это тип channel и ключевое слово chan. Помните, что для того, чтобы создать канал, нужно использовать make.

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

Для того, чтобы посылать и принимать данные, канал использует оператор <-.

ch <- v    // посылаем v в канал ch.
v := <-ch  // получаем данные из ch, присваиваем их v

Посмотрим еще на примеры:

package main

import "fmt"

func sum(a []int, c chan int) {
	total := 0
	for _, v := range a {
    total += v
	}
	c <- total  // посылаем total в c
}

func main() {
	a := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(a[:len(a)/2], c)
	go sum(a[len(a)/2:], c)
	x, y := <-c, <-c  // принимаем из c

	fmt.Println(x, y, x + y)
}

Прием и передача данных по умолчанию блокирует канал, и это делает использование синхронных горутин намного легче. Под блокированием я имею в виду то, что горутина не продолжит свое выполнение, если пытается получить данные из пустого канала, например, (value := <-ch), пока другие горутины не отправят данные в этот канал. С другой строны, горутина не продожит свое выполнение, пока данные, которые она послала в канал, например (ch<-5), не приняты.

Буферизованные каналы

Выше я говорил о небуферизованных каналах. В Go также есть буферизованные каналы, которые могут хранить больше, чем один элемент. Нарпимер, ch := make(chan bool, 4), здесь мы создали канал, который может содержать 4 булевых элемента. Поэтому в этот канал мы можем послать до 4 булевых элементов, и горутина не заблокируется, но она заблокируется, когда Вы попытаетесь послать в канал пятый элемент, и ни одна горутина его не примет.

ch := make(chan type, n)

n == 0 ! не буферизованный(блокирует выполнение горутины)
n > 0 ! буферизованный (не заблокирует выполнение, пока не получит n элементов)

Вы можете попробовать запустить следующий код на своем компьютере и поменять какие-нибудь значения:

package main

import "fmt"

func main() {
	c := make(chan int, 2)  // если изменить 2 на 1, то в процессе выполнения будет вызвана ошибка, но если заменить 2 на 3, то все будет в порядке
	c <- 1
	c <- 2
	fmt.Println(<-c)
	fmt.Println(<-c)
}

Range и Close

Чтобы оперировать данными в буферизованных каналах так же, как в срезах или картах, мы можем использовать range:

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 1, 1
	for i := 0; i < n; i++ {
    	c <- x
    	x, y = y, x + y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
	    fmt.Println(i)
	}
}

for i := range c не остановит чтение данных из канала, пока тот не будет закрыт. Чтобы закрыть канал в вышеприведенном примере мы используем ключевое слово close. Послать или принять данные из закрытого канала невозможно; чтобы проверить, закрыт ли канал, можно использовать команду v, ok := <-ch. Если ok вернул false, это означает, что в канале нет данных, и он был закрыт.

Не забывайте закрывать каналы в горутинах, которые посылают данные, а не в тех, которые получают, иначе очень легко получить состояние "panic".

Другое, что надо помнить о каналах - это то, что каналы не подобны файлам. Вам не нужно закрывать их, пока Вы не придете к выводу, что канал больше не нужен, или Вы хотите выйти из цикла range.

Select

В примерах выше мы использовали только один канал, но что, если мы имеем дело более чем с одним каналом? В Go есть ключевое слово, называемое select, используемое для того, чтобы слушать много каналов.

select по умолчанию блокирует дальнейшее выполнение и продолжает его лишь только тогда, когда в одном из каналов появляются данные для получения или отправки. Если несколько каналов готовы принять или отправить данные одновременно, select в случайном порядке выбирает, с каким из них работать.

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 1, 1
	for {
    	select {
    	case c <- x:
        	x, y = y, x + y
    	case <-quit:
    	fmt.Println("Выход")
        	return
    	}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
    	for i := 0; i < 10; i++ {
        	fmt.Println(<-c)
    	}
    	quit <- 0
	}()
	fibonacci(c, quit)
}

У select есть также поведение по умолчанию default, так же, как и у switch. Если ни один их каналов не готов к использованию, исполняется случай по умолчанию (при этом больше не ожидается, пока канал будет готов).

select {
case i := <-c:
	// use i
default:
	// выполняется, когда c заблокирован
}

Тайм-аут

Иногда горутина блокируется. Как избежать того, чтобы это заблокировало всю программу? Это просто, можно установить тайм-аут в select:

func main() {
	c := make(chan int)
	o := make(chan bool)
	go func() {
    	for {
        	select {
            	case v := <- c:
               		println(v)
            	case <- time.After(5 * time.Second):
                	println("Тайм-аут")
                	o <- true
                	break
        	}
    	}
	}()
	<- o
}

Runtime и горутины

В пакете runtime есть несколько функций для работы с горутинами:

  • runtime.Goexit()

    Прекращает работу текущей горутины, но функции после слова defer будут выполнены в обычном порядке

  • runtime.Gosched()

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

  • runtime.NumCPU() int

    Возвращает количество ядер процессора

  • runtime.NumGoroutine() int

    Возвращает количество горутин

  • runtime.GOMAXPROCS(n int) int

    Устанавливает количество ядер процессора, которые Вы хотите использовать

Ссылки