Урок 11 «Быстро выучить язык го» — Тысячи лошадей, бегущих по сопрограммам

задняя часть Go сервер Безопасность
Урок 11 «Быстро выучить язык го» — Тысячи лошадей, бегущих по сопрограммам

Сопрограммы и каналы — одна из наиболее важных особенностей языка Go как параллельного языка программирования.Новички могут полностью понять сопрограммы как потоки, но их проще использовать, чем потоки, и они занимают меньше ресурсов. Обычно запуск десятков тысяч потоков в процессе уже перегружен, но язык Go позволяет запускать миллионы сопрограмм и легко с этим справляется. Если сопрограммы сравнить с небольшими островами, то канал является коммуникационным мостом между островами, и данные проходят по каналу, чтобы течь от одной сопрограммы к другой. Каналы — это защищенные от параллелизма структуры данных, похожие на очереди сообщений в памяти, которые позволяют многим сопрограммам одновременно читать и записывать в каналы.

Сопрограмма в языке Go называется горутиной, а канал называется каналом.

начало сопрограммы

Создать сопрограмму в Go очень просто, просто используйте ключевое слово go и добавьте вызов функции. Язык Go запустит новую сопрограмму, и вызов функции станет точкой входа этой сопрограммы.

package main

import "fmt"
import "time"

func main() {
	fmt.Println("run in main goroutine")
	go func() {
		fmt.Println("run in child goroutine")
		go func() {
			fmt.Println("run in grand child goroutine")
			go func() {
				fmt.Println("run in grand grand child goroutine")
			}()
		}()
	}()
	time.Sleep(time.Second)
	fmt.Println("main goroutine will quit")
}

-------
run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine
main goroutine will quit

Основная функция выполняется в основной сопрограмме (основная горутина).В приведенном выше примере мы запускаем подпрограмму в основной сопрограмме, подпрограмма запускает дочернюю сопрограмму, а внук. Эти сопрограммы как бы образуют родитель-потомок, потомок и отношения, но на самом деле иерархических отношений между сопрограммами не так уж и много. параллельные отношения между сопрограммами.

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

В приведенном выше коде основная сопрограмма спит в течение 1 секунды, ожидая завершения выполнения подпрограмм. Если вы удалите эту строку кода для сна, вы не увидите следов работы дочерних сопрограмм.

-------------
run in main goroutine
main goroutine will quit

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

Дочерняя сопрограмма аварийно завершает работу

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

package main

import "fmt"
import "time"

func main() {
	fmt.Println("run in main goroutine")
	go func() {
		fmt.Println("run in child goroutine")
		go func() {
			fmt.Println("run in grand child goroutine")
			go func() {
				fmt.Println("run in grand grand child goroutine")
				panic("wtf")
			}()
		}()
	}()
	time.Sleep(time.Second)
	fmt.Println("main goroutine will quit")
}

---------
run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine
panic: wtf

goroutine 34 [running]:
main.main.func1.1.1()
	/Users/qianwp/go/src/github.com/pyloque/practice/main.go:14 +0x79
created by main.main.func1.1
	/Users/qianwp/go/src/github.com/pyloque/practice/main.go:12 +0x75
exit status 2

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

Чтобы защитить безопасность подпрограммы, мы обычно добавляем оператор recovery() в начало функции входа сопрограммы, чтобы восстановить исключение, возникающее внутри сопрограммы, и заблокировать его распространение на основную сопрограмму. вызвать сбой программы. Оператор восстановления должен быть написан внутри оператора отсрочки.

go func() {
  defer func() {
    if err := recover(); err != nil {
      // log error
    }
  }()
  // do something
}()

Запустить миллион сопрограмм

Язык Go может управлять миллионами сопрограмм одновременно. Это не хвастовство. Давайте напишем код для запуска этих миллионов сопрограмм. Читатели, пожалуйста, представьте себе ощущение этой армии миллионов, работающих одновременно.

package main

import "fmt"
import "time"

func main() {
	fmt.Println("run in main goroutine")
	i := 1
	for {
		go func() {
			for {
				time.Sleep(time.Second)
			}
		}()
		if i % 10000 == 0 {
			fmt.Printf("%d goroutine started\n", i)
		}
		i++
	}
}

Приведенный выше код будет создавать сопрограммы бесконечно, каждая сопрограмма спит, чтобы гарантировать, что все они живы, сопрограмма будет просыпаться раз в 1 секунду. На моем персональном компьютере эта программа создала десятки миллионов сопрограмм и еще не достигла верхнего предела.Обратите внимание, что использование памяти составляет менее 1 ГБ, что означает, что использование памяти каждой сопрограммой составляет менее 100 байт.

бесконечный цикл сопрограммы

Ранее мы использовали функцию recovery(), чтобы сбой отдельных сопрограмм не повлиял на весь процесс. Но если есть бесконечный цикл отдельных сопрограмм, будут ли другие сопрограммы голодать и не работать? Давайте проведем эксперимент

package main

import "fmt"
import "time"

func main() {
	fmt.Println("run in main goroutine")
	n := 3
	for i:=0; i<n; i++ {
		go func() {
			fmt.Println("dead loop goroutine start")
			for {}  // 死循环
		}()
	}
	for {
		time.Sleep(time.Second)
		fmt.Println("main goroutine running")
	}
}

Интересное явление можно обнаружить, настроив значение переменной n в приведенном выше коде: когда значение n больше 3, у основной сопрограммы не будет шансов запуститься, а если значение n равно 3, 2, или 1, основная сопрограмма по-прежнему может запускаться каждый раз, когда выводится каждую секунду. Чтобы объяснить это явление, мы должны иметь глубокое понимание того, как работают сопрограммы.

Природа сопрограмм

Процесс может запускать несколько потоков, и каждый поток может запускать множество сопрограмм. Потоки отвечают за планирование сопрограмм, чтобы обеспечить возможность выполнения каждой сопрограммы. Когда сопрограмма спит, она дает право на выполнение потока другим сопрограммам для запуска и не может продолжать занимать поток. В одном потоке может выполняться не более одной сопрограммы.

За планирование потоков отвечает операционная система, алгоритм планирования работает в режиме ядра, а вызов сопрограммы отвечает за время выполнения языка Go, а алгоритм планирования работает в пользовательском режиме.

Сопрограмму можно упростить до трех состояний: рабочее состояние, состояние готовности и состояние сна. В одном потоке может быть не более одной сопрограммы в состоянии выполнения. Сопрограммы в состоянии готовности относятся к тем сопрограммам, которые могут выполняться, но еще не получили возможности для запуска. Сопрограммы еще не могут работать, они ждут выполнения определенных условий, таких как завершение операций ввода-вывода, окончание времени сна и т. д.

Планирование потоков операционной системой является упреждающим, то есть бесконечный цикл одного потока не повлияет на выполнение других потоков, а непрерывная работа каждого потока ограничена временным интервалом.

Планирование сопрограмм средой выполнения языка Go не является упреждающим. Если одна сопрограмма занимает право выполнения потока через бесконечный цикл, то у этого потока нет шансов запустить другие сопрограммы, и можно сказать, что этот поток приостановлен. Тем не менее, внутри процесса часто бывает несколько потоков.Фальсифицировать поток можно, но все фальшивые смерти приведут к зависанию всего процесса.

Каждый поток будет содержать несколько сопрограмм состояния готовности для формирования очереди готовности. Если этот поток будет приостановлен из-за мертвого цикла какой-либо другой сопрограммы, все сопрограммы состояния готовности в этой очереди не смогут запуститься? ? Планировщик среды выполнения языка Go использует алгоритм кражи работы.Когда поток простаивает, то есть все сопрограммы в потоке спят (или ни одна из сопрограмм), он переходит в очередь готовности других потоков. и украсть несколько сопрограмм для запуска. То есть эти потоки будут активно искать работу, и при нормальных обстоятельствах среда выполнения будет пытаться равномерно распределить рабочие задачи.

Установите количество потоков

По умолчанию среда выполнения Go устанавливает количество потоков равным количеству логических ядер ЦП машины. В то же время его встроенный пакет среды выполнения предоставляет функцию GOMAXPROCS(n int), которая позволяет нам динамически регулировать количество потоков.Обратите внимание, что имя этой функции все в верхнем регистре.Разработчики языка Go настолько капризны .Эта функция вернет количество потоков до модификации.Если параметр n

package main

import "fmt"
import "runtime"

func main() {
    // 读取默认的线程数
    fmt.Println(runtime.GOMAXPROCS(0))
    // 设置线程数为 10
    runtime.GOMAXPROCS(10)
    // 读取当前的线程数
    fmt.Println(runtime.GOMAXPROCS(0))
}

--------
4
10

Чтобы получить текущее количество сопрограмм, вы можете использовать метод NumGoroutine(), предоставляемый пакетом времени выполнения.

package main

import "fmt"
import "time"
import "runtime"

func main() {
	fmt.Println(runtime.NumGoroutine())
	for i:=0;i<10;i++ {
		go func(){
			for {
				time.Sleep(time.Second)
			}
		}()
	}
	fmt.Println(runtime.NumGoroutine())
}

------
1
11

Приложение сопрограммы

В повседневных интернет-приложениях сопрограммы языка Go в основном используются в приложениях HTTP API, системах отправки сообщений, системах чата и т. д.

В приложении HTTP API сервер открывает отдельную сопрограмму для обработки каждого HTTP-запроса. В этом процессе обработки запросов требуется много вызовов ввода-вывода, таких как доступ к базе данных, доступ к кешу, вызов внешних систем и т. д. Сопрограмма будет бездействовать, и после завершения обработки ввода-вывода сопрограмма будет запланирована для повторного запуска. После ответа на запрос ссылка отключается, и на этом жизнь сопрограммы заканчивается.

В системе push-уведомлений срок службы канала клиента очень велик. Большую часть времени канал находится в состоянии ожидания. Клиент будет периодически использовать пульсацию каждые десятки секунд, чтобы сообщить серверу, что вы не хотите отключить меня. На стороне сервера для каждого обслуживания соединения от клиента требуется отдельная сопрограмма. Поскольку ссылки, поддерживаемые системой push-сообщений, как правило, очень простаивают, один сервер часто может легко поддерживать миллионы ссылок.Эти сопрограммы, которые поддерживают ссылки, будут готовы и запланированы для запуска только при поступлении push-сообщения или сообщения пульса.

Система чата также является системой с длинными ссылками, и ее внутренние сообщения гораздо чаще, чем система push-сообщений.Из-за нагрузки на ЦП и сетевую карту количество соединений, которые она может поддерживать, намного меньше, чем у системы. толкающая система. Но принцип аналогичен, линк долго держится сопрограммой, а соединение разрывается и сопрограмма умирает.

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

Читайте больше качественных статей, сканируйте приведенный выше QR-код на WeChat и подписывайтесь на общедоступный аккаунт Code Cave.