Показаны сообщения с ярлыком многопоточность в Go. Показать все сообщения
Показаны сообщения с ярлыком многопоточность в Go. Показать все сообщения

пятница, 21 июня 2019 г.

Спецификация Go: go утверждения (go statements)

Утверждение "go" запускает выполнение вызова функции как независимый конкурентый поток управления, или goroutine, в том же адресном пространстве.

GoStmt = "go" Expression .

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

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

go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)


Читайте также:


четверг, 28 февраля 2019 г.

Go Code Review Comments: Время жизни goroutine

Когда вы порождаете go-процедуры, делайте четким и ясным время, когда они завершаются (или не завершаются).

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

Даже когда go-процедуры не приводят к утечкам памяти, оставление их в подвешенном состоянии, когда они больше не нужны, может вызвать другие тонкие и трудно диагностируемые проблемы. Отправка panic по закрытым каналам. Изменение находящихся в использовании вводимых данных «после того, как результат не нужен», может привести к "гонкам за данные" (data races). А оставление go-процедур в подвешенном состоянии в течение сколь угодно долгого времени может привести к непредсказуемому использованию памяти.

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


Читайте также:


суббота, 16 февраля 2019 г.

Go FAQ: Почему не существует идентификаторов go-процедур (goroutine ID)?

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

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

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

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

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


Читайте также:


Go FAQ: Как можно контролировать количество процессоров, используемых в Go программе?

Количество процессоров, доступных одновременно для выполнения go-процедур (goroutines), управляется переменной среды оболочки GOMAXPROCS, значение по умолчанию - количество доступных процессорных ядер. Поэтому программы с возможностью параллельного выполнения должны получить все процессоры по умолчанию на многопроцессорной машине. Чтобы изменить количество параллельных процессоров, установите переменную среды или используйте одноименную функцию runtime пакета для настройки использования различного количества потоков во время выполнения. Установка его в 1 исключает возможность истинного параллелизма, заставляя независимые go-процедуры (goroutines) выполняться по очереди.

Среда выполнения (runtime) может выделить больше потоков, чем значение GOMAXPROCS для обслуживания нескольких невыполненных запросов ввода/вывода. GOMAXPROCS влияет только на количество go-процедур (goroutines), которые могут реально выполниться сразу; произвольно больше может быть заблокировано в системных вызовах.

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


Читайте также:


Go FAQ: Почему программа не работает быстрее с большим количеством процессоров?

Будет ли программа работать быстрее с большим количеством процессоров, зависит от проблемы, которую она решает. Язык Go предоставляет конкурентные примитивы, такие как go-процедуры (goroutines) и каналы, но конкурентность позволяет параллелизм только когда основная проблема внутренне параллельна. Проблемы, которые являются последовательными, не могут быть ускорены путем добавления большего количества процессоров, в то время как те, которые могут быть разбиты на части, которые могут выполняться параллельно, могут быть ускорены, иногда значительно.

Иногда добавление большего количества процессоров может замедлить работу программы. На практике у программ, которые проводят больше времени в синхронизировании или общении, чем в выполнении полезных вычислений, может возникнуть снижение производительности при использовании несколько потоков операционной системы. Это потому, что передача данных между потоками включает в себя переключение контекстов, которое имеют значительную стоимость, и эта стоимость может увеличиться с большим количеством процессоров. Например, пример первичного сита из спецификации Go не имеет существенного параллелизма, хотя он запускает много goroutines; увеличение количества потоков (процессоров) скорее замедлит его, вместо того чтобы ускорить.


Читайте также:


пятница, 15 февраля 2019 г.

Go FAQ: Какие операции атомарные? Как насчет мьютексов?

Описание атомарности операций в Go можно найти в описании модели памяти Go.

Низкоуровневая синхронизация и атомарные примитивы доступны в sync и sync/atomic пакетах. Эти пакеты хороши для простых задач, таких как увеличение значения счетчика ссылок или гарантирование небольшого взаимного исключения.

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

Не общайтесь, разделяя память. Вместо этого делитесь памятью, общаясь.

Большие параллельные программы, вероятно, могут использовать преимущества обоих этих наборов инструментов.


Читайте также:


суббота, 9 февраля 2019 г.

Go FAQ: Почему операции с map (картой) не определены как атомарные?

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

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

Доступ к карте небезопасен только когда происходят обновления. Пока все программы только читают - ищут элементы на карте, в том числе итерируя с помощью цикла for range, и не изменяя карты посредством присваивания к элементам карты или выполнения удаления - такой процесс (чтение) безопасен и при одновременном доступе к карте без синхронизации.

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


Читайте также:


пятница, 8 февраля 2019 г.

Go FAQ: Почему go-процедуры (goroutines) вместо потоков?

go-процедуры (goroutines) являются частью облегчения использования параллелизма. Это идея, которая существует уже довольно давно - мультиплексировать независимо исполняемые функции - сопрограммы (coroutines) - на множество потоков. Когда coroutine блокируется, например, путем вызова системного вызова блокировки, runtime (окружение среды исполнения) автоматически перемещает другие сопрограммы в том же потоке операционной системы в другой, работающий поток, чтобы они не были заблокированы. Программист не видит ничего из этого, в чем и заключается вся суть - механизм работает сам без помощи разработчика. Результат, который называют goroutines (go-процедуры), может быть очень дешевым: у него мало накладных расходов за пределами памяти для стека, который составляет всего несколько килобайт.

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


Читайте также:


четверг, 7 февраля 2019 г.

Go FAQ: Почему параллелизм в Go построен на идеях CSP?

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

Одна из самых успешных моделей для обеспечения высокоуровневой языковой поддержки параллелизма основана на взаимодействующих последовательных процессах Хора (Hoare's Communicating Sequential Processes), или CSP. Occam и Erlang - два хорошо известных языка, которые используют CSP. Примитивы параллелизма Go происходят из другой части родословной, чей основной вклад - мощное понятие каналов как первоклассных объектов. Опыт работы создателей Go с несколькими более ранними языками показал, что модель CSP хорошо вписывается в процедурные языковые рамки.


Читайте также:


четверг, 31 января 2019 г.

Эффективный Go: текущий буфер

Инструменты параллельного программирования могут облегчить выражение даже не-многопоточных идей. Вот пример, извлеченный из RPC пакета. Клиентская программа выполняет цикл получения данных из какого-либо источника, возможно сети. Чтобы избежать выделения и освобождения буферов, она сохраняет свободный список, и использует буферизованный канал для его представления. Если канал пуст, выделяется новый буфер. Когда буфер сообщений готов, он отправляется на сервер serverChan.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Берем буфер, если он доступен; 
        // иначе - аллоцируем новый.
        select {
        case b = <-freeList:
            // Получили буфер; больше ничего не делаем.
        default:
            // Нет свободных, поэтому аллоцируем новый.
            b = new(Buffer)
        }

        // Читаем следующее сообщение из сети.
        load(b)              
        serverChan <- b  // Отправляем на сервер.
    }
}

Цикл сервера получает каждое сообщение от клиента, обрабатывает его, и возвращает буфер в свободный список.

func server() {
    for {
        b := <-serverChan    // Ожидание работы.
        process(b)
        // Повторно используем буфер, если есть место.
        select {
        case freeList <- b:
            // Буфер в свободном списке; 
            // больше ничего не делаем.
        default:
            // Список свободных полон, просто продолжаем.
        }
    }
}

Клиент пытается получить буфер из freeList; если ни один не доступен, он выделяет новый. Отправка сервером в freeList возвращает b в список свободных если список не полный, иначе буфер выбрасывается, чтобы быть утилизированным сборщиком мусора. (Условие default в select операторах выполняются, когда ни один другой случай не готов, это означает, что select никогда не блокируется.) Эта реализация создает список без утечек памяти всего в несколько строк, полагаясь на буферизованный канал и сборщик мусора для учета.


Читайте также:


Эффективный Go: распараллеливание вычислений

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

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

type Vector []float64

// Применяеи операцию к v[i], v[i+1] ... до v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // сигнализируем что эта часть выполнена
}

Мы запускаем части вычисления независимо друг от друга, по одному на каждом ядре процессора. Они могут быть выполнены в любом порядке, но это не имеет значения; мы только считаем сигналы завершения, опустошив канал после запуска всех go-процедур.

const numCPU = 4 // количество ядер процессора

func (v Vector) DoAll(u Vector) {

    // Буферизация необязательна, но разумна
    c := make(chan int, numCPU)  
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, 
                   (i+1)*len(v)/numCPU, u, c)
    }
    // Опустошаем канал
    for i := 0; i < numCPU; i++ {
        <-c    // ждем завершения одного задания
    }
    // Все выполнено
}

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

var numCPU = runtime.NumCPU()

Также есть функция runtime.GOMAXPROCS, которая сообщает (или устанавливает) указанное пользователем количество ядер, которое может запустить Go программа одновременно. По умолчанию используется значение runtime.NumCPU, но оно может быть переопределено установкой одноименной переменной среды оболочки или вызывом этой функции с положительным номером. Вызов этой функции с нолем просто запрашивает значение. Поэтому, если мы хотим удовлетворить запрос ресурса пользователя, мы должны написать:

var numCPU = runtime.GOMAXPROCS(0)

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


Читайте также:


Эффективный Go: каналы каналов

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

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

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

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

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Отправляем запрос (request)
clientRequests <- request
// Ждем ответ
fmt.Printf("answer: %d\n", <-request.resultChan)

На стороне сервера функция обработчика (handle function) - единственное, что изменяется.

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

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


Читайте также:


среда, 30 января 2019 г.

Эффективный Go: каналы

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

// небуферизованный канал целых чисел
ci := make(chan int)

// небуферизованный канал целых чисел            
cj := make(chan int, 0)   
     
// буферизованный канал указателей на Files
cs := make(chan *os.File, 100)  

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

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

c := make(chan int)  // Аллоцируем канал
// Начинаем сортировку в go-процедуре(goroutine); 
// когда она завершится, посылаем сигнал по каналу
go func() {
    list.Sort()
    c <- 1  // Отправляем сигнал; значение не важно
}()
doSomethingForAWhile()
<-c   // Ждем завершения сортировки; 
      // отменяем посланное значение

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

Буферизованный канал может использоваться как семафор, например, для ограничения пропускной способности. В следующем примере входящие запросы передаются handle, который отправляет значение в канал, обрабатывает запрос, а затем получает значение из канала, чтобы подготовить "семафор" для следующего потребителя. Емкость буфера канала ограничивает количество одновременных вызовов process.

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Ожидание истечения активной очереди.
    process(r)  // Может занять длительное время.
    <-sem       // Выполнено; 
                // разрешаем выполнение следующего запроса.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Не ждем окончания handle
    }
}

Пока обработчики(handlers) в MaxOutstanding выполняют process, будут блокироваться попытки отправки в заполненный буфер канала, пока один из существующих обработчиков не завершит работу и не получит из буфера.

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

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Ошибочно; 
                         // смотрите объяснение ниже
            <-sem
        }()
    }
}

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

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

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

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Создаем новый экземпляров req 
                   // для go-процедуры
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

Это может показаться странным писать

req := req

но в Go это законно и идиоматично. Вы получаете свежую версию переменной с тем же именем, сознательно затеняя переменную цикла локально, но уникально для каждой go-процедуры.

Возвращаясь к общей проблеме написания сервера, другой подход, который хорошо управляет ресурсами, состоит в том, чтобы начать фиксированное количество go-процедур handle, которые все читают из канала запросов. Количество go-процедур ограничивает количество одновременных вызовов process. Следующая функция Serve также принимает канал, на котором будет сказано выйти; после запуска go-процедур он блокирует получение из этого канала.

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Стартуем handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Ждем того, что будет сказано выйти
}


Читайте также:


Эффективный Go: параллелизм, go-процедуры (goroutines)

Разделение через общение

Параллельное программирование - это большая тема, и здесь будут упомянуты только некоторые Go-специфичные моменты.

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

Не общайтесь, разделяя память; вместо этого делитесь памятью, общаясь.

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

Один из способов обдумать эту модель - рассмотреть типичную однопоточную программу, которая работает на одном процессоре. Она не нуждается в синхронизации примитивов. Теперь запустите другой такой экземпляр; он тоже не нуждается в синхронизации. Теперь пусть эти экземпляры общаются; если связь является синхронизатором, по-прежнему нет необходимости для другой синхронизации. Unix-конвейеры (Unix pipelines), например, подходят для этой модели в совершенстве. Хотя подход Go к параллелизму берет свое начало в Связи последовательных процессов Хора (Hoare's Communicating Sequential Processes) (CSP), его также можно рассматривать как безопасное для типов обобщение каналов Unix.

Go-процедуры (Goroutines)

Они называются Go-процедуры (goroutines), потому что существующие термины - потоки (threads), сопрограммы (coroutines), процессы и т. д. - передают неточные значения. Go-процедура (goroutine) имеет простую модель: это функция, выполняющаяся одновременно с другими go-процедурами в том же адресном пространстве. Она легковесная, стоящая чуть больше, чем аллокация стекового пространства. И стеки стартуют малыми, поэтому они дешевы и растут выделяя (и освобождая) хранилище heap по мере необходимости.

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

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

// выполняем list.Sort параллельно; не ждите его.
go list.Sort()  

Литерал функции может быть полезен при вызове go-процедуры.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  
    // Обратите внимание на круглые скобки 
    // - должна быть вызвана функция.
}

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

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


Читайте также:


среда, 16 января 2019 г.

Основы Go: sync.Mutex

Мы видели, насколько хорошо каналы подходят для связи между go-процедурами.

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

Эта концепция известна как взаимное исключение, и общепринятым названием для структуры данных, которая это позволяет, является мьютекс (mutex).

Стандартная библиотека Go предоставляет взаимное исключение с помощью sync.Mutex и двух его методов:

Lock
Unlock

Мы можем объявить блок кода, который будет выполняться при взаимном исключении, окружив его вызовами к Lock и Unlock, как показано в методе Inc.

Мы также можем использовать defer, чтобы удостовериться, что мьютекс будет освобожден, как в методе Value.

package main

import (
  "fmt"
  "sync"
  "time"
)

// SafeCounter безопасен для конкурентного использования.
type SafeCounter struct {
  v   map[string]int
  mux sync.Mutex
}

// Inc инкрементирует счетчик для переданного ключа.
func (c *SafeCounter) Inc(key string) {
  c.mux.Lock()
  // Вызван Lock, поэтому только одна go-процедура за раз 
  // может получить доступ к карте c.v
  c.v[key]++
  c.mux.Unlock()
}

// Value возвращает текущее значение счетчика 
// для переданного ключа.
func (c *SafeCounter) Value(key string) int {
  c.mux.Lock()
  // Вызван Lock, поэтому только одна go-процедура за раз 
  // может получить доступ к карте c.v
  defer c.mux.Unlock()
  return c.v[key]
}

func main() {
  c := SafeCounter{v: make(map[string]int)}
  for i := 0; i < 1000; i++ {
    go c.Inc("somekey")
  }

  time.Sleep(time.Second)
  fmt.Println(c.Value("somekey"))
}

Вывод:

1000


Читайте также:


Основы Go: оператор select

Оператор select позволяет go-процедуре находиться в ожидании нескольких операций передачи данных.

select блокируется до тех пор, пока один из его блоков case не будет готов к запуску, а затем выполняет этот блок. Если сразу несколько блоков могут быть запущены, то выбирается произвольный.

package main

import "fmt"

func fibonacci(c, quit chan int) {
  x, y := 0, 1
  for {
    select {
    case c <- x:
      x, y = y, x+y
    case <-quit:
      fmt.Println("quit")
      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)
}

Вывод:

0
1
1
2
3
5
8
13
21
34
quit

Блок default в select

Блок default в select запускается, если никакой другой блок не готов.

Используйте default, чтобы посылать и получать данные без блокировок:

select {
case i := <-c:
    // используйте i
default:
    // получение из c вызвало бы блокировку
}

Пример:

package main

import (
  "fmt"
  "time"
)

func main() {
  tick := time.Tick(100 * time.Millisecond)
  boom := time.After(500 * time.Millisecond)
  for {
    select {
    case <-tick:
      fmt.Println("tick.")
    case <-boom:
      fmt.Println("BOOM!")
      return
    default:
      fmt.Println("    .")
      time.Sleep(50 * time.Millisecond)
    }
  }
}

Вывод:

    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
BOOM!


Читайте также:


вторник, 15 января 2019 г.

Основы Go: каналы, использование range и close

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

v, ok := <-ch

ok будет false, если больше нет значений для получения, и канал закрыт.

Цикл for i := range c получает значения из канала до тех пор, пока он не закрыт.

Замечание: Только отправитель данных должен закрывать канал, но никогда - получатель. Попытка отправить данные в закрытый канал вызовет panic.

Еще замечание: Каналы не как файлы; обычно вам не надо их закрывать. Закрывать необходимо, только когда получателю надо указать, что данных для получения больше нет, например, для завершения цикла range.

package main

import (
  "fmt"
)

func fibonacci(n int, c chan int) {
  x, y := 0, 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)
  }
}

Вывод:

0
1
1
2
3
5
8
13
21
34


Читайте также:


Основы Go: буферизированные каналы

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

ch := make(chan int, 100)

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

package main

import "fmt"

func main() {
  ch := make(chan int, 2)
  ch <- 1
  ch <- 2
  fmt.Println(<-ch)
  fmt.Println(<-ch)
}

Вывод:

1
2


Читайте также:


Основы Go: каналы

Каналы это типизированные "трубы", по которым вы можете посылать и получать значения с помощью оператора <-.

ch <- v    // Послать v в канал ch.
v := <-ch  // Получить из канала ch, и
           // присвоить значение переменной v.

(Данные перемещаются в направлении стрелки.)

Так же как карты и срезы, каналы должны быть созданы перед использованием:

ch := make(chan int)

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

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

package main

import "fmt"

func sum(s []int, c chan int) {
  sum := 0
  for _, v := range s {
    sum += v
  }
  c <- sum // отправить sum к c
}

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

  c := make(chan int)
  go sum(s[:len(s)/2], c)
  go sum(s[len(s)/2:], c)
  x, y := <-c, <-c // получить из c

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

Вывод:

-5 17 12


Читайте также:


Основы Go: Go-процедуры (goroutines)

Go-процедура это легкий поток, управляемый средой выполнения Go.

go f(x, y, z)

создает новую go-процедуру и запускает в ней

f(x, y, z)

f, x, y, и z определяются в текущей go-процедуре, а выполнение f происходит в новой go-процедуре.

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

package main

import (
  "fmt"
  "time"
)

func say(s string) {
  for i := 0; i < 5; i++ {
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
  }
}

func main() {
  go say("world")
  say("hello")
}

Вывод:

world
hello
hello
world
world
hello
hello
world
world
hello

Еще один простой пример:

package main

import (
    "fmt"
    "time"
)

func main() {
    go say("ho!", 2*time.Second)  
    go say("hey!", 1*time.Second)

    // Делаем чтобы main уснул на 4 секунды
    // чтобы go-процедуры могли завершиться
    time.Sleep(4 * time.Second)
}

// say печатает текст после засыпания на Х секунд
func say(text string, secs time.Duration) {
    time.Sleep(secs)
    fmt.Println(text)
}

Вывод:

hey!
ho!


Читайте также: