Кто с кем конкурирует при конкурентности?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Природа конкурентности в Go: кто с кем соревнуется?
В контексте программирования на Go (Golang) под конкурентностью понимается структура программы, в которой несколько вычислительных процессов (горутин) выполняются как бы одновременно и независимо, даже если физически они работают на одном ядре CPU. Это — модель проектирования, а не физическая одновременность. Вопрос о том, «кто с кем конкурирует», глубоко связан с внутренним устройством Go и требует многоуровневого рассмотрения.
Главные участники «соревнования»
- Горутины (Goroutines) — основные «спортсмены»:
* Это легковесные потоки, управляемые **рантаймом Go**. Их можно создавать тысячами и миллионами.
* **Они конкурируют между собой за ресурсы планировщика**. Ресурсы CPU — главный приз. Планировщик Go (часть рантайма) решает, какую горутину запустить на каком **логическом процессоре (M)**, обеспечивая видимость параллельного выполнения.
- Планировщик Go (Scheduler) — «рефери и организатор»:
* Он работает по модели **M:N**:
* **M (Machine)** — поток ОС (kernel thread). Именно на нём выполняется код.
* **G (Goroutine)** — сама горутина.
* **P (Processor)** — логический контекст процессора (до Go 1.14) или абстракция для локальной очереди (ныне). **P** — ключевой ресурс: горутина должна быть привязана к **P**, чтобы выполняться на **M**.
* Конкуренция происходит **за доступ к `P`** и, как следствие, к потоку **M**.
Условия возникновения реальной «конкуренции»
1. Неблокирующие горутины (CPU-bound задачи)
Когда горутина выполняет расчёты, не вызывая системных вызовов или операций ввода-вывода (I/O), она занимает P и M. Чтобы дать ход другим, планировщик принудительно переключает горутины в ключевых точках (напр., при вызове функций, в бесконечных циклах без вызовов runtime.Gosched()). Пример:
func main() {
runtime.GOMAXPROCS(1) // Ограничимся одним P для наглядности
go func() {
for i := 0; i < 5; i++ {
fmt.Println("Горутина 1:", i)
}
}()
go func() {
for i := 0; i < 5; i++ {
fmt.Println("Горутина 2:", i)
}
}()
time.Sleep(time.Second)
}
Здесь две горутины конкурируют за один P. Планировщик будет переключать их, создавая иллюзию параллелизма.
2. Блокирующие операции (I/O, системные вызовы, синхронизация)
При вызове time.Sleep, чтении из сети или канала, блокировке мьютексом горутина отпускает P. Этот P немедленно может быть передан другой готовой горутине из очереди. Пример конкуренции за канал:
func worker(id int, ch chan string) {
ch <- fmt.Sprintf("Работник %d выполнил задачу", id)
}
func main() {
ch := make(chan string, 2)
for i := 1; i <= 5; i++ {
go worker(i, ch)
}
// Все 5 горутин пытаются записать в буферизованный канал ёмкостью 2.
// Первые две получат доступ сразу, остальные будут ждать (блокироваться),
// конкурируя за освободившееся место.
}
3. Конкуренция за доступ к общим данным (Race Condition)
Разные горутины могут конкурировать за изменение одной переменной в памяти. Без синхронизации это приводит к состоянию гонки (data race):
var counter int
var wg sync.WaitGroup
func increment() {
defer wg.Done()
for i := 0; i < 1000; i++ {
counter++ // Здесь горутины конкурируют за доступ к `counter`
}
}
func main() {
wg.Add(2)
go increment()
go increment()
wg.Wait()
fmt.Println(counter) // Результат может быть меньше 2000!
}
Для защиты используется мьютексы (sync.Mutex), за которые горутины также конкурируют, чтобы получить эксклюзивный доступ.
Ключевые выводы
- Главные конкуренты — горутины, борющиеся за ресурсы планировщика (P и M) и процессорное время.
- Уровни конкуренции:
1. **Системный**: горутины → P → M → ядро CPU.
2. **Пользовательский**: конкуренция за общие данные (требует примитивов синхронизации: `mutex`, `atomic`, каналы).
- Задача разработчика — правильно организовать эту конкуренцию:
* Использовать каналы и `select` для коммуникации.
* Применять `sync.Mutex`, `sync.RWMutex` или атомарные операции для защиты данных.
* Проектировать программу так, чтобы горутины не блокировали P надолго (разбивать CPU-bound задачи).
Таким образом, конкуренция — это соревнование горутин за вычислительные ресурсы и доступ к общему состоянию, а планировщик Go выступает арбитром, обеспечивающим чередование и реализующим принцип «Не общайтесь через общую память; вместо этого делитесь памятью через общение» (каналы).