Как написать высокопроизводительный код с помощью Go-процедур в Golang

задняя часть Go Программа перевода самородков React.js

Как писать высокопроизводительный код с помощью Go-Routines в Golang

Чтобы писать быстрый код на Голанге, вам нужно посмотреть видео Роба Пайка —Go-Routines.

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

Что ж, есть три концепции, которые вам нужно полностью понять, чтобы писать быстрые программы на Golang, а именно Go-Routines, Closures и Pipes.

Go-Routines

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

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

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

Любой блок кода по умолчанию будет использовать только одно ядро ​​​​ЦП, если в блоке кода не объявлены подпрограммы. Итак, если у вас есть 70-строчная программа без подпрограмм. Он будет выполняться одним ядром. Как и в нашем примере, ядро ​​может выполнять только одну инструкцию за раз. Поэтому, если вы хотите ускорить свое приложение, вы должны использовать все ядра процессора.

Итак, что такое рутина. Как объявить это в Голанге?

Давайте рассмотрим простую программу и введем в нее go-рутину.

Пример программы 1

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

package main

import "fmt"

func main() {
    fmt.Println("Box 1")
    fmt.Println("Box 2")
    fmt.Println("Box 3")
    fmt.Println("Box 4")
    fmt.Println("Box 5")
    fmt.Println("Box 6")
    fmt.Println("Box 7")
    fmt.Println("Box 8")
    fmt.Println("Box 9")
    fmt.Println("Box 10")
}

Поскольку go-routines не объявлены, приведенный выше код выводит следующий результат.

вывод

Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Box 7
Box 8
Box 9
Box 10

Итак, если мы хотим использовать дополнительные ядра процессора в процессе перемещения ящика, нам нужно объявить go-routine.

Пример программы с Go-Routines 2

package main

import "fmt"

func main() {
    go func() {
        fmt.Println("Box 1")
        fmt.Println("Box 2")
        fmt.Println("Box 3")
    }()
    fmt.Println("Box 4")
    fmt.Println("Box 5")
    fmt.Println("Box 6")
    fmt.Println("Box 7")
    fmt.Println("Box 8")
    fmt.Println("Box 9")
    fmt.Println("Box 10")
}

Здесь объявлена ​​подпрограмма, которая содержит первые три оператора печати. Это означает, что ядро, которое обрабатывает основную функцию, выполняет только 4-10 строк операторов. На выполнение блоков из 1-3 строк отводится другое ядро.

вывод

Box 4
Box 5
Box 6
Box 1
Box 7
Box 8
Box 2
Box 9
Box 3
Box 10

Результат анализа

В этом коде два ядра ЦП работают одновременно, пытаясь выполнить свои задачи, и оба ядра полагаются на стандартный вывод для выполнения своих соответствующих задач (поскольку в этом примере мы использовали операторы печати).
Другими словами, стандартный вывод (работающий на одном из собственных ядер) может одновременно принимать только одну задачу. Итак, то, что вы видите здесь, является случайным порядком, который зависит от stdout, чтобы решить, какие задачи принять core1 core2.

Как объявить go-routine?

Для того, чтобы объявить нашу собственную go-рутину, нам нужно сделать три вещи.

  1. Создаем анонимную функцию
  2. Мы называем эту анонимную функцию
  3. Мы используем ключевое слово «go» для вызова

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

func() {
    fmt.Println("Box 1")
    fmt.Println("Box 2")
    fmt.Println("Box 3")
}

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

func() {
  fmt.Println("Box 1")
  fmt.Println("Box 2")
  fmt.Println("Box 3")
} ()

Шаг 3 можно выполнить с помощью ключевого слова go. Что такое ключевое слово go, оно может объявить функциональный блок как блок кода, который может выполняться независимо. Таким образом, блок кода может выполняться другими бездействующими ядрами системы.

#Деталь 1: Что происходит, когда go-процедур больше, чем ядер?

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

#Попробуйте сами 1: Попробуйте удалить ключевое слово go в примере программы 2. Каков результат?

Ответ: Результат примера программы 2 точно такой же, как и у 1.

#Попробуйте сами 2: Увеличьте количество операторов в анонимной функции с 3 до 8. Результат изменился?

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

Теперь мы знаем, что такое рутины. Далее посмотримЗакрытие.

Если вы раньше не знали о замыканиях в Python или JavaScript, вы можете изучить их сейчас в Golang. Учащиеся могут пропустить эту часть, чтобы сэкономить время, потому что замыкания в Golang такие же, как в Python или JavaScript.

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

  1. Функции имеют доступ только к двум типам переменных: глобальным переменным и локальным переменным (переменным внутри функций).
  2. Ни одна функция не может получить доступ к переменным, объявленным в других функциях.
  3. После завершения выполнения функции все переменные, объявленные в этой функции, исчезают.

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

  1. Функции могут быть объявлены внутри функций.
  2. Функции могут возвращать функции.

Следствие № 1. Поскольку функции могут быть объявлены внутри функций, вложенные цепочки объявлений одной функции внутри другой функции являются распространенным побочным продуктом этой гибкости.

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

Так что же такое закрытие?

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

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

package main

import "fmt"

var zero int = 0

func main() {
    var one int = 1
    child := func() {
        var two int = 3
        fmt.Println(zero)
        fmt.Println(one)
        fmt.Println(two)
        fmt.Println(three) // causes compilation Error
    }
    child()
    var three int = 2
}

Есть две функции - основная функция и подфункция, где подфункция определена в основной функции. Доступ к подфункциям

  1. нулевая переменная - это глобальная переменная
  2. одна переменная - свойство Closure - одна принадлежит основной функции, она находится в основной функции и определяется перед дочерней функцией.
  3. две переменные - это локальная переменная подфункции

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

То же, что вложение.

package main

import "fmt"

var global func()

func closure() {
    var A int = 1
    func() {
        var B int = 2
        func() {
            var C int = 3
            global = func() {
                fmt.Println(A, B, C)
                fmt.Println(D, E, F) // causes compilation error
            }
            var D int = 4
        }()
        var E int = 5
    }()
    var F int = 6
}
func main() {
    closure()
    global()
}

Если мы подумаем о том, чтобы связать самую внутреннюю функцию с глобальной переменной «global».

  1. Он имеет доступ к переменным A, B и C независимо от замыканий.
  2. Он не может получить доступ к переменным D, E, F, поскольку они не были определены ранее.

Примечание. Даже после выполнения замыкания его локальные переменные не будут уничтожены. К ним по-прежнему можно получить доступ через имя функции с именем «глобальный».

Давайте представимChannels.

Каналы — это ресурс для связи между go-процедурами, и они могут быть любого типа.

ch := make(chan string)

Мы определяем канал типа string с именем ch. Через этот канал могут обмениваться данными только переменные строкового типа.

ch <- "Hi"

Это все для отправки сообщения на канал.

msg := <- ch

Вот как можно получать сообщения с канала.

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

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

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

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

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

ch := make(chan string, 100)

Буферизованные каналы являются полублокирующими по своей природе.

Например, ch — буферизованный символьный канал размером 100. Это означает, что первые 100 отправленных ему сообщений не блокируются. Последний будет заблокирован.

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

Таким образом, поведение буферизованного канала как небуферизованного канала зависит от того, свободен ли буфер во время выполнения.

Закрытие каналов

close(ch)

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

msg, ok := <- ch
if !ok {
  fmt.Println("Channel closed")
}

Пишите быстрый код с Golang

Теперь точки знаний, которые мы рассмотрели, охватили go-рутины, замыкания и каналы. Учитывая, что алгоритм перемещения ящиков уже эффективен, мы можем приступить к разработке общего решения проблемы с помощью Golang, просто сосредоточимся на том, чтобы нанять нужное количество людей для выполнения задачи.

Давайте подробнее рассмотрим нашу проблему и переопределим ее.

У нас есть 100 ящиков, которые нужно переместить из одной комнаты в другую. Важно отметить, что нет никакой разницы в работе, связанной с перемещением ящика 1 и перемещением ящика 2. Итак, мы можем определить метод для перемещения ящика, переменная «i» представляет перемещаемый ящик. Методы называются «задачами», а количество ящиков обозначается «N». Любой курс «Основы компьютерного программирования 101» научит вас, как решить эту проблему: написать цикл for, который вызывает «задачу» N раз, в результате чего вычисления занимают одно ядро, а доступные ядра в системе аппаратная проблема, в зависимости от марки системы, модели и дизайна. Поэтому, как разработчики программного обеспечения, мы исключаем из рассмотрения аппаратное обеспечение и говорим о подпрограммах, а не о ядре. Большее количество ядер поддерживает больше go-процедур, давайте предположим, что «R» — это количество go-процедур, поддерживаемых нашей базовой системой «X».

К вашему сведению: количество «X» ядер может обрабатывать более «X» количества подпрограмм. Количество go-процедур (R/X), поддерживаемых одним ядром, зависит от обработки, связанной с go-процедурами, и платформы, на которой они выполняются. Например, если все go-процедуры включают только блокирующие вызовы, такие как сетевой ввод-вывод или дисковый ввод-вывод, для их обработки достаточно одного ядра. Это верно, потому что каждая подпрограмма — это скорее ожидание, чем операции. Следовательно, одно ядро ​​может обрабатывать переключение контекста между всеми go-процедурами.

Итак, общее определение нашей проблемы таково.

Назначьте «N» задач на «R» подпрограмм, где все задачи одинаковы.

Если N≤R, мы можем решить это следующим образом.

package main

import "fmt"

var N int = 100

func Task(i int) {
    fmt.Println("Box", i)
}
func main() {
    ack := make(chan bool, N) // Acknowledgement channel
    for i := 0; i < N; i++ {
        go func(arg int) { // Point #1
            Task(arg)
            ack <- true // Point #2
        }(i) // Point #3
    }

    for i := 0; i < N; i++ {
        <-ack // Point #2
    }
}

Объясните, что мы делаем...

  1. Мы создаем рутину для каждой задачи. Наша система может одновременно поддерживать подпрограммы "R". Пока N≤R, мы делаем это безопасно.
  2. Мы удостоверяемся, что основная функция ожидает завершения всех go-процедур перед возвратом. Мы сообщаем о его завершении, ожидая канала подтверждения («ack»), используемого всеми go-процедурами (через свойство замыкания).
  3. Вместо того, чтобы передавать количество циклов «i» в качестве аргумента «arg» программе goСвойство закрытияСсылка на него непосредственно в go-рутине.

С другой стороны, если N>R, приведенное выше решение проблематично. Он создает подпрограммы, с которыми система не может справиться. Все ядра, пытающиеся запустить больше подпрограмм, чем могут, в конечном итоге будут тратить больше времени на переключение контекста, чем на выполнение программ (обычно это называется дрожанием). Накладные расходы на переключение контекста становятся более заметными по мере увеличения разницы в числах между N и R. Поэтому всегда ограничивайте количество go-процедур до R. И назначьте N задач R go-процедурам.

Ниже мы представляемworkersфункция

var R int = 100
func Workers(task func(int)) chan int { // Point #4
 input := make(chan int)                // Point #1
 for i := 0; i < R; i++ {               // Point #1
   go func() {
     for {
       v, ok := <-input                   // Point #2
       if ok {
         task(v)                           // Point #4
       } else {
         return                            // Point #2
       }
     }
   }()
 }
 return input                          // Point #3
}
  1. Создайте пул подпрограмм "R". Ни больше, ни меньше, все слушатели «входного» канала ссылаются через свойство closure.
  2. Создайте подпрограммы, которые проверяют, закрыт ли канал, проверяя параметр ok в каждом цикле, и убивают себя, если канал закрыт.
  3. Возвращает входной канал, позволяющий вызывающей функции назначать задачи пулу.
  4. Используйте параметр «task», чтобы разрешить вызывающей функции определять тело go-процедур.

использовать

func main() {
ack := make(chan bool, N)
workers := Workers(func(a int) {     // Point #2
  Task(a)
  ack <- true                        // Point #1
 })
for i := 0; i < N; i++ {
  workers <- i
 }
for i := 0; i < N; i++ {             // Point #3
  <-ack
 }
}

Добавляя оператор (точка № 1) в рабочий метод (точка № 2), свойство замыкания разумно добавляет вызов канала подтверждения в определении параметра задачи, мы используем этот цикл (точка № 3), чтобы сделать основную функцию Существует механизм, позволяющий узнать, выполнили ли все подпрограммы в пуле свои задачи. Вся логика, связанная с go-процедурами, должна содержаться в самом воркере, так как они создаются там. Основная функция не должна знать подробности того, как работают внутренние рабочие функции.

Поэтому для достижения полной абстракции мы вводим функцию «climax», которая запускается только после завершения всех подпрограмм в пуле. Это достигается за счет настройки другой go-процедуры, которая отдельно проверяет состояние пула, кроме того, для разных задач требуются разные типы типов каналов. Один и тот же int cannel нельзя использовать во всех случаях, поэтому для написания более общей рабочей функции мы будем использоватьпустой тип интерфейсаПереопределить рабочую функцию.

package main

import "fmt"

var N int = 100
var R int = 100

func Task(i int) {
    fmt.Println("Box", i)
}
func Workers(task func(interface{}), climax func()) chan interface{} {
    input := make(chan interface{})
    ack := make(chan bool)
    for i := 0; i < R; i++ {
        go func() {
            for {
                v, ok := <-input
                if ok {
                    task(v)
                    ack <- true
                } else {
                    return
                }
            }
        }()
    }
    go func() {
        for i := 0; i < R; i++ {
            <-ack
        }
        climax()
    }()
    return input
}
func main() {

    exit := make(chan bool)

    workers := Workers(func(a interface{}) {
        Task(a.(int))
    }, func() {
        exit <- true
    })

    for i := 0; i < N; i++ {
        workers <- i
    }
    close(workers)

    <-exit
}

Видите ли, я пытался показать мощь Голанга. Мы также рассмотрели, как писать производительный код на Golang.

Посмотрите видео Роба Пайка Go-Routines и отлично проведите время с Golang.

до скорого...

благодарныйPrateek Nischal.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,React,внешний интерфейс,задняя часть,продукт,дизайнЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.