Go : параллельное выполнение

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 07 фев 2024, 13:17



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

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 13:23

Olej писал(а):
07 фев 2024, 13:17
использование пакета errgroup для параллельного выполнения задач с возможностью обработки ошибок и отмены всех задач при возникновении первой ошибки.
Пекет errgroup: https://pkg.go.dev/golang.org/x/sync/errgroup
Всё это строится на базе такого интересного, необычного (подобного не видел ни в одном языке программирования) и сложного (в понимании) механизма высокого уровня как контексты - пакет context стандартной библиотеки https://pkg.go.dev/context.
И прежде, чем разбираться с errgroup, нужно разбираться с context :!:

P.S. Кстати, поиск (и не мало) показывает, что по context именно - мало чего внятно написано, на любых языках считая ... а если что и написано, то с ошибками и переписывают друг у друга ... всё больше какие-то маловнятные примеры.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 14:11

Olej писал(а):
08 фев 2024, 13:23
нужно разбираться с context
Разбираемся с пакетом Context в Golang
29 июл 2019 в 13:53
Пакет context в go позволяет вам передавать данные в вашу программу в каком-то «контексте». Контекст так же, как и таймаут, дедлайн или канал, сигнализирует прекращение работы и вызывает return.
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
Здесь становится чуть интереснее. Эта функция создает новый контекст из переданного ей родительского.
Возвращается производный контекст и функция отмены. Вызывать функцию отмены контекста должна только та функция, которая его создает. Вы можете передавать функцию отмены другим функциям, если хотите, но это настоятельно не рекомендуется. Обычно это решение принимается от непонимания работы отмены контекста.

Код: Выделить всё

ctx, cancel := context.WithCancel(context.Background())
context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)
Эта функция возвращает производный контекст от своего родителя, который отменяется после дедлайна или вызова функции отмены.

Код: Выделить всё

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
Эта функция похожа на context.WithDeadline. Разница в том, что в качестве входных данных используется длительность времени.

Код: Выделить всё

ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
В следующем примере вы можете видеть, что функция, принимающая контекст, запускает горутину и ожидает ее возврата или отмены контекста. Оператор select помогает нам определить, что случится первым, и завершить работу функции.
После закрытия канала Done <-ctx.Done() выбирается случай case <-ctx.Done():. Как только это происходит, функция должна прервать работу и подготовиться к возврату.
В любых случаях срабатывание контекста "ловят" по каналу ctx.Done() (... про который в документации толком не написано):

Код: Выделить всё

...
// Используем select для выхода по истечении времени жизни контекста
    select {
        case <-ctx.Done():
            // Если контекст истекает, выбирается этот случай
            // Высвобождаем ресурсы, которые больше не нужны из-за прерывания работы
            // Посылаем сигнал всем горутинам, которые должны завершиться (используя каналы)
            // Обычно вы посылаете что-нибудь в канал,
            // ждете выхода из горутины, затем возвращаетесь
            // Или используете группы ожидания вместо каналов для синхронизации
...

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 16:51

Olej писал(а):
08 фев 2024, 14:11
про который в документации толком не написано
https://cs.opensource.google/go/go/+/go ... xt.go;l=68
Только в комментариях к самому типу type Context:

Код: Выделить всё

type Context interface {
...
// Done возвращает канал, который закрыт, когда от имени контекста выполнена работа
// и контекст должен быть отменен. Done может вернуть nil, если этот контекст может
// никогда не быть отмененным. Последовательные вызовы Done возвращают одно и то же значение.
// Закрытие канала Done может произойти асинхронно, уже после возврата функции отмены.

// WithCancel обеспечивает закрытие Done при вызове отмены;
// WithDeadline организует закрытие Done по истечении крайнего срока действия; 
// WithTimeout обеспечивает закрытие Done по истечении тайм-аута.


Код: Выделить всё

// Done предоставляется для использования в операторах select:
	//  // Stream generates values with DoSomething and sends them to out
	//  // until DoSomething returns an error or ctx.Done is closed.
	//  func Stream(ctx context.Context, out chan<- Value) error {
	//  	for {
	//  		v, err := DoSomething(ctx)
	//  		if err != nil {
	//  			return err
	//  		}
	//  		select {
	//  		case <-ctx.Done():
	//  			return ctx.Err()
	//  		case out <- v:
	//  		}
	//  	}
	//  }

Код: Выделить всё

Done() <-chan struct{}
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	Err() error

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 17:18

Olej писал(а):
08 фев 2024, 14:11
Разбираемся с пакетом Context в Golang
Лучшие практики
- context.Background следует использовать только на самом высоком уровне, как корень всех производных контекстов.
- context.TODO должен использоваться, когда вы не уверены, что использовать, или если текущая функция будет использовать контекст в будущем.
- Отмены контекста рекомендуются, но эти функции могут занимать время, чтобы выполнить очистку и выход.
- context.Value следует использовать как можно реже, и его нельзя применять для передачи необязательных параметров. Это делает API непонятным и может привести к ошибкам. Такие значения должны передаваться как аргументы.
- Не храните контексты в структуре, передавайте их явно в функциях, предпочтительно в качестве первого аргумента.
- Никогда не передавайте nil-контекст в качестве аргумента. Если сомневаетесь, используйте TODO.
- Структура Context не имеет метода cancel, потому что только функция, которая порождает контекст, должна его отменять.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 17:28

Olej писал(а):
08 фев 2024, 13:23
нужно разбираться с context
Разбираемся с контекстами в Go
Ильдар Карымов
December 5, 2021 · 9 min
Я заметил, что тема контекстов в языке Go у многих почему-то вызывает сложности с пониманием. Возможно, это связано с тем, что контекст — это очень абстрактная сущность и не встречается в других языках программирования в таком виде, по крайней мере в тех языках, что довелось использовать мне.
Если мы взглянем на документацию к пакету context, то первый абзац будет таким:
Пакет context определяет тип Context, который позволяет управлять дедлайнами, сигналами отмены и другими значениями области действия запросов между границами API и процессами.

Что это, чёрт побери, значит?

А значит это примерно следующее:

Контекст — это объект, который предназначен в первую очередь для того, чтобы иметь возможность отменить извне выполнение потенциально долгой операции. Кроме того, с помощью контекста можно хранить и передавать информацию между функциями и методами внутри вашей программы.
Отменять долгие операции с помощью контекста можно несколькими способами:
- По явному сигналу отмены (context.WithCancel)
- По истечению промежутка времени (context.WithTimeout)
- По наступлению временной отметки или дедлайна (context.WithDeadline)
Вы можете спросить: а что за context.Background()?

Дело в том, что любой контекст должен наследоваться от какого-то другого, родительского контекста. Исключения: Background и TODO. Background — это контекст-заглушка, используемый как правило как самый верхний родитель для всех дочерних контекстов в иерархии. TODO — это тоже заглушка, но используется в тех случаях, когда мы ещё не определились, какой тип контекста мы хотим использовать. Эти два типа контекста по сути одно и тоже, и разница исключительно семантическая.

Окей, зачем нужна схема с родительскими и дочерними контекстами? Это сделано для того, чтобы внутри функции, куда был проброшен контекст, не было возможности повлиять на условия отмены сверху. Таким образом мы имеем гарантию (с некоторым оговорками), что контекст с дедлайном отменится не позже данного дедлайна.
Мудрёно :?:
Ну ... несколько да :!: :oops:
Окей, а что насчёт передачи значений через контекст? Для этого в пакете существует функция WithValue.
...
Когда стоит передавать данные через контекст?
Короткий ответ — никогда. Передача данных через контекст является антипаттерном, поскольку это порождает неявный контракт между компонентами вашего приложения, к тому же ещё и ненадёжный. Исключение составляют случаи, ...
Круто? :-o
А то :!: :lol:
Советы и лучшие практики
- Передавайте контекст всегда первым аргументом — это общепринятое соглашение;
- Передавайте контекст только в функции и методы, не храните в состоянии (внутри структуры). Контексты спроектированы так, чтобы их использовали как одноразовые и неизменяемые объекты. Например, если вы сохраните контекст с таймутом в 15 секунд в поле структуры, а спустя 15 секунд попробуете выполнить операцию с данным контекстом, у вас ничего не получится. Обнулить счётчик таймаута вы тоже не сможете;
- Используйте context.WithValue только в крайних случаях. В 99,(9)% случаев вы сможете передать данные через аргументы функции;
- context.Background должен использоваться только как самый верхний родительский контекст, поскольку он является заглушкой и не предоставляет средств контроля;
- Используйте context.TODO, если пока не уверены, какой контекст нужно использовать;
- Не забывайте вызывать функцию отмены контекста, т.к. функции, принимающей контекст может потребоваться время на завершение перед выходом;
- Передавайте только контекст, без функции отмены. Контроль за завершением контекста должен оставаться на вызывающей стороне, иначе логика приложения может стать очень запутанной.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 17:35

Olej писал(а):
08 фев 2024, 13:23
нужно разбираться с context
Контекст
Что такое контекст?
В Go, контекст (context) используется для передачи сигналов относительно отмены операций, таймаутов и передачи метаданных между API. Это особенно полезно в ситуациях, когда у вас есть множество горутин и вы хотите контролировать их выполнение.
...
Когда контекст отменяется, все горутины, которые получают этот контекст, получают сигнал об отмене, и они должны прекратить свою работу.
Внутри, контекст представляет собой интерфейс с несколькими методами:
- Deadline() (deadline time.Time, ok bool): Возвращает время, когда работа должна быть завершена. Второе возвращаемое значение ok показывает, был ли установлен крайний срок.
- Done() <-chan struct{}: Возвращает канал, который будет закрыт, когда работа должна быть отменена. Если канал закрыт, то Err() вернет не nil.
- Err() error: Возвращает ошибку, которая описывает причину завершения контекста. Это может быть context.Canceled или context.DeadlineExceeded.
- Value(key interface{}) interface{}: Возвращает значение, связанное с ключом. Если ключа нет, возвращается nil.

Когда контекст отменяется, все его дочерние контексты также отменяются. Это позволяет управлять группами горутин, которые выполняют связанные задачи. Если одна задача отменяется, все связанные задачи также отменяются
Как работает WithCancel?

Функция WithCancel из пакета context в Go создает новый контекст из существующего (родительского) контекста, который может быть отменен. Эта функция возвращает новый контекст и функцию cancel, которую можно вызвать, чтобы отменить контекст.

Вот как это работает:

Код: Выделить всё

ctx, cancel := context.WithCancel(parentCtx)
Здесь ctx - это новый контекст, который наследует все свойства от parentCtx, и cancel - это функция, которую можно вызвать, чтобы отменить ctx и все контексты, производные от ctx.

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

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

Код: Выделить всё

defer cancel() // Make sure to cancel when done with context

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 08 фев 2024, 18:28

Olej писал(а):
08 фев 2024, 13:23
И прежде, чем разбираться с errgroup, нужно разбираться с context
Теперь, когда более-менее понятно, начинаем эксперименты ... сначала с context, а затем и с множественными конкурентными горутинами...
Это контекст тайм-аута:

Код: Выделить всё

package main

import (
    "os"
    "strconv"
    "fmt"
    "time"
    "context"
)

var t, d = 2, 3; // тайм-аут и длительность - сек.

func main() {
    if len(os.Args) > 1 { t, _ = strconv.Atoi(os.Args[1])}
    if len(os.Args) > 2 { d, _ = strconv.Atoi(os.Args[2]) }

    ctx, cancel := context.WithTimeout(context.Background(),
                        time.Duration(t) *  time.Second)
    defer cancel()
    action(ctx)
    fmt.Printf("код завершения: %v\n", ctx.Err())
    if err := ctx.Err(); err == nil {
        fmt.Println("нормальное завершение")
    } else {
        fmt.Printf("завершение по тайм-ауту\n")
    }
}

func action(ctx context.Context) {
	t0 := time.Now()
    defer func() {
        fmt.Printf("завершено ... %v\n", time.Now().Sub(t0))
    }()
	for {
		select {
		case <-ctx.Done():
			return
		default:
			time.Sleep(1 * time.Second) // "работа"
			fmt.Printf("выполнено ... %v\n", time.Now().Sub(t0))
            if d--; 0 == d { return }
		}
    }
}

Параметры команды запуска (если они есть):
1-й параметр - время (сек.) отпущенное программе по тайм-ауту
2-й параметр - время (сек.) полного заказнного выполнения приложения
Здесь программе не дали выполнитться до конца:

Код: Выделить всё

olej@R420:~/2024/own.BOOKs/BHV.Go.2/examples.work/goproc/errgrp$ go run context1.go 4 5
выполнено ... 1.000075204s
выполнено ... 2.000575443s
выполнено ... 3.001836834s
выполнено ... 4.002930861s
завершено ... 4.002980116s
код завершения: context deadline exceeded
завершение по тайм-ауту
А здесь программа полностью вырабатывает своё время:

Код: Выделить всё

olej@R420:~/2024/own.BOOKs/BHV.Go.2/examples.work/goproc/errgrp$ go run context1.go 5 4
выполнено ... 1.000134164s
выполнено ... 2.000528501s
выполнено ... 3.000706946s
выполнено ... 4.001091152s
завершено ... 4.001156449s
код завершения: <nil>
нормальное завершение
Вложения
context1.go
(1.05 КБ) 6 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 09 фев 2024, 02:03

Olej писал(а):
08 фев 2024, 18:28
Теперь, когда более-менее понятно, начинаем эксперименты ... сначала с context, а затем и с множественными конкурентными горутинами...
То, что гораздо интереснее того что выше ...
- несколько (N=4) параллельных горутин выполнения (action()) "полезной работы"...
- ожидание нажатия клавиши на клавиатуре ... для того чтобы это был неблокирующий ввод - это отдельная горутина...
- взаимодейсткие порождающей эти N=4 горутин вызывающей функции (activity()) - через контекст завершения context.WithCancel)

Код: Выделить всё

package main

import (
    "github.com/eiannone/keyboard"
    "fmt"
    "time"
    "context"
)

func main() {
    t0 := time.Now()
    activity()
    fmt.Printf("время выполнения: %v\n", time.Now().Sub(t0))
}

func activity() {
    const d = 4                      // время автономного выполнения - сек.
    var (
        chChan  = make(chan bool, 1) // индикатор ввода клавиатуры
        timer = time.NewTimer(d * time.Second)
    )
    
    ctx, cancel := context.WithCancel(context.Background())
    
    defer func() {
         cancel()
         fmt.Printf("код завершения ветвей = %s\n", ctx.Err())
         timer.Stop()
         keyboard.Close()  
    }()
    
    const потоки = 4                 // запуск горутин "работы"
    for i := 0; i < потоки; i++ {
        go action(ctx)
    }
   
    go func(chChan chan <- bool) {   // горутина ожидание нажатия клавиши
        _, _, _ = keyboard.GetSingleKey()
        chChan <- true 
    }(chChan)

    for {
        select {
        case <-timer.C:
            println("timer")
            return
        case <-chChan:
            println("keyboard")
            return
        default:
            time.Sleep(1 * time.Second) 
            println("wait ...")            
        }
    }
}

func action(ctx context.Context) {
    for {
	select {
	case <-ctx.Done():
	    return
	default:                     // выполняемая горутиной "работа"
            time.Sleep(100 * time.Millisecond)
	}
    }
}
Вложения
contextc.go
(1.59 КБ) 6 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go : параллельное выполнение

Непрочитанное сообщение Olej » 09 фев 2024, 02:11

Olej писал(а):
09 фев 2024, 02:03
То, что гораздо интереснее того что выше ...
И как это выполняется:

Код: Выделить всё

olej@R420:~/2024/own.BOOKs/BHV.Go.2/examples.work/goproc/errgrp$ go run contextc.go 
wait ...
wait ...
wait ...
wait ...
timer
код завершения ветвей = context canceled
время выполнения: 4.00179948s
- здесь я программу оставил в покое, и она доработала "до упора" - отведенные её 4 сек. по тайм-ауту timer

Код: Выделить всё

olej@R420:~/2024/own.BOOKs/BHV.Go.2/examples.work/goproc/errgrp$ go run contextc.go 
wait ...
wait ...
wait ...
keyboard
код завершения ветвей = context canceled
время выполнения: 3.002850196s
- а здесь я, до истечения таймера, нажимаю что-то на клавиатуре - рабочие потоки получают команду на завершение через контекст ctx :

Код: Выделить всё

...
<-ctx.Done()
...

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 9 гостей