Зачем в многопоточном приложении создавать больше потоков, чем ядер у процессора?

«Зачем в многопоточном приложении создавать больше потоков, чем ядер у процессора?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

Ключевые причины:

  • Маскировка задержек: Потоки, ожидающие завершения медленных операций (например, сетевого запроса), не используют CPU. Дополнительные потоки позволяют загрузить процессор другой работой.
  • Автоматическое управление: Системы вроде Grand Central Dispatch (GCD) или OperationQueue используют пул потоков и создают их оптимальное количество, избегая ручного overhead.
  • Риски: Слишком большое количество активных потоков приводит к накладным расходам на переключение контекста (context switching), что может снизить производительность.

Практическое правило: Оптимальное количество конкурентных задач часто лежит в диапазоне 2-4x от числа ядер CPU, но окончательно зависит от характера нагрузки (CPU-bound vs I/O-bound).

Пример на Swift (GCD):

let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)

// Запускаем 100 задач, многие из которых включают ожидание
for i in 0..<100 {
    concurrentQueue.async {
        // 1. Имитация CPU-работы
        performCalculation()
        // 2. Имитация длительного ожидания (сеть/диск)
        sleep(1)
        print("Задача (i) завершена")
    }
}
// GCD сам управляет потоками в пуле, эффективно используя ядра.