Поведение канала в Golang

задняя часть Go алгоритм

Введение

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

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

Чтобы понять, как работают сигналы, мы должны понять следующие три свойства:

  • Гарантия доставки
  • условие
  • С данными или без

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

Гарантия доставки

Гарантии доставки основаны на вопросе: «Нужно ли мне гарантировать, что сигнал, отправленный определенной горутиной, был получен?»

Другими словами, мы можем привести пример листинга 1:

Листинг 1

01 go func() {
02     p := <-ch // Receive
03 }()
04
05 ch <- "paper" // Send

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

Основываясь на ответе на этот вопрос, вы будете знать, какой из двух типов каналов использовать:небуферизованныйилибуферизованный. Каждый канал обеспечивает различное поведение в отношении гарантий доставки.

фигура 1

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

условие

Поведение канала напрямую зависит от его текущего состояния. Состояние канала:nil,openилиclosed.

В листинге 2 ниже показано, как объявить или перевести канал в эти три состояния.

Листинг 2

// ** nil channel

// A channel is in a nil state when it is declared to its zero value
var ch chan string

// A channel can be placed in a nil state by explicitly setting it to nil.
ch = nil


// ** open channel

// A channel is in a open state when it’s made using the built-in function make.
ch := make(chan string)    


// ** closed channel

// A channel is in a closed state when it’s closed using the built-in function close.
close(ch)

статус определяет, какsend(отправить) иreceive(Получить) Поведение действия.

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

фигура 2

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

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

С данными и без

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

Сигнал, имеющий данные в канале, выполняет отправку.

Листинг 3

01 ch <- "paper"

Когда у вашего сигнала есть данные, это обычно происходит потому, что:

  • Горутине предлагается начать новую задачу.
  • Горутина сообщает результат.

Нет данных сигнализируется закрытием канала.

Листинг 4

01 close(ch)

Когда сигнал не имеет данных, это обычно происходит потому, что:

  • Горутине приказано прекратить то, что она делает.
  • Горутина сообщает, что они выполнены безрезультатно.
  • Горутина сообщает, что она завершила обработку и завершает работу.

Есть исключения из этих правил, но это основные варианты использования, и мы сосредоточимся на них в этой статье. Я считаю эти исключения из правил исходным запахом кода.

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

с сигналом данных

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

Рисунок 3: С сигналом данных

Три варианта канала:Unbuffered, Buffered >1илиBuffered =1.

  • гарантировано

  • ОдиннебуферизованныйКанал дает вам уверенность в том, что отправляемый сигнал был получен.

    • Потому что прием сигнала происходит до завершения передачи сигнала.
  • Без гарантии

  • Одинsize > 1Буферизованный канал не гарантирует, что переданный сигнал был принят.

  • Потому что сигнализация происходит до того, как сигнализация сделана.

  • Гарантия задержки

  • Одинsize = 1Буферизованный канал обеспечивает гарантированную задержку. Это гарантирует, что ранее отправленные сигналы были получены.

  • Потому что первый полученный сигнал происходит до второго завершенного сигнала отправки.

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

нет сигнала данных

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

Рисунок 4: Нет сигнала данных

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

В большинстве случаев вы хотите использовать стандартную библиотекуcontextпакет для реализации сигнала без данных.contextПакет использует небуферизованный канал для доставки сигналов и встроенных функций.closeНе отправлять сигнал данных.

Если вы решите использовать свой собственный канал вместоcontextпакет для отмены, ваш канал должен бытьchan struct{}тип, который представляет собой идиоматический способ обозначения сигнала только для сигнализации.

Сцены

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

Сигналы данных - Гарантированные - Небуферизованные каналы

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

Сценарий 1 — Ожидание задачи

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

Листинг 5

Адрес онлайн-демонстрации

01 func waitForTask() {
02     ch := make(chan string)
03
04     go func() {
05         p := <-ch
06
07         // Employee performs work here.
08
09         // Employee is done and free to go.
10     }()
11
12     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
13
14     ch <- "paper"
15 }

В строке 2 листинга 5 создается небуферизованный канал со свойствамиstringДанные будут отправлены вместе с сигналом. В строке 4 нанимают сотрудника и просят дождаться вашего сигнала [в строке 5], прежде чем приступить к работе. Строка 5 — это канал получения, в результате чего сотрудник блокируется до тех пор, пока вы не отправите отчет. Как только отчет будет получен сотрудником, сотрудник выполнит работу и может уйти, когда она будет сделана.

Вы как менеджер работаете одновременно со своими сотрудниками. Итак, после того, как вы наняли сотрудника в строке 4, вы узнаете, что вам нужно сделать, чтобы разблокировать и подать сигнал сотруднику (строка 12). Стоит отметить, что неизвестно, сколько времени займет подготовка этой статьи.

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

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

Сценарий 2 — Ожидание результатов

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

Листинг 6 Адрес онлайн-демонстрации

01 func waitForResult() {
02     ch := make(chan string)
03
04     go func() {
05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
06
07         ch <- "paper"
08
09         // Employee is done and free to go.
10     }()
11
12     p := <-ch
13 }

затрат и выгод

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

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

Передача данных – Нет гарантии – Буферизированные каналы > 1

Сценарий 1. Разветвление

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

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

Листинг 7 демонстрационный адрес

01 func fanOut() {
02     emps := 20
03     ch := make(chan string, emps)
04
05     for e := 0; e < emps; e++ {
06         go func() {
07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
08             ch <- "paper"
09         }()
10     }
11
12     for emps > 0 {
13         p := <-ch
14         fmt.Println(p)
15         emps--
16     }
17 }

В строке 3 листинга 7 создается буферизованный канал со свойствамиstringДанные будут отправлены вместе с сигналом. В это время, начиная с объявления в строке 2empsбудет создан канал с 20 буферами.

Между линиями 5 и 10 нанимают 20 сотрудников, и они сразу же приступают к работе. В строке 7 вы не знаете, сколько времени займет каждый сотрудник. На этот раз в строке 8 сотрудник отправляет свой отчет, но на этот раз отправка не блокируется в ожидании получения. Из-за места в ящике для каждого сотрудника отправка по каналу конкурирует только с другими сотрудниками, которые хотят отправить свои отчеты одновременно.

Код между строками 12 и 16 полностью ваш. Здесь вы ждете, пока 20 сотрудников сделают свою работу и пришлют отчет. В строке 12 вы находитесь в петле, в строке 13 вы заблокированы на канале, ожидающем получения вашего отчета. Как только отчет получен, он распечатывается в 14 и используется локальная переменная счетчика, чтобы показать, что сотрудник завершил свою работу.

Сценарий 2 — падение

Режим Drop позволяет вам бросить работу, когда ваши сотрудники работают на полную мощность. Преимущество этого заключается в том, что вы продолжаете принимать работу клиента и никогда не применяете давление или приемлемую задержку работы. Ключевым моментом здесь является то, чтобы знать, когда вы работаете на полную мощность, чтобы не брать на себя слишком много работы, которую вы пытаетесь выполнить. Часто интеграционные тесты или метрики могут помочь вам определить это число.

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

Листинг 8 демонстрационный адрес

01 func selectDrop() {
02     const cap = 5
03     ch := make(chan string, cap)
04
05     go func() {
06         for p := range ch {
07             fmt.Println("employee : received :", p)
08         }
09     }()
10
11     const work = 20
12     for w := 0; w < work; w++ {
13         select {
14             case ch <- "paper":
15                 fmt.Println("manager : send ack")
16             default:
17                 fmt.Println("manager : drop")
18         }
19     }
20
21     close(ch)
22 }

В строке 3 листинга 8 создается буферизованный канал с атрибутами,stringДанные будут отправлены вместе с сигналом. Поскольку объявление в строке 2capConstant, то создается канал с 5 буферами.

В строках с 5 по 9 для выполнения работы нанимается один сотрудник,for rangeИспользуется для циклического приема каналов. Каждый раз при получении отчета он обрабатывается в строке 7.

Между строками 11 и 19 вы пытаетесь отправить сотруднику отчет из 20 пунктов. В это времяselectоператор находится первым в строке 14caseиспользуется для выполнения отправки. потому чтоdefaultпункт используется в строке 16selectутверждение. Если отправка заблокирована из-за того, что в буфере не осталось места, отправка отбрасывается выполнением строки 17.

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

затрат и выгод

Буферизованный канал с буфером больше 1 не гарантирует, что передаваемый сигнал будет принят. Есть преимущества в том, чтобы оставить гарантии, когда задержка в общении между двумя горутинами минимальна или отсутствует. существуетРазветвлениеСценарий, в нем есть буферизованное пространство для отчетов, которые будут отправлены сотрудникам. существуетDropСценарий: измеряется емкость буфера. Если емкость заполнена, задание отбрасывается, чтобы его можно было продолжить.

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

С сигналом данных - задержка гарантирована - буфер 1 канал

Сценарий 1 — Ожидание задачи

Листинг 9 демонстрационный адрес

01 func waitForTasks() {
02     ch := make(chan string, 1)
03
04     go func() {
05         for p := range ch {
06             fmt.Println("employee : working :", p)
07         }
08     }()
09
10     const work = 10
11     for w := 0; w < work; w++ {
12         ch <- "paper"
13     }
14
15     close(ch)
16 }

В строке 2 листинга 9 создается канал размером с буфер с атрибутомstringДанные будут отправлены вместе с сигналом. Между строками 4 и 8 нанимается сотрудник для выполнения работы.for rangeИспользуется для циклического приема каналов. В строке 6 он обрабатывается каждый раз при получении отчета.

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

В строке 15 в конце встроенная функцияcloseПризван закрыть канал, который будет посылать сигнал отсутствия данных сотрудникам о том, что их работа сделана и они могут уйти. Тем не менее, последняя вакансия, которую вы отправите, будет вfor rangeполучено до прерывания.

Нет сигнала данных – контекст

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

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

Листинг 10 демонстрационный адрес

01 func withTimeout() {
02     duration := 50 * time.Millisecond
03
04     ctx, cancel := context.WithTimeout(context.Background(), duration)
05     defer cancel()
06
07     ch := make(chan string, 1)
08
09     go func() {
10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
11         ch <- "paper"
12     }()
13
14     select {
15     case p := <-ch:
16         fmt.Println("work complete", p)
17
18     case <-ctx.Done():
19         fmt.Println("moving on")
20     }
21 }

В строке 2 листинга 10 объявлено значение времени, которое показывает, сколько времени потребуется сотруднику для выполнения своей работы. Это значение используется в строке 4 для создания тайм-аута 50 мс.context.Contextстоимость.contextупаковкаWithTimeoutфункция возвращаетContextзначение и функция отмены.

contextПакет создает горутину, которая по истечении заданного времени отключается сContextНебуферизованные каналы, связанные со значением. Что бы ни случилось, ты должен нести ответственность за звонок.cancelфункция. Это очиститContextсозданные вещи.cancelЭто нормально, когда вам звонят больше одного раза.

В строке 5, как только функция прерывается,cancelФункция выполняется отложенным. В строке 7 создается буферизованный канал, по которому сотрудники отправляют вам результаты своей работы. В строках 09 и 12 наемник сразу запускает сотрудника на работу, и вам не нужно указывать, сколько времени требуется сотруднику для выполнения своей работы.

Между строками 14 и 20 вы используетеselectЗаявления принимать по двум каналам. В строке 15 получения вы ждете, пока сотрудник отправит свои результаты. Получение в строке 18, вы ждете, чтобы увидетьcontextСигнализирует ли пакет, что 50 мс истекли. Независимо от того, какой сигнал вы получите первым, он будет обработан.

Важным аспектом этого алгоритма является использование буферизованного канала. Если сотрудник не закончит работу вовремя, вы уйдете, не предупредив сотрудника. Для работника на 11 строке он всегда будет присылать свой отчет, там вы его или не получите, он слепой. Если вы используете небуферизованный канал, если вы уйдете, сотрудник будет продолжать блокировать его, пытаясь отправить вам отчет. Это может привести к утечке горутин. Таким образом, для предотвращения этой проблемы используется буферизованный канал.

Суммировать

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

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

В качестве резюме просмотрите эти пункты, когда и как думать о каналах и эффективно их использовать:

языковой механизм

  • Используйте каналы для организации и совместной работы горутин:

    • Сосредоточьтесь на свойствах сигнала, а не на совместном использовании данных
    • С сигналом данных и без него
    • Спросите, что они используют для синхронизации доступа к общим данным
      • Есть случаи, когда канал мог быть проще для этой проблемы, но исходный вопрос был.
  • Небуферизованные каналы:

    • Получение происходит перед отправкой
    • Преимущество: 100% гарантированный прием сигнала
    • Стоимость: неизвестная задержка, неизвестно, когда будет получен сигнал.
  • Есть буферизованные каналы:

    • Отправка происходит до получения.
    • Преимущество: уменьшена задержка блокировки между сигналами.
    • Стоимость: Нет гарантии, когда сигнал будет получен.
      • Чем больше буфер, тем меньше гарантий.
      • Буферизация до 1 дает вам гарантию отложенной доставки.
  • Закрытые каналы:

    • Закрытие происходит перед получением (как буферизация).
    • Нет сигнала данных.
    • Идеально подходит для подавления или отключения сигнала.
  • нулевые каналы:

    • Блок отправки и приема.
    • Закрыть сигнал.
    • Идеально подходит для ограничения скорости или коротких остановок.

философия дизайна

  • Если любая данная отправка на канале может привести к блокировке отправляющей горутины:

    • Буферизованные каналы больше 1 не допускаются.
      • Должна быть причина/мера для буферизации больше 1.
    • Необходимо знать, что происходит, когда отправляющая горутина блокируется.
  • Если какая-либо данная отправка на канале не приводит к блокировке отправки:

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

    • Думая о буферизации, не думайте о производительности.
    • Буферизация может помочь уменьшить блокирующие задержки между сигналами.
      • Снижение задержки блокировки до 0 не обязательно означает повышение пропускной способности.
      • Если буфер может дать вам достаточную пропускную способность, сохраните его.
      • Проблемы с буферизацией больше 1 нужно измерять размером.
      • Найдите наименьший возможный буфер, обеспечивающий достаточную пропускную способность