← Назад к вопросам

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

2.0 Middle🔥 142 комментариев
#Конкурентность и горутины#Операционные системы и Linux

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Параллельное выполнение задач на одном ядре

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

Ключевые механизмы псевдопараллельного выполнения

1. Кооперативная многозадачность (Cooperative Multitasking)

В этой модели каждая задача добровольно отдает управление, позволяя работать другим. В Go это реализуется через горутины (goroutines) и планировщик Go.

package main

import (
    "fmt"
    "time"
)

func task1() {
    for i := 0; i < 3; i++ {
        fmt.Println("Task 1:", i)
        time.Sleep(100 * time.Millisecond) // Точка кооперативного переключения
    }
}

func task2() {
    for i := 0; i < 3; i++ {
        fmt.Println("Task 2:", i)
        time.Sleep(150 * time.Millisecond)
    }
}

func main() {
    go task1() // Запуск горутины
    go task2()
    
    time.Sleep(1 * time.Second) // Даем время на выполнение
}

2. Вытесняющая многозадачность (Preemptive Multitasking)

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

Роль планировщика Go (Goroutine Scheduler)

Планировщик Go реализует модель M:N, где M горутин выполняются на N потоках ОС (системных потоках). На одном ядре это работает так:

// Пример с двумя долгими задачами на одном ядре
package main

import (
    "runtime"
    "fmt"
)

func computeTask(id int) {
    result := 0
    for i := 0; i < 1000000; i++ {
        result += i * i
    }
    fmt.Printf("Task %d completed: %d\n", id, result)
}

func main() {
    // Явно ограничиваем одним процессором
    runtime.GOMAXPROCS(1)
    
    go computeTask(1)
    go computeTask(2)
    
    runtime.Gosched() // Явное указание переключить контекст
    
    // Ждем завершения (в реальном коде использовали бы sync.WaitGroup)
    time.Sleep(2 * time.Second)
}

Принципы работы планировщика на одном ядре:

  1. Локальная очередь выполнения - планировщик поддерживает очередь готовых к выполнению горутин

  2. Точки переключения (yield points):

    • Системные вызовы (файловые операции, сетевые запросы)
    • Каналы (channel operations)
    • Вызов runtime.Gosched()
    • Блокировки (mutex, wait groups)
    • Размещение новых горутин
  3. Распределение времени - планировщик пытается справедливо распределить процессорное время между горутинами

Критические аспекты выполнения

  • Контекстное переключение (context switching) - сохранение состояния одной задачи и восстановление другой
  • Проблема инверсии приоритетов - длительная горутина может блокировать выполнение других
  • Голодание (starvation) - одна задача может не получать достаточно времени CPU

Практические рекомендации для разработчика Go:

// Плохой пример - горутина монополизирует CPU
func busyTask() {
    for {
        // Нет точек переключения - может заблокировать другие горутины
    }
}

// Хороший пример - горутина с точками переключения
func wellBehavedTask() {
    for {
        // Полезная работа
        doWork()
        
        // Явное переключение контекста
        runtime.Gosched()
        
        // Или использование каналов/таймеров
        select {
        case <-time.After(time.Millisecond):
            continue
        }
    }
}

Вывод

На одном ядре две задачи выполняются псевдопараллельно через быстрое переключение контекста. Планировщик Go обеспечивает эффективное распределение времени CPU между горутинами, создавая иллюзию параллельного выполнения. Ключевыми для производительности являются правильное проектирование горутин с учетом точек переключения контекста и понимание работы планировщика, особенно при ограничении ресурсов (GOMAXPROCS=1).

Как будут выполняться две задачи на одном ядре? | PrepBro