La semplice risposta è che non tutti i thread sono in esecuzione contemporaneamente. Per una spiegazione più completa, continua a leggere.
Il programma di pianificazione delle operazioni del sistema operativo è generalmente pensato per pianificare le applicazioni e, così facendo, consente di eseguire un'attività mentre il computer sta lavorando su un'altra. Ai vecchi tempi, la cartina di tornasole del multitasking stava formattando un floppy disk mentre faceva qualcos'altro. Se si desidera veramente mettere il sistema operativo alla prova, si formattare un disco floppy durante il download di un file su un modem collegato alla porta seriale. Dato che l'hardware è diventato abbastanza potente da farlo davvero in modo significativo, la riproduzione del video a volte è stata inclusa anche in questi test. Se il task scheduler del sistema operativo fosse in grado di gestire senza problemi queste attività, allora potrebbe gestire qualsiasi cosa.
Tuttavia, l'utilità di pianificazione non pianifica realmente le applicazioni (processi), ma pianifica discussioni . Ogni applicazione ha almeno un thread, ma può potenzialmente utilizzare un numero elevato di thread per dividere il lavoro che fa in parti correlate o indipendenti. Ad esempio, è comune che un'applicazione contenga un thread che gestisce l'interfaccia utente e che crei un altro thread quando l'utente avvia un'operazione potenzialmente di lunga durata (che potrebbe essere un'operazione come la stampa, il ricalcolo di un foglio di calcolo, l'esecuzione di un ambiente di sviluppo) una ricerca di simboli, ecc. ecc.). Alcuni ambienti di programmazione introducono una certa quantità di thread invisibili al programmatore; per esempio, Giava e .NETTO potresti fare raccolta dei rifiuti in un thread separato, che è fuori dal controllo immediato del programmatore. Alcuni programmi creano presto un certo numero di thread e li raggruppano, perché la creazione di nuovi thread è un'operazione relativamente costosa (quindi non è necessario creare un thread ogni volta che ne hai bisogno). Tutto ciò che fa un'anteprima viene solitamente eseguito in un thread separato, quindi il resto dell'interfaccia utente rimane reattivo mentre viene generata l'anteprima. E così via. Presi insieme, tutto ciò significa che il numero di thread sul sistema in qualsiasi momento può facilmente essere molte volte il numero di processi.
Ogni thread può essere in uno dei pochi stati possibili, ma la distinzione più importante è tra in esecuzione , eseguibile e in attesa stati; la terminologia può differire leggermente, ma questa è l'idea generale. In qualsiasi momento, solo un thread per virtual (a causa di hyperthreading e tecnologie simili) può essere il core della CPU in esecuzione (cioè, eseguendo le istruzioni del codice macchina), ma qualsiasi numero di thread può essere eseguibile (significa che è un candidato ad ottenere la CPU la prossima volta che lo scheduler deve prendere una decisione su quale thread dovrebbe essere autorizzato a funzionare). In attesa (noto anche come thread bloccato) sono proprio questo, in attesa di qualcosa - i casi più comuni probabilmente sono che è in attesa di utente, disco o I / O di rete (l'input dell'utente, in particolare, è eccezionalmente lento).
Il conteggio dei thread che vedi nel task manager è il numero totale di thread in uno di questi stati. Ad esempio, il sistema Windows 7 su cui sto scrivendo attualmente ha circa 70 processi avviati, ma quasi 900 thread. Con tutti i processi in background per gestire vari compiti e come sono probabilmente suddivisi in una moltitudine di thread ciascuno, questo non è un numero eccessivo.
Andando un po 'più in profondità nell'implementazione tecnica, il fulcro di un programmatore di task del sistema operativo multitasking pre-empiricamente è solitamente un qualche tipo di hook di interrupt hardware. Ciò significa che il kernel può arrestare la CPU quando non ha lavoro utile da eseguire (questo è quasi certamente uno dei motivi, se non il motivo, perché Linux controlla il HLT
istruzione all'avvio IA-32 CPU -compatibili, e probabilmente esegue controlli simili su altre architetture), senza la certezza che in alcuni momenti futuri ragionevolmente determinati, un interrupt verrà attivato e l'utilità di pianificazione sarà invocata. Poiché l'interrupt si attiva indipendentemente da quale altro lavoro la CPU sta eseguendo (è l'idea alla base degli interrupt), lo scheduler viene eseguito regolarmente e ottiene la possibilità di determinare quale thread deve essere eseguito durante il seguente intervallo di tempo. Poiché i commutatori di contesto sono relativamente costosi, di solito è possibile (almeno attraverso il codice sorgente) sintonizzare quanto aggressivamente lo schedulatore passa da un thread all'altro; il cambio di thread più spesso fa sì che il sistema diventi più reattivo, ma il sovraccarico di commutazione significa che il tempo complessivo per terminare una determinata serie di attività è più lungo. Il più veloce il sistema sarà uno che passa solo tra i thread quando il thread in esecuzione non è più possibile eseguire (ovvero viene bloccato in attesa di qualcosa, o ha finito il suo lavoro) perché riduce al minimo il sovraccarico, mentre il più reattivo il sistema passerà da un thread all'altro ogni volta che lo scheduler viene richiamato perché riduce al minimo il tempo medio di attesa prima che un particolare thread ottenga il tempo della CPU. L'impostazione ideale di solito è una via di mezzo tra questi due, e il compromesso tra queste scelte è probabilmente uno dei motivi principali per cui Linux offre diversi scheduler tra cui scegliere e alcuni parametri di ottimizzazione attraverso la configurazione del kernel.
D'altro canto, i sistemi operativi e gli ambienti operativi multitasking multitasking ( Windows 3.x essendo un esempio), affidarsi a ciascuna applicazione per cedere regolarmente il controllo allo scheduler. Di solito c'è una funzione API specificatamente pensata per farlo, e spesso molte funzioni API lo fanno come parte del loro flusso di esecuzione interno, perché aiuta a rendere l'esperienza utente più fluida. Questo approccio alla progettazione funziona bene purché tutte le applicazioni siano ben educate e cedano il controllo con brevi intervalli durante qualsiasi operazione di lunga durata (a lungo termine significa più di una piccola frazione di secondo), ma un'applicazione che non può intasare l'intero sistema. Questo è uno dei motivi principali per cui Windows 3.x ha fatto così male nel test multitasking che ho citato sopra, mentre OS / 2 passeggiava allegramente mentre eseguiva gli stessi compiti sullo stesso hardware: un'applicazione poteva dire all'unità disco floppy di scrivere un determinato settore e il tempo necessario per farlo prima che la chiamata fosse effettivamente misurabile (da decine a centinaia di millisecondi o Di Più); un sistema multitasking preventivamente avrebbe il suo scheduler irrompere nella sua prossima chiamata programmata, notare che il thread che è attualmente in esecuzione è effettivamente bloccato dalla chiamata di scrittura e passa semplicemente a un altro thread che è eseguibile. (In pratica è un po 'più coinvolto, ma questa è l'idea generale).
In entrambi gli ambienti di multitasking e cooperazione preventivamente, esiste anche la possibilità che diversi thread abbiano priorità diverse. Ad esempio, è probabilmente più importante eseguire in modo tempestivo il thread che sta ricevendo i dati su un collegamento di comunicazione rispetto a quello che aggiorna la visualizzazione dell'ora di sistema, quindi il thread ricevente ha priorità alta e il thread di aggiornamento della visualizzazione del tempo ha bassa priorità . Le priorità dei thread hanno un ruolo nella decisione dello scheduler su quale thread consentire di eseguire (ad esempio, molto semplificato i thread con priorità alta devono sempre essere eseguiti prima dei thread a bassa priorità, quindi, anche se il thread con priorità bassa ha del lavoro da svolgere, se il thread ad alta priorità diventa eseguibile ha la precedenza), ma tali decisioni di pianificazione specifiche non influiscono sulla progettazione del meccanismo sottostante.