---
id: 208cfd8127284f07337b5f5002f3351d
author: hamori
title: 'Funzionamento delle goroutine in Go: stack variabile da 2KB e scheduler GMP M:N'
description: Verranno illustrate le differenze tra concorrenza e parallelismo, lo stack variabile iniziale di 2KB delle goroutine, il context switching a livello utente, GOMAXPROCS e il modello di scheduling del runtime GMP (Goroutine-Machine-Processor).
language: it
date: 2025-08-22T13:28:15.766056383Z
path: /blog/posts/goroutine-basics-z3fbe209e
category: concurrency
---

# Goroutine

Quando si chiede ai Gopher di parlare dei vantaggi di Golang, compare spesso un argomento legato alla **concorrenza (Concurrency)**. Il suo fondamento è la **goroutine (goroutine)**, che consente di gestire le operazioni in modo leggero e semplice. Di seguito ne presentiamo una breve descrizione.

### **Concorrenza (Concurrency) vs Parallelismo (Parallelism)**

Prima di comprendere le goroutine, desideriamo chiarire due concetti che vengono spesso confusi.

- Concorrenza: la concorrenza riguarda la gestione di molte operazioni nello stesso intervallo di tempo. Non significa necessariamente che vengano eseguite simultaneamente, ma è un concetto strutturale e logico che, suddividendo più attività in unità piccole ed eseguendole alternativamente, fa apparire agli utenti che più operazioni vengano elaborate contemporaneamente. La concorrenza è possibile anche su un singolo core.
- Parallelismo: il parallelismo consiste nell'"elaborare più operazioni contemporaneamente su più core". Letteralmente, indica l'avanzamento parallelo del lavoro, con l'esecuzione simultanea di attività diverse.

Le goroutine permettono di implementare facilmente la concorrenza tramite lo scheduler runtime di Go e sfruttano naturalmente anche il parallelismo attraverso la configurazione di `GOMAXPROCS`.

Il multithreading (Multi thread) di Java, molto diffuso, è un esempio rappresentativo del parallelismo.

# Perché le goroutine sono vantaggiose?

### Leggerezza (lightweight)

Il costo di creazione è notevolmente inferiore rispetto ad altri linguaggi. A questo punto sorge il dubbio sul perché Golang ne utilizzi così poche: ciò accade perché il luogo di creazione è gestito all'interno del runtime di Go. Infatti, si tratta di un thread logico leggero, più piccolo dell'unità dei thread del sistema operativo, che richiede inizialmente uno stack di circa 2KB e può essere ampliato dinamicamente aggiungendo stack secondo l'implementazione dell'utente.

Poiché viene gestita a livello di stack, la creazione e la rimozione sono molto rapide ed economiche, e l'esecuzione di centinaia di migliaia di goroutine risulta gestibile senza difficoltà. Grazie allo scheduler runtime, le goroutine possono inoltre ridurre al minimo l'intervento del kernel del sistema operativo.

### Prestazioni (performance)

Innanzitutto, come illustrato sopra, le goroutine comportano un intervento limitato del kernel del sistema operativo; pertanto, il cambio di contesto a livello utente (User-Level) costa meno rispetto all'unità dei thread del sistema operativo, consentendo di passare rapidamente da un'operazione all'altra.

Inoltre, il modello M:N viene utilizzato per assegnare e gestire le goroutine sui thread del sistema operativo. Tramite un pool di thread del sistema operativo, è possibile elaborare il lavoro con un numero ridotto di thread, senza doverne creare molti. Ad esempio, quando si entra in uno stato di attesa, come una chiamata di sistema, il runtime di Go può eseguire un'altra goroutine sul thread del sistema operativo; in tal modo, i thread del sistema operativo non restano inattivi e utilizzano la CPU in modo efficiente, garantendo un'elaborazione rapida.

Per questo motivo Golang può offrire prestazioni superiori rispetto ad altri linguaggi, in particolare nelle operazioni di I/O.

### Concisione (concise)

Quando è necessaria la concorrenza, è un grande vantaggio poter eseguire una funzione con la sola parola chiave `go`.

Con `Mutex`, `Semaphore` e altri lock complessi, è necessario considerare lo stato di deadlock (DeadLock), inevitabile quando si usano i lock, e questo rende la fase di progettazione, prima dello sviluppo, decisamente complessa.

Le goroutine seguono la filosofia "non comunicare condividendo la memoria, ma condividi la memoria comunicando", raccomandando la trasmissione dei dati tramite il `canale (Channel)`; inoltre, `SELECT` si combina con i canali (Channel) e consente di elaborare per primo il canale su cui i dati sono pronti. Inoltre, utilizzando `sync.WaitGroup` è possibile attendere in modo semplice il completamento di più goroutine, facilitando la gestione del flusso di lavoro. Grazie a questi strumenti è possibile prevenire le race condition tra thread e gestire la concorrenza in modo più sicuro.

Inoltre, tramite il context (context) è possibile controllare, a livello utente (User-Level), il ciclo di vita, la cancellazione, i timeout, le scadenze e l'ambito delle richieste, garantendo così un certo grado di stabilità.

# Parallelismo delle goroutine (GOMAXPROCS)

Dopo aver illustrato i vantaggi della concorrenza delle goroutine, sorgerà certamente il dubbio se anche il parallelismo sia supportato. Il numero di core delle CPU recenti supera ormai le due cifre, e anche i PC domestici dispongono di un numero considerevole di core.

Le goroutine, tuttavia, eseguono anche il lavoro parallelo; questo è il compito di `GOMAXPROCS`.

Se `GOMAXPROCS` non viene configurato, il valore predefinito varia a seconda della versione.

1. Prima della 1.5: valore predefinito 1; per utilizzare più di 1 è necessario impostarlo, ad esempio con `runtime.GOMAXPOCS(runtime.NumCPU())`
2. Dalla 1.5 alla 1.24: è stato modificato in modo da utilizzare tutti i core logici disponibili. Da questo momento, a meno che non vi siano particolari restrizioni, non è più necessario configurarlo
3. 1.25: in linea con la reputazione di linguaggio orientato agli ambienti container, verifica nel cgroup di Linux il `limite di CPU` configurato per il container.

   Pertanto, se il numero di core logici è 10 e il limite di CPU è 5, `GOMAXPROCS` viene impostato al valore inferiore, cioè 5.

La modifica della 1.25 presenta una portata molto significativa, perché aumenta l'utilizzabilità del linguaggio negli ambienti container. Ciò consente di ridurre la creazione superflua di thread e il cambio di contesto, prevenendo il throttling della 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("모든 작업이 완료되었습니다.")

}

```

# Lo scheduler delle goroutine (modello M:N)

Per approfondire il punto precedente sull'assegnazione delle goroutine ai thread del sistema operativo tramite il modello M:N, esiste il modello GMP delle goroutine.

- G (Goroutine): l'unità di lavoro più piccola eseguita in Go
- M (Machine): thread del sistema operativo (luogo effettivo di esecuzione)
- P (Processor): processo logico gestito dal runtime di Go

Il P possiede inoltre una coda di esecuzione locale (Local Run Queue) e svolge il ruolo di scheduler che assegna le G alle M. In breve, il funzionamento del GMP procede come segue:

1. Quando viene creata una G (Goroutine), questa viene assegnata alla coda di esecuzione locale di un P (Processor).
2. Il P (Processor) assegna la G (Goroutine) presente nella coda di esecuzione locale a una M (Machine).
3. La M (Machine) restituisce lo stato della G (Goroutine), che può essere block, complete o preempted.
4. Work-Stealing (furto di lavoro): se la coda di esecuzione locale di un P risulta vuota, l'altro P controlla la coda globale. Qualora anche lì non vi siano G (Goroutine), viene sottratto lavoro alla coda locale di un altro P (Processor), in modo che tutte le M operino senza sosta.
5. Gestione delle chiamate di sistema (Blocking): quando una G (Goroutine) entra in stato di block durante l'esecuzione, la M (Machine) passa allo stato di attesa; in quel momento il P (Processor) si separa dalla M (Machine) bloccata e si associa a un'altra M (Machine) per eseguire la G (Goroutine) successiva. Durante questa attesa, anche nelle operazioni di I/O, non si verifica alcuno spreco di CPU.
6. Se una G (Goroutine) occupa a lungo la CPU per prelazione (preempted), viene data l'opportunità di eseguire alle altre G (Goroutine).

Il GC (Garbage Collector) di Golang viene anch'esso eseguito sopra le Goroutine e, riducendo al minimo l'interruzione dell'esecuzione dell'applicazione (STW), può liberare la memoria in parallelo, utilizzando in modo efficiente le risorse di sistema.

Infine, Golang è uno dei grandi punti di forza del linguaggio e, oltre a questi, ce ne sono molti altri; auguro a tanti sviluppatori di godere di Go.

Grazie.
