Sobes.tech
Middle+

Разкажете за планирането на goroutine в Go.

sobes.tech AI

Отговор от AI

Планировчикът на горутините в Go използва модел M:N, където M горутини се мапват на N нишки на ОС. Той ефективно превключва между горутините, реализирайки невытесняваща многозадачност с точки на прекъсване.

Основните компоненти на модела GPM:

  • G (Goroutine): Абстракция на изпълняваната единица код, лек поток, управляван от планировчика на Go.
  • P (Processor): Логически процесор, представляващ контекст на локалната опашка на горутините и ресурсите като мьютекси и планировчика. Броят на P по подразбиране се определя от броя на логическите ядра на процесора, но може да бъде променен чрез GOMAXPROCS.
  • M (Machine/Thread): Поток на операционната система, който изпълнява кода. M е свързан с P и изпълнява горутини от локалната опашка на P или глобалната опашка.

Принцип на работа:

  1. Новата горутина се създава и поставя в локалната опашка на наличния P.
  2. M, свързан с P, взема горутина от локалната опашка и я изпълнява.
  3. Ако локалната опашка е празна, M се опитва да открадне горутини от други P.
  4. Ако P блокира (например при системно извикване), свързаният с него M се отделя, а друг M се свързва с този P. Блокираният M може да бъде повторно използван, когато системното извикване приключи.
  5. Ако горутината е блокирана на канал или мьютекс, тя се поставя в опашка за изчакване, а планировчикът избира друга горутина за изпълнение на същия M.
  6. Планировчикът извършва периодични проверки за предсрочно прекъсване на дълго работещи горутини, позволявайки на други горутини да получат време за изпълнение.

Точки на предсрочно прекъсване (preemption points) възникват на определени места:

  • Викащи функции/методи.
  • Цикли, съдържащи извиквания на функции.
  • Алокация на памет.
  • Синхронизиращи примитиви (канали, мьютекси).

Пример за планиране:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker(id int) {
	fmt.Printf("Горутината %d започна работа\n", id)
	time.Sleep(time.Second) // Имитация на работа
	fmt.Printf("Горутината %d приключи работа\n", id)
}

func main() {
	runtime.GOMAXPROCS(2) // Настройка на броя логически процесори

	for i := 1; i <= 5; i++ {
		go worker(i) // Създаване на горутини
	}

	time.Sleep(3 * time.Second) // Изчакване за завършване на горутините
	fmt.Println("Основната горутина е приключила")
}

В този пример 5 горутини се планират на 2 логически процесора. Планировчикът на Go ще разпределя тези горутини между наличните потоци на ОС, използвайки локални и евентуално глобални опашки, както и механизма "кражба" на работа (work stealing).

Предимства на този подход:

  • Мащабируемост: Ефективно използване на многоядрени системи.
  • Нисък накладен разход: Леките горутини в сравнение с потоците на ОС.
  • Лесна употреба: Вграденото планиране в езика прави паралелното програмиране по-достъпно.