Thread in Go (al massimo sola lettura)

Thread in Go (al massimo sola lettura)

CARATTERISTICHE
  • In go creare thread è molto semplice, si chiamano goroutine
    • la funzione main è una goroutine
  • possono essere creati e distrutti dinamicamente (in runtime)
    • per creare un thread in go, al momento della chiamata precedo il nome della funzione con la keyword go nome_thread
    • Al termine del thread, tutte le sue strutture dati vengono deallocate
  • i thread sono indipendenti dal main, in Go, e quando faccio molteplici chiamate uno sotto l’altro, la cpu deve eseguirli tutti, è sequenziale ma non aspetta la fine dell’esecuzione come le funzioni
    • se finisce di eseguire il main, il programma si chiude indipendentemente dall’esecuzione non terminata dei thread

Evolvendo l’esercizio sui thread, dimostra come nn c’è simultaneità nell’esecuzione dei thread, pk la CPU è una sola, e implicazioni
ESERCIZIO: stampa a consolle un asterisco ogni mezzo secondo
BUSY WAITING
  • Il prof crea un for annidato
  • quello esterno è infinito e stampa ciclicamente un asterisco
  • quello interno termina alla fine di un tempo specificato → 500 millisecondi, implementato con la libreria time
  • il corpo del for interno è vuoto, il for serve solo ripetere quello esterno esclusivamente quando termina il tempo
CALL SYSTEM
PROBLEMA
Questo sistema ha un costo computazionale che può essere ottimizzato, pk il col secondo for, la CPU verifica ad ogni millisecondo se sono passati 500 ms → busy waiting
SOLUZIONE
  • Implementa al posto del for interno una system call Sleep che mette in pausa il processo per un tempo specificato, quindi lancerà un interrupt di clock non appena passano i 500ms
SVILUPPO ULTERIORE: parametrizzare/delegare la stampa 1 poi molteplici threads che stampano allo stesso ritmo
  • Crea molteplici threads chiamati dal main che stampano per 10s
  • Per ognuno specifica 2 parametri in input, la stringa da stampare e un intero per determinare ogni quanto tempo nello sleep
  • ogni thread ha la sua implementazione del for contenente il codice di stampa e di attesa
    • Nel main
  • ho le chiamate ai threads
  • Aggiunge 10s di sleep dopo le chiamate, nel main
STAMPA NON SIMUTLANEA
  • anche se 2 thread hanno lo stesso tempo, nn saranno stampati simultaneamente, lo scheduler sceglie quale prioritizzare
SVILUPPO ULTERIORE: controllare la terminazione di un thread senza terminare l’intero programma
  • Diversamente da una funzione, la terminazione di un thread nn si può controllare dall’esterno, quindi usa una variabile globale flag e le assegna il valore 1, ciclo for implementato in ogni thread prima era infinito, ora cicla solo se flag=1
  • nel main specifica che passati i 10s, la flag = 0 ed i threads terminano
  • seguono altri 5s, in cui il programma è ancora aperto, poi si chiude
PROBLEMA
  • Mancanza di località e flessibilità
    • Se in futuro volessi aggiungere altre flag per terminare ogni thread in momenti differenti, dovrei inizializzarli globalmente e implementare manualmente il nome delle variabili nelle chiamate del main e in ogni thread
SOLUZIONE
  • parametrizzo anche la flag (3° parametro), e lo passo non per valore (pk ne passerei solo una copia ed io voglio poi cambiare valore a tutti i flag on modo flessibile dal main) bensì per puntatore, in questo modo posso specificare quale variabile assegnare dalle chiamate nel main ed i thread si adeguano automaticamente
  • In questo modo qualsiasi modifica al valore di flag nel main influenza anche il thread senza necessità di aggiornamenti manuali
    • flag1 := 1 flag2 := 1 go bumper("*", 333, &flag1) // Gruppo 1 usa flag1 go bumper("?", 444, &flag2) // Gruppo 2 usa flag2 func bumper(mark string, delay int, &flag) { //se cambio nome variabile, il valore rimane pk passo il puntatore fmt.Print(mark) } }
MANCANZA DI ATOMICITA → RACE CONDITION
La sincronizzazione non è simultanea in tutti i threads. Anche se le modifiche al valore di flag sono visibili tra i thread grazie al puntatore, la gestione dell'ordine in cui queste modifiche avvengono dipende dallo scheduler che assegna alla CPU un thread alla volta.
  • questo problema si verificherebbe anche in caso di variabile globale
Dimostra quindi la necessità di meccanismi di sincronizzazione quando si usano risorse condivise
PROBLEMA DELLA BANCA
Necessità di meccanismi di SINCRONIZZAZIONE
ess
package main import ( "fmt" "time" ) type Account struct { balance int } func sleep(t int) { time.Sleep(time.Duration(t) * time.Millisecond) } func main() { account := Account{1000} invest := 1 thinkTime := 10 // set this to 0 to see data inconsistency watchTime := 500 go broker(&account, invest, thinkTime) go broker(&account, invest, thinkTime) go observer(&account, watchTime) sleep(10000) } func broker(a *Account, qtt int, delay int) { for { a.balance -= qtt sleep(delay) a.balance += qtt } } func observer(a *Account, delay int) { for { fmt.Println(a.balance) sleep(delay) } }
  • Io ho un account iniziale di 1000
  • creo una funzione per monitorarlo: un osservatore fa un report di quanti soldi ci sono ogni tot millisecondi
  • Creo un’altra funzione che opera sull’account: implemento un broker che prima prende una qtà x e poi la rimette nel conto con un ritardo di 100 millisecondi
    • notion image
  • chiamo la funzione dal main 2 volte uno sotto l’altro
    • notion image
      OUTPUT: Funziona, ma crea inconsistenza, in quanto il valore non rimane costante
SPIEGAZIONE:
  • il main, come spiegato prima esegue il suo codice indipendentemente dalle chiamate ai threads e quando termina arresta tutto
  • anche se ogni thread è indipendente e rimane aperto fino alla terminazione del codice nel main, la CPU rimane una sola, quindi deve arrangiarsi ad eseguire il main, il primo thread ed il secondo thread, ma senza meccanismi di sincronizzazione, causa race condition:
    • Esempio
      Broker 1: preleva 1 → balance = 999 → si ferma in sleep(10).
      1. Broker 2: preleva 1 → balance = 998.
      1. Entrambi poi restituiscono 1, e il saldo torna corretto.
      Tuttavia, nel mezzo, observer potrebbe stampare valori come 999 o 998, che sono momentaneamente inconsistenti.