---
id: 208cfd8127284f07337b5f5002f3351d
author: hamori
title: 'Принцип на работа на Go горутините (Goroutine): 2KB променлив стек и M:N планировчик GMP'
description: Обяснява се разликата между конкурентност и паралелизъм, както и 2KB началния променлив стек на горутините, потребителското (user-level) превключване на контекста, GOMAXPROCS и модела на изпълнение на GMP (Goroutine-Machine-Processor) в рамките на времето за изпълнение (runtime) на Go.
language: bg
date: 2025-08-22T13:28:15.766056383Z
path: /blog/posts/goroutine-basics-z3fbe209e
category: concurrency
---

# Goroutine

Когато молим Gopher-ите да разкажат за предимствата на golang, често се появява статия, посветена на **конкурентността (Concurrency)**. Нейната основа е **goroutine**, която може да се обработва леко и просто. Тук е представено кратко описание на това понятие.

### **Конкурентност (Concurrency) срещу паралелизъм (Parallelism)**

Преди да разгледаме goroutine, искаме да обясним две понятия, които често се объркват.

- Конкурентност: Конкурентността се отнася до обработката на много задачи едновременно. Това не означава непременно, че те действително се изпълняват едновременно, а структурна и логическа концепция, при която множество задачи се разделят на малки части и се изпълняват на редуване, така че за потребителя да изглежда, че няколко задачи се обработват едновременно. Конкурентността е възможна и върху едно ядро.
- Паралелизъм: Паралелизмът е „обработка на множество задачи едновременно на множество ядра“. Буквално, работата се извършва паралелно, като други задачи се изпълняват едновременно.

Goroutine улеснява реализацията на конкурентност чрез планировчика на Go runtime, а чрез настройката `GOMAXPROCS` използва и паралелизъм на естествен начин.

Многонишковостта (Multi thread) в Java, която се използва често, е типичен пример за понятието паралелизъм.

# Защо goroutine са добри?

### Леки са (lightweight)

Разходите за създаване са значително по-ниски в сравнение с други езици. Възниква въпросът защо golang използва толкова малко ресурси? Причината е, че местата за създаване се управляват вътре в Go runtime. Това е така, защото goroutine е лека логическа нишка, по-малка от единица OS нишка, която започва със стек от около 2KB и според реализацията може динамично да се разширява чрез добавяне на стек.

Тъй като се управляват на ниво стек, създаването и премахването им е много бързо и евтино, поради което е възможно работата с милиони goroutine да не натоварва системата. В резултат на това Goroutine, благодарение на планировчика на runtime, може да минимизира намесата на OS kernel.

### Добра производителност (performance)

Първо, Goroutine, както беше описано по-горе, има малка намеса от страна на OS kernel, поради което превключването на контекста на потребителско ниво (User-Level) е по-евтино в сравнение с единица OS нишка и задачите могат да се сменят бързо.

Освен това се използва модел M:N, при който goroutine се разпределят върху OS нишки и се управляват. Създава се пул от OS нишки, така че без да са необходими много нишки, обработката е възможна с малко нишки. Например при състояние на изчакване, като системно извикване, Go runtime изпълнява друга goroutine върху същата OS нишка, така че OS нишката не спира и ефективно използва CPU, което позволява бърза обработка.

Поради това Golang може да постигне висока производителност в сравнение с други езици, особено при I/O операции.

### Компактност (concise)

Също така голямо предимство е, че когато е необходима конкурентност, една ключова дума `go` позволява лесно да се изпълни функция.

Трябва да се използват сложни механизми за заключване, като `Mutex` и `Semaphore`, а при използване на заключвания неизбежно трябва да се вземе предвид състоянието на взаимно блокиране (DeadLock), което прави етапа на проектиране преди разработката по-сложен.

Goroutine следва философията „не споделяй памет за комуникация, а комуникирай, за да споделяш памет“ и препоръчва предаване на данни чрез `канал (Channel)`. `SELECT` се комбинира с канали и поддържа функционалност, която позволява обработка от канала, в който данните са готови. Освен това, с помощта на `sync.WaitGroup` може лесно да се изчака завършването на всички goroutine, което улеснява управлението на работния процес. Благодарение на тези инструменти се предотвратяват състоянията на състезание за данни между нишки и конкурентната обработка е по-безопасна.

Също така, чрез context се управляват на потребителско ниво (User-Level) жизненият цикъл, отмяната, таймаутът, крайният срок и обхватът на заявката, което позволява до известна степен да се гарантира стабилност.

# Паралелна работа на Goroutine (GOMAXPROCS)

Може да възникне въпросът: след като говорихме за добрата конкурентност на goroutine, не поддържа ли тя и паралелизъм? Броят на ядрата на съвременните процесори вече е двуцифрен, а дори домашните компютри често имат значителен брой ядра.

Goroutine обаче извършва и паралелна работа, а това е `GOMAXPROCS`.

Ако `GOMAXPROCS` не е зададен, стойността се определя различно в зависимост от версията.

1. До 1.5: стойност по подразбиране 1; за повече от 1 е необходимо задаване по начин като `runtime.GOMAXPOCS(runtime.NumCPU())`
2. От 1.5 до 1.24: стойността е променена на броя на всички налични логически ядра. От този момент, освен при специфични ограничения, разработчикът обикновено не е необходимо да я задава
3. 1.25: като известен език за контейнерна среда, той проверява cGroup в linux, за да установи настроеното в контейнера `CPU ограничение`.

   Тогава, ако броят на логическите ядра е 10, а CPU ограничението е 5, `GOMAXPROCS` се задава на по-малкото число 5.

Промяната в 1.25 има много голямо значение. Причината е, че се повишава използваемостта на езика в контейнерна среда. Това позволява да се намалят ненужното създаване на нишки и превключването на контекста, като по този начин се предотвратява CPU троттлинг (throttling).

```go
package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"time"
)

func exe(name int, wg *sync.WaitGroup) {
	defer wg.Done()

	fmt.Printf("Goroutine %d: 시작\n", name)
	time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연
	fmt.Printf("Goroutine %d: 시작\n", name)
}

func main() {
	runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용
	wg := sync.WaitGroup();
  goroutineCount := 10
	wg.Add(goroutineCount)

	for i := 0; i < goroutineCount; i++ {
		go exe(i, &wg)
	}

	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...")
	wg.Wait()
	fmt.Println("모든 작업이 완료되었습니다.")

}

```

# Планировчик на Goroutine (модел M:N)

Във връзка с по-горе споменатото разпределение върху OS нишки чрез модела M:N, по-подробно съществува GMP моделът на goroutine.

- G (Goroutine): Най-малката единица работа, която се изпълнява в Go
- M (Machine): OS нишка (реалното място на изпълнение)
- P (Processor): Логически процес, управляван от Go runtime

Това са съставните части. P допълнително притежава локална опашка за изпълнение (Local Run Queue) и изпълнява ролята на планировчик, който назначава присвоените G на M. Накратко, goroutine работи по следния начин:

Работният процес на GMP е както следва:

1. Когато се създаде G (Goroutine), то се поставя в локалната опашка за изпълнение на P (Processor).
2. P (Processor) назначава G (Goroutine) от локалната опашка за изпълнение към M (Machine).
3. M (Machine) връща състоянието на G (Goroutine): block, complete или preempted.
4. Work-Stealing (кражба на работа): Ако локалната опашка за изпълнение на P се изпразни, другият P първо проверява глобалната опашка. Ако и там няма G (Goroutine), той краде работа от локалната опашка на друг P (Processor), така че всички M да работят без прекъсване.
5. Обработка на системни извиквания (Blocking): Когато по време на изпълнение на G (Goroutine) възникне блокиране, M (Machine) влиза в състояние на изчакване. В този момент P (Processor) се отделя от блокирания M (Machine), свързва се с друг M (Machine) и изпълнява следващия G (Goroutine). По този начин при изчакване по време на I/O операции не се губи CPU време.
6. Ако една G (Goroutine) заема процесора прекомерно дълго (preempted), на други G (Goroutine) се дава възможност за изпълнение.

Golang изпълнява и GC (Garbage Collector) върху Goroutine, като почиства паметта паралелно, докато прекъсването на изпълнението на приложението (STW) се свежда до минимум, което позволява ефективно използване на системните ресурси.

И накрая, Golang е един от силните аспекти на езика и има още много други, затова се надявам много разработчици да се насладят на Go.

Благодаря ви.
