При разработке программы go часто необходимо использовать горутины для одновременной обработки задач.Иногда эти горутины не зависят друг от друга, а иногда часто требуется синхронизация и связь между несколькими горутинами. В другом случае основная горутина должна контролировать подпрограмму, к которой она принадлежит.Подводя итог, можно сказать, что синхронизация и связь между несколькими горутинами примерно таковы:
- глобальная общая переменная
- канал связи (модель CSP)
- Пакет контекста
В этой статье используется типичный сценарий синхронизации горутин и подпрограмм, уведомляющих о связи, для прекращения работы, чтобы подробно объяснить управление параллелизмом в golang.
Уведомление нескольких дочерних горутин о прекращении работы
Как инструмент параллелизма языка go, горутина не только обладает высокой производительностью, но и проста в использовании: обычные функции могут выполняться одновременно только с одним ключевым словом go, а горутина занимает очень мало памяти (одна горутина занимает всего 2 КБ памяти), поэтому разработка программы go Многие разработчики часто используют этот инструмент параллелизма. Независимая параллельная задача относительно проста. Вам нужно только использовать ключевое слово go, чтобы изменить функцию, чтобы горутина могла работать напрямую. Однако реальный сценарий параллелизма часто требует синхронизации и синхронизация между сопрограммами, связь и точный контроль начала и окончания подпрограмм, один из типичных сценариев заключается в том, что основной процесс уведомляет все подпрограммы под своим именем о корректном выходе.
Из-за конструкции механизма выхода горутины выход горутины может контролироваться только ею самой, и принудительное завершение горутины извне не допускается. Исключений всего два, то есть конец основной функции или конец краха программы, поэтому для реализации начала и конца основного процесса по управлению дочерней горутиной его необходимо реализовать с помощью других инструменты.
Способы контроля параллелизма
Способы управления параллелизмом можно условно разделить на следующие три категории:
- глобальная общая переменная
- канал связи
- Пакет контекста
глобальная общая переменная
Это самый простой способ реализовать параллелизм управления.Этапы реализации:
- объявить глобальную переменную;
- Все дочерние горутины совместно используют эту переменную и постоянно опрашивают эту переменную на наличие обновлений;
- Изменить глобальную переменную в основном процессе;
- Дочерняя горутина обнаруживает обновление глобальной переменной и выполняет соответствующую логику.
Пример выглядит следующим образом:
package main
import (
"fmt"
"time"
)
func main() {
running := true
f := func() {
for running {
fmt.Println("sub proc running...")
time.Sleep(1 * time.Second)
}
fmt.Println("sub proc exit")
}
go f()
go f()
go f()
time.Sleep(2 * time.Second)
running = false
time.Sleep(3 * time.Second)
fmt.Println("main proc exit")
}
Преимущество глобальных переменных в том, что это просто и удобно, и не требует слишком много сложных операций.Начало и конец всех подпрограмм можно контролировать через переменную, недостаток в том, что функция ограничена.Из-за архитектуре глобальная переменная может быть прочитана и записана только несколько раз., иначе возникнет проблема синхронизации данных.Конечно, эту проблему можно решить и заблокировав глобальные переменные, но это увеличивает сложность.Кроме того, этот метод не подходит для связи между подгорутинами, т.к. могут быть переданы глобальные переменные.Информации очень мало, так же есть факт, что основной процесс не может дождаться выхода всех дочерних горутин, т.к. этот метод может быть только односторонним уведомления, так что этот метод подходит только для сценариев с очень простой логикой и не слишком много параллелизма.Немного сложнее, этот метод немного растянут.
канал связи
Другой более общий и гибкий способ реализации управления параллелизмом — использование каналов для связи.
Прежде всего, давайте разберемся, что такое канал в golang: канал — это основной тип в Go, вы можете думать о нем как о конвейере, через который параллельные основные блоки могут отправлять или получать данные для связи.
Чтобы понять каналы, вы должны сначала узнать модель CSP:
CSP — это аббревиатура от Communicating Sequential Process, которая на китайском языке может называться Communication Sequential Process.Это модель параллельного программирования, предложенная Тони Хоаром в 1977 году. Проще говоря, модель CSP состоит из сущностей (потоков или процессов), которые выполняются одновременно. Сущности общаются, отправляя сообщения. Здесь при отправке сообщений используется канал или канал. Ключом к модели CSP является сосредоточение внимания на канале, а не на объекте, отправляющем сообщение. Язык Go реализует часть теории CSP, горутина соответствует объекту, выполняющемуся одновременно в CSP, канал Он соответствует каналу в CSP. Другими словами, CSP описывает такую модель параллелизма: несколько процессов взаимодействуют с использованием канала, процессы, подключенные к этому каналу, обычно анонимны, а передача сообщений обычно синхронна (в отличие от модели акторов).
Давайте сначала посмотрим на пример кода:
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func consumer(stop <-chan bool) {
for {
select {
case <-stop:
fmt.Println("exit sub goroutine")
return
default:
fmt.Println("running...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
stop := make(chan bool)
var wg sync.WaitGroup
// Spawn example consumers
for i := 0; i < 3; i++ {
wg.Add(1)
go func(stop <-chan bool) {
defer wg.Done()
consumer(stop)
}(stop)
}
waitForSignal()
close(stop)
fmt.Println("stopping all jobs!")
wg.Wait()
}
func waitForSignal() {
sigs := make(chan os.Signal)
signal.Notify(sigs, os.Interrupt)
signal.Notify(sigs, syscall.SIGTERM)
<-sigs
}
Здесь можно изящно дождаться завершения всех подпрограмм, прежде чем основной процесс завершится и завершится.С помощью группы ожидания в синхронизации стандартной библиотеки это способ управления параллелизмом, который может реализовать ожидание нескольких горутин. Официальный документ описывает это следующим образом:
A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.
Проще говоря, его исходный код реализует структуру, подобную счетчику, для записи каждой зарегистрированной в нем сопрограммы, а затем каждой сопрограмме необходимо выйти из нее после завершения своей задачи, а затем ждать в основном процессе, пока все сопрограммы не завершат свои задачи и выход. Шаги для использования:
- Создайте экземпляр группы ожидания wg;
- Когда запускается каждая горутина, вызовите wg.Add(1) для регистрации;
- Перед выходом после завершения каждой задачи Goroutine вызовите WG.DONE () списание.
- В ожидании всех Goroutines, позвоните в WG.Wait (), чтобы заблокировать процесс. После того, как все Goroutines завершили свои задачи и вызовите WG.DONE (), чтобы выйти, метод ожидания () вернет.
Этот пример программы является типичным использованием golang select+channel.Давайте проанализируем это типичное использование немного глубже:
channel
Прежде всего, поймите канал, который можно понимать как конвейер, его основные функциональные точки:
- Очередь для хранения данных
- Блокировать и пробуждать горутины
Реализация канала ориентирована на файлruntime/chan.go, базовая структура данных канала выглядит следующим образом:
type hchan struct {
qcount uint // 队列中数据个数
dataqsiz uint // channel 大小
buf unsafe.Pointer // 存放数据的环形数组
elemsize uint16 // channel 中数据类型的大小
closed uint32 // 表示 channel 是否关闭
elemtype *_type // 元素数据类型
sendx uint // send 的数组索引
recvx uint // recv 的数组索引
recvq waitq // 由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列
sendq waitq // 由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
Из исходного кода видно, что на самом деле это очередь плюс блокировка (облегченная).Сам код не сложный, но включает в себя много деталей контекста, поэтому читать его непросто.Заинтересованные студенты могут взять Я предлагаю начать с вышеизложенного.Начнем с двух суммированных функциональных точек, одна из которых является кольцевым буфером, который используется для хранения данных, а другая представляет собой очередь горутин, которые хранят операции (чтение и запись) на канал.
- buf — это указатель общего назначения, используемый для хранения данных.При просмотре исходного кода сосредоточьтесь на чтении и записи этой переменной.
- recvq — это список горутин, чьи операции чтения заблокированы в канале, а sendq — список горутин, чьи операции записи заблокированы в канале. Реализация списка — sudog, которая на самом деле является инкапсуляцией структуры g. При просмотре исходного кода сосредоточьтесь на том, как заблокировать и разбудить горутину с помощью этих двух переменных.
Поскольку задействовано много исходных кодов, я не буду здесь вдаваться в подробности.
select
Затем есть механизм выбора.Механизм выбора Golang можно понимать как реализацию функций, подобных выбору, опросу и epoll на уровне языка: прослушивание таких событий, как чтение/запись нескольких дескрипторов, как только дескриптор готов (обычно чтение или событие записи), вы можете уведомить соответствующее приложение о событии для его обработки. Механизм выбора Golang заключается в прослушивании нескольких каналов, каждый случай представляет собой событие, это может быть событие чтения или записи, случайный выбор одного для выполнения, вы можете установить значение по умолчанию, его функция: когда несколько отслеживаемых событий заблокированы, Live будет выполнить логику по умолчанию.
Исходный код select находится вruntime/select.go, при просмотре рекомендуется ориентироваться на pollorder и lockorder
- Pollorder сохраняет порядковый номер шкалы, а out-of-order — для случайности при последующем выполнении.
- Блокировка сохраняет адреса каналов во всех случаях.Здесь непрерывная память, соответствующая блокировке, организована в соответствии с размером адреса.Цель сортировки каналов — удалить дублирование и гарантировать, что все каналы не будут заблокированы повторно, когда они будут заблокированы позже.
Поскольку я не изучал эту часть исходного кода очень глубоко, поэтому я могу нажать здесь Если вам интересно, вы можете перейти к исходному коду!
Конкретно для демонстрационного кода: потребителем является конкретный код сопрограммы.Существует только один цикл, который непрерывно опрашивает переменную канала stop, поэтому основной процесс использует stop, чтобы уведомить подпрограмму, когда она должна завершиться. метод close После того, как остановка будет удалена, чтение закрытого канала немедленно вернет нулевое значение типа данных канала, поэтому операция
Фактически метод управления подпрограммами через каналы можно обобщить следующим образом: зацикливание для мониторинга канала, вообще говоря, выбранный канал мониторинга помещается в цикл for для достижения эффекта уведомления подпрограмм. С помощью группы ожидания основной процесс может дождаться корректного выхода всех сопрограмм, прежде чем завершить свою собственную операцию, что обеспечивает элегантный контроль над запуском и окончанием параллелизма горутин по каналам.
Управление связью канала основано на модели CSP.По сравнению с традиционной моделью параллелизма потоков и блокировок, она позволяет избежать большого потребления производительности при блокировке и разблокировке и является более гибким, чем модель Актера.При использовании модели Актера среда и исполнительный блок, отвечающий за коммуникацию, тесно связаны — у каждого Актера есть почтовый ящик. В модели CSP канал является первым объектом, который можно создавать, записывать и читать независимо, что упрощает его расширение.
Убийственный контекст
Контекст обычно переводится как контекст, что является относительно абстрактным понятием. Контекст также часто упоминается при обсуждении техники цепочек. Под ним обычно понимается работающее состояние, сцена и снимок программной единицы, а верхний и нижний слои в переводе очень хорошо объясняют его суть. В языке Go программный модуль также относится к Goroutine.
Перед выполнением каждой горутины она должна сначала узнать текущее состояние выполнения программы, которое обычно инкапсулируется в переменную контекста и передается горутине для выполнения. Контексты стали почти стандартным способом передачи переменных с тем же временем жизни, что и запросы. При сетевом программировании, когда получен запрос сетевого запроса, в горутине, обрабатывающей запрос, может потребоваться продолжить открытие нескольких новых горутин в текущей горутине для получения данных и логической обработки (например, доступ к базам данных, службам RPC и т. д.). .), то есть запрос запроса должен быть обработан в нескольких горутинах. И этим горутинам может потребоваться поделиться некоторой информацией о запросе; в то же время, когда запрос отменяется или истекает время ожидания, все горутины, созданные из этого запроса, также должны быть завершены.
Контекст был введен в стандартную библиотеку после версии go1.7. В версиях Go до версии 1.7 для использования контекста необходимо установить пакет golang.org/x/net/context. Более подробное описание контекста golang см. в официальной документации:context
Контекстный первый тест
Отношения создания и вызова Context являются прогрессивными слой за слоем, что мы обычно называем цепным вызовом, подобно дереву в структуре данных, начиная с корневого узла, и каждый вызов является производным конечным узлом. Сначала сгенерируйте корневой узел, используйте метод context.Background для генерации, а затем сделайте цепочку вызовов для использования различных методов в пакете контекста, всех методов в пакете контекста:
- func Background() Context
- func TODO() Context
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
- func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
- func WithValue(parent Context, key, val interface{}) Context
Здесь мы возьмем только методы WithCancel и WithValue в качестве примеров управления параллелизмом и обменом данными: Нечего говорить, код выше:
package main
import (
"context"
"crypto/md5"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
type favContextKey string
func main() {
wg := &sync.WaitGroup{}
values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
ctx, cancel := context.WithCancel(context.Background())
for _, url := range values {
wg.Add(1)
subCtx := context.WithValue(ctx, favContextKey("url"), url)
go reqURL(subCtx, wg)
}
go func() {
time.Sleep(time.Second * 3)
cancel()
}()
wg.Wait()
fmt.Println("exit main goroutine")
}
func reqURL(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
url, _ := ctx.Value(favContextKey("url")).(string)
for {
select {
case <-ctx.Done():
fmt.Printf("stop getting url:%s\n", url)
return
default:
r, err := http.Get(url)
if r.StatusCode == http.StatusOK && err == nil {
body, _ := ioutil.ReadAll(r.Body)
subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
wg.Add(1)
go showResp(subCtx, wg)
}
r.Body.Close()
//启动子goroutine是为了不阻塞当前goroutine,这里在实际场景中可以去执行其他逻辑,这里为了方便直接sleep一秒
// doSometing()
time.Sleep(time.Second * 1)
}
}
}
func showResp(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("stop showing resp")
return
default:
//子goroutine里一般会处理一些IO任务,如读写数据库或者rpc调用,这里为了方便直接把数据打印
fmt.Println("printing ", ctx.Value(favContextKey("resp")))
time.Sleep(time.Second * 1)
}
}
}
Ранее мы говорили, что Context предназначен для решения сценария, когда несколько горутин обрабатывают запрос, и этим нескольким горутинам необходимо обмениваться некоторой информацией о запросе.Выше приведена демонстрация, которая просто имитирует описанный выше процесс.
Сначала вызовите context.Background() для создания корневого узла, затем вызовите метод withCancel, передайте корневой узел, получите новый дочерний контекст и метод отмены корневого узла (уведомите все дочерние узлы о прекращении работы), обратите внимание здесь : этот метод также возвращает контекст, который является новым дочерним узлом, не является тем же экземпляром, что и исходный входящий корневой узел, но каждый дочерний узел будет сохранять информацию о ссылке от исходного корневого узла к этому узлу для достижения цепочки.
Метод reqURL программы получает URL-адрес, затем запрашивает URL-адрес через http, чтобы получить ответ, а затем запускает подпрограмму в текущей горутине для вывода ответа, а затем начинает с ReqURL для получения конечных узлов из Дерево контекста (каждая цепочка вызывает новый сгенерированный ctx), каждый ctx в середине может передать значение (для достижения связи) через метод WithValue, и каждая дочерняя горутина может получить значение от родительской горутины через метод Value для реализации связь между сопрограммами, каждый дочерний ctx может вызывать Готово Метод определяет, есть ли родительский узел, вызывающий метод отмены, чтобы уведомить дочерний узел о прекращении работы Вызов отмены корневого узла уведомит каждый дочерний узел по ссылке, поэтому реализуется сильный контроль параллелизма.Процесс выглядит следующим образом:Эта демонстрация сочетает в себе группу WaitGroup, упомянутую выше, для достижения элегантного управления параллелизмом и связи. Принцип и использование группы ожидания были проанализированы ранее, поэтому я не буду повторять их здесь. Конечно, реальный сценарий приложения не так прост. обрабатывает Запрос запускает несколько подпрограмм.Большинство горутин имеют дело с задачами с интенсивным вводом-выводом, такими как чтение и запись баз данных или вызовы rpc, а затем продолжают выполнять другую логику в основной горутине.Здесь простейшая обработка выполняется для удобства объяснения.
Контекст широко используется как убийца управления параллелизмом и связи в golang. Некоторые студенты, которые используют go для разработки http-сервисов, узнают, читали ли они исходный код этих многочисленных веб-фреймворков. Контекст можно увидеть повсюду в веб-фреймворке, поскольку обработка HTTP-запросов является типичными связанными процессами и одновременными сценариями, поэтому многие веб-фреймворки используют контекст для реализации логики связанных вызовов. Если вам интересно, вы можете прочитать исходный код пакета контекста, и вы обнаружите, что реализация Context на самом деле реализована путем объединения блокировок Mutex и каналов.На самом деле, многие расширенные компоненты параллелизма и синхронизации неотделимы от одного и того же. вещь, и они собраны через структуру данных самого низкого уровня Upstream, если вы знаете самые основные концепции, восходящую архитектуру можно увидеть с первого взгляда.
спецификация использования контекста
Наконец, хотя контекст является артефактом, разработчики должны следовать Основному закону при его использовании.Ниже приведены некоторые спецификации для использования контекста:
-
Не храните контексты внутри типа структуры, вместо этого явно передавайте контекст каждой функции, которая в нем нуждается.Контекст должен быть первым параметром, обычно называемым ctx; В качестве первого параметра необходимо использовать переменную Context, обычно называемую ctx;
-
Не передавайте nil Context, даже если это разрешено функцией Передавайте context.TODO, если вы не уверены в том, какой Context использовать При передаче context.TODO;
-
Используйте значения контекста только для данных в области запроса, которые передаются процессам и API, а не для передачи необязательных параметров функциям для передачи некоторых необязательных параметров;
-
Один и тот же контекст может быть передан функциям, работающим в разных горутинах, контексты безопасны для одновременного использования несколькими горутинами, один и тот же контекст может быть передан в разные горутины, контекст безопасен в нескольких горутинах.
Ссылка на ссылку
- [1] deepmagazine.com/post/golang…
- [2] woohoo.fly snow.org/2017/05/12/…
- [3] golang.org/pkg/context…
- [4]Уууу, Мо Се, что/2017/05/05/…
Эта статья написанаПан Цзяньфэнсоздавать, использоватьАтрибуция Creative Commons 4.0Международное лицензионное соглашение о лицензировании
Статьи на этом сайте являются оригинальными или переведены этим сайтом, если не указана перепечатка/источник, обязательно подпишитесь перед перепечаткой
Последнее редактирование: 23.06.2018 15:29