В каких случаях не стоит занимать все ядра под выполнение кода?

Ответ

Использование всех ядер CPU (например, устанавливая GOMAXPROCS равным runtime.NumCPU()) может быть неэффективным или даже вредным в следующих случаях:

  1. I/O-bound задачи – Если ваше приложение в основном ждёт завершения операций ввода-вывода (сеть, диск, база данных), увеличение количества рабочих горутин/потоков сверх необходимого не приведёт к ускорению, а лишь создаст дополнительные накладные расходы на переключение контекста и управление планировщиком.
  2. Высокая конкуренция за общие ресурсы – При активной работе с общими структурами данных (например, sync.Map, мьютексы, каналы) излишний параллелизм может привести к увеличению блокировок (lock contention) и снижению общей производительности из-за ожидания доступа к ресурсам.
  3. Совместное использование ресурсов с другими сервисами – Если на той же машине работают другие критичные сервисы или приложения (например, база данных, кэш, другие микросервисы), полное занятие всех ядер вашим приложением может привести к их деградации производительности. В таких случаях лучше оставить часть CPU для них, явно ограничив GOMAXPROCS или количество рабочих горутин.

Пример ограничения количества рабочих горутин в Go:

import (
    "runtime"
    "sync"
)

func main() {
    // Ограничиваем количество логических процессоров, доступных планировщику Go
    // runtime.GOMAXPROCS(runtime.NumCPU() / 2) // Пример: использовать половину ядер

    // Или ограничиваем количество параллельных задач через семафор/worker pool
    maxWorkers := runtime.NumCPU() / 2
    sem := make(chan struct{}, maxWorkers)
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ { // Пример 100 задач
        wg.Add(1)
        sem <- struct{}{} // Занимаем слот
        go func(taskID int) {
            defer wg.Done()
            defer func() { <-sem }() // Освобождаем слот
            // Выполнение задачи
            // fmt.Printf("Выполняется задача %dn", taskID)
        }(i)
    }
    wg.Wait()
}

Ответ 18+ 🔞

Да ты посмотри, какой народ пошёл, блядь! Все как с цепи сорвались — «давай все ядра в дело, нахуй! GOMAXPROCS в NumCPU и поехали!». А потом сидят, чешут репу: «Чё-то у меня приложение тормозит, как сука старая...».

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

Во-первых, если твоя программа — это одна сплошная сосалка. Ну, I/O-bound, ёпта. Она не процессор жрёт, а тупо ждёт, когда база данных отпишется, файл прочитается или сетевой пакет прилетит. Ты накрутишь ей горутин по числу ядер — а они все, сука, будут висеть в ожидании. Планировщик Go с ума сойдёт, переключая их туда-сюда, а толку — ноль, волнение ебать. Только накладные расходы, чих-пых тебя в сраку.

Во-вторых, если у вас там общага на замке. Представь: десять голодных горутин ломятся в одну sync.Map или дерутся за один мьютекс. Это ж блядский базар, а не параллелизм! Конкуренция, lock contention, ёперный театр! Все стоят в очереди, друг другу ебало машут, а работа стоит. Чем больше таких «работничков» ты нагонишь, тем дольше они будут друг другу мешать. Удивление пиздец.

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

Смотри, как можно не наебнуться. Вот тебе пример, где мы сами рулим, сколько горутин одновременно пашут:

import (
    "runtime"
    "sync"
)

func main() {
    // Вариант 1: Сказать Go сразу — не жри всё, поделись.
    // runtime.GOMAXPROCS(runtime.NumCPU() / 2) // Возьмём только половинку ядер, не жадничаем

    // Вариант 2: Сделать свою очередь-семафор, чтоб лишние не лезли.
    maxWorkers := runtime.NumCPU() / 2 // Опять же, половинку
    sem := make(chan struct{}, maxWorkers) // Это и есть семафор, слоты в нём
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ { // Допустим, 100 задач на выполнение
        wg.Add(1)
        sem <- struct{}{} // Пытаемся занять слот. Если свободных нет — тут и повиснем, ждём!
        go func(taskID int) {
            defer wg.Done()
            defer func() { <-sem }() // Как закончили — ОБЯЗАТЕЛЬНО освобождаем слот, а то все встанут!
            // ... тут делаем полезную работу ...
            // fmt.Printf("Таска %d пошлаn", taskID)
        }(i)
    }
    wg.Wait() // Ждём, пока все горутинки доработают
}

Суть в чём, блядь? Мы создаём канал sem с буфером, скажем, на 4 слота (если ядер 8). Значит, одновременно работать будут только 4 горутины. Остальные будут сидеть и ждать, пока место освободится. Никакой давки, никакой лишней конкуренции. Красота, ёпта!

Так что включай голову, э бошка думай. Не всегда «больше» значит «лучше». Иногда «ровно столько, сколько надо» — это овердохуища эффективнее.