Comments 22
Не стоит изобретать новые велосипеды. Уже есть https://pkg.go.dev/golang.org/x/sync/errgroup
Не то чтобы семафоры были изобретены только что https://pkg.go.dev/golang.org/x/sync/semaphore
Вообще по стечению обстоятельств вот только что обсуждали мой код, где коллега убедительно убедил что использовать семафоры - хорошая затея. Код действительно стал проще.
Правда в моем кейсе на производительность наплевать, так что бонус лично для меня чисто эстетический
Спасибо!
А если это не NDA, то можно абстрактно расписать кейс?
Джсоны перекладываем...
Достаем из сервис1 список ресурсов, и для каждого ресурса выполняем пайплайн в сервис2.
Пайплайн синхронный и состоит из нескольких api-вызовов (условно, resource1/check
, resource1/init
, resource1/update
)
Чтобы не насиловать api сервис2, запускаем пайплайн на N ресурсов параллельно, если для какого-то ресурса пайплайн завершился - может запускать для следующего
Я не точно выразился, думал сразу будет понятно. Я не семафоры имел ввиду, а worker pool. Хотя, истины ради, и я писал свою реализацию.
Спасибо! Видел этот пакет, даже пользовался пару раз)
Порекомендую также посмотреть на https://github.com/sourcegraph/conc
субъективно, оказалось удобнее, чем errgroup и semaphore
А мы пользуемся https://github.com/alitto/pond
Группировка, таймауты и много ещё чего.
Различные стратегии для наращивания воркеров.
Отличная статья! Есть пример для семафора, к сожалению он не затрагивает воркер пул, но думаю тк статья про семафор, то имеет место быть. Плюс, если кто-то захочет найти еще примеры использования семафора, то может посмотреть исходники pgx (а точнее pgxpool).
Когда я писал тесты для слоя базы данных(тестировал без мока бд). То была проблема с тем, что тесты ломали друг друга. Решений было несколько:
1. Ограничить количество одновременных тестов до 1 (т.к у нас не только бд тесты, то их выполнение на 1 потоке занимает много времени)
2. Создавать отдельные схемы для каждого тесты (усложнение логики)
3. Запускать для каждого теста отдельный контейнер
4. Использовать семафор с размером 1 для лока 1 теста бд за раз (плюсы: простая логика. Минусы, при большом количество тестов для базы данных скорее всего это станет узким горлом).
Собственно 4 вариант и был выбран. К слову, если кто-то знает еще варианты решения этого вопроса, то буду рад узнать, как минимум для понимания того, что я мог упустить.
Также было бы интересно посмотреть использование паттернов fan-in и fan-out на практике.
Использовать семафор с размером 1 для лока 1 теста бд за раз
Что-то я не уловил. Разве этим самым вы не указываете, что тесты с БД должны выполняться последовательно? Но ведь тоже самое можно получить, просто не указывая для таких тестов
t.Parallel()
Или я чего-то не понял?
Тесты из разных пакетов буду запускаться в любом случае параллельно(только если не ограничить количество одновременных задач, см первый вариант), t.Parallel() указывается для параллельного запуска тестов из одного пакета или сабтестов. В этом и была проблема, у меня несколько пакетов, 1 за каждую таблицу. Если я где-то ошибаюсь, то буду рад если кто-то меня поправит.
>узким горлом
Лишь бы не бутылочным местом, пьянству бой!
Автору большое спасибо за примеры!
А зачем плодить миллион висящих горутин?
for _, v := range usrs {
sem.Acquire()
go func(usr users.User) {
defer sem.Release()
...
}
}
Вот меня мучил тот же вопрос, когда я гуглил реализации семафоров когда-то. Поэтому когда я решал аналогичную задачу, я делал это именно вторым способом. Горутины нет смысла плодить бесконечно, это забьет ресурсы, поэтому для меня в тот момент было логичным ограничить как раз количество созданных горутин.
Но я не отрицаю, что первый способ нужен в рамках каких-нибудь других примеров (было бы интересно узнать для каких). И из плюсов я заметил, что не будет ситуации что ты воркерпулу из большого числа заранее созданных висящих горутин скормишь пару задач. Но это условный плюс.
select {
case outputCh <- resultWithError{
User: usr,
Err: err,
}:
case <-sgnlCh:
return
}
В этой конструкции sgnlCh должен идти первым в селекте, а не последним
А какая разница? Порядок обработки select-case не определен, если в обоих каналах есть данные
Вы правы, перемещение одного case вверх не изменит ситуацию. Мне изначально резануло глаз то, что первый case во всех случаях готов исполниться. А значит, даже если у нас есть сигнал на выход, нет никакой гарантии сразу завершить работу. Изначально я почему то думал, что первый case немного приоритетнее, но был не прав, извиняюсь.
Есть некое количество пользователей, например от 1 до 100 000, по каждому надо выполнить функцию Deactivate. Как правило, это происходит путём отправки одного запроса в сторонний API
Тогда ваша функция и этот внешний API должны принимать контекст и код обоих подходов будет выглядеть иначе.
Также смущают эти моменты:
Сомнительный дизайн у cемафора. Если вы его выделяете в отдельную сущность (а не просто работаете с каналом), то очевидно канал лучше сделать приватным и обернуть его создание в конструктор.
Вообще говоря подход с семафором и cancel-ом по первой ошибке реализован в golang.org/x/sync/errgroup и там это сделано почище — единственное чего там может не хватать recover-а паник внутри горутин (см. https://github.com/golang/go/issues/53757)
Во втором примере у вас утечка горутин. После return-а по ошибке в основной функции горутины будут вечно висеть на записи в
outCh
. Это легко отловить через условный go.uber.org/goleakТут уже мое имхо, но кажется, что лучше дожидаться завершения всех запущенных горутин в функции, даже если вы уверены, что они рано или поздно завершатся
Спасибо за развернутый комментарий! Особенно за то, что расписали по пунктам (это хорошо читается)
Согласен с моментом насчет контекста. Поскольку это обучающая статья, я убрал select'ы по контексту. Своим студентам, когда работал на курсе от МТС я давал пример с контекстами. Но здесь посчитал, что это перегрузит обучающую статью.
1. Сделал максимально простым, но читаемым (чтобы видно было, что "семафор"). Вообще необязательно даже отдельный тип делать, можно каналом ограничиться. Кстати, если у вас есть под рукой реализация такая которая нравится можете скинуть)
2. Согласен
3. Согласен. В бою я обычно вешаю контекст везде. В этом случае всегда будет кейс который снимает блокировку висящих горутин и завершает этот процесс.
4. Я так понял, это связано с 3-м: мы гарантируем, что за пределами функций не будет утечек. В таком случае согласен.
Учимся применять Semaphore и Worker Pool на Go