Modelli di threading in Java

1. Introduzione

Spesso nelle nostre applicazioni dobbiamo essere in grado di fare più cose contemporaneamente. Possiamo raggiungere questo obiettivo in diversi modi, ma la chiave tra questi è implementare il multitasking in qualche forma.

Multi-tasking significa eseguire più attività contemporaneamente , in cui ciascuna attività svolge il proprio lavoro. Queste attività in genere vengono eseguite tutte nello stesso momento, leggendo e scrivendo la stessa memoria e interagendo con le stesse risorse, ma facendo cose diverse.

2. Thread nativi

Il modo standard per implementare il multi-tasking in Java è utilizzare i thread . Il threading è generalmente supportato fino al sistema operativo. Chiamiamo thread che funzionano a questo livello "thread nativi".

Il sistema operativo ha alcune capacità di threading che spesso non sono disponibili per le nostre applicazioni, semplicemente a causa di quanto sia più vicino all'hardware sottostante. Ciò significa che l'esecuzione di thread nativi è in genere più efficiente. Questi thread vengono mappati direttamente ai thread di esecuzione sulla CPU del computer e il sistema operativo gestisce la mappatura dei thread sui core della CPU.

Il modello di threading standard in Java, che copre tutti i linguaggi JVM, utilizza thread nativi . Questo è stato il caso da Java 1.2 ed è il caso indipendentemente dal sistema sottostante su cui è in esecuzione la JVM.

Ciò significa che ogni volta che utilizziamo uno dei meccanismi di threading standard in Java, utilizziamo thread nativi. Ciò include java.lang.Thread , java.util.concurrent.Executor , java.util.concurrent.ExecutorService e così via.

3. Fili verdi

Nell'ingegneria del software, un'alternativa ai thread nativi sono i thread verdi . Qui è dove stiamo usando i thread, ma non vengono mappati direttamente ai thread del sistema operativo. Invece, l'architettura sottostante gestisce i thread stessi e gestisce il modo in cui questi vengono associati ai thread del sistema operativo.

In genere questo funziona eseguendo diversi thread nativi e quindi allocando i thread verdi su questi thread nativi per l'esecuzione . Il sistema può quindi scegliere quali thread verdi sono attivi in ​​un dato momento e su quali thread nativi sono attivi.

Sembra molto complicato, e lo è. Ma è una complicazione di cui generalmente non dobbiamo preoccuparci. L'architettura sottostante si prende cura di tutto questo, e possiamo usarla come se fosse un modello di threading nativo.

Allora perché dovremmo farlo? I thread nativi sono molto efficienti da eseguire, ma hanno un costo elevato per l'avvio e l'arresto. I fili verdi aiutano a evitare questo costo e danno all'architettura molta più flessibilità. Se stiamo usando thread a esecuzione relativamente lunga, i thread nativi sono molto efficienti. Per lavori di brevissima durata, il costo per avviarli può superare il vantaggio derivante dal loro utilizzo . In questi casi, i fili verdi possono diventare più efficienti.

Sfortunatamente, Java non ha il supporto integrato per i thread verdi.

Le prime versioni utilizzavano thread verdi invece di thread nativi come modello di threading standard. Questo è cambiato in Java 1.2 e da allora non è stato supportato a livello di JVM.

È anche difficile implementare thread verdi nelle librerie perché avrebbero bisogno di un supporto di livello molto basso per funzionare bene. In quanto tale, un'alternativa comune utilizzata sono le fibre.

4. Fibre

Le fibre sono una forma alternativa di multi-threading e sono simili ai fili verdi . In entrambi i casi, non stiamo usando thread nativi e invece stiamo usando i controlli di sistema sottostanti che sono in esecuzione in qualsiasi momento. La grande differenza tra fili verdi e fibre è nel livello di controllo, e in particolare chi ha il controllo.

I fili verdi sono una forma di multitasking preventivo. Ciò significa che l'architettura sottostante è interamente responsabile della decisione dei thread in esecuzione in un dato momento.

Ciò significa che si applicano tutti i soliti problemi del threading, in cui non sappiamo nulla sull'ordine di esecuzione dei nostri thread o su quali verranno eseguiti contemporaneamente. Significa anche che il sistema sottostante deve essere in grado di mettere in pausa e riavviare il nostro codice in qualsiasi momento, potenzialmente nel mezzo di un metodo o anche di un'istruzione.

Le fibre sono invece una forma di multitasking cooperativo, il che significa che un thread in esecuzione continuerà a funzionare finché non segnala che può cedere a un altro . Significa che è nostra responsabilità che le fibre cooperino tra loro. Questo ci mette in controllo diretto su quando le fibre possono mettere in pausa l'esecuzione, invece di decidere il sistema per noi.

Ciò significa anche che dobbiamo scrivere il nostro codice in un modo che lo consenta. Altrimenti non funzionerà. Se il nostro codice non ha punti di interruzione, allora potremmo anche non utilizzare affatto le fibre.

Java attualmente non ha il supporto integrato per le fibre. Esistono alcune librerie che possono introdurre questo nelle nostre applicazioni, incluse ma non limitate a:

4.1. Quasar

Quasar è una libreria Java che funziona bene con Java puro e Kotlin e ha una versione alternativa che funziona con Clojure.

Funziona con un agente Java che deve essere eseguito insieme all'applicazione e questo agente è responsabile della gestione delle fibre e garantisce che funzionino correttamente insieme. L'uso di un agente Java significa che non sono necessari passaggi di compilazione speciali.

Quasar richiede anche che Java 11 funzioni correttamente in modo da limitare le applicazioni che possono utilizzarlo. Le versioni precedenti possono essere utilizzate su Java 8, ma non sono supportate attivamente.

4.2. Kilim

Kilim è una libreria Java che offre funzionalità molto simili a Quasar ma lo fa utilizzando la tessitura bytecode invece di un agente Java . Ciò significa che può funzionare in più posti, ma rende il processo di compilazione più complicato.

Kilim funziona con Java 7 e versioni successive e funzionerà correttamente anche in scenari in cui un agente Java non è un'opzione. Ad esempio, se uno diverso è già utilizzato per la strumentazione o il monitoraggio.

4.3. Progetto Loom

Project Loom è un esperimento del progetto OpenJDK per aggiungere fibre alla stessa JVM, piuttosto che come libreria aggiuntiva . Questo ci darà i vantaggi delle fibre rispetto ai fili. Implementandolo direttamente sulla JVM, può aiutare a evitare le complicazioni introdotte dagli agenti Java e dalla tessitura bytecode.

Non esiste un programma di rilascio corrente per Project Loom, ma possiamo scaricare i binari di accesso anticipato in questo momento per vedere come stanno andando le cose. Tuttavia, poiché è ancora molto presto, dobbiamo stare attenti a fare affidamento su questo per qualsiasi codice di produzione.

5. Co-routine

Le co-routine sono un'alternativa alla filettatura e alle fibre. Possiamo pensare alle co-routine come fibre senza alcuna forma di programmazione . Invece del sistema sottostante che decide quali attività vengono eseguite in qualsiasi momento, il nostro codice lo fa direttamente.

Generalmente, scriviamo co-routine in modo che cedano in punti specifici del loro flusso. Questi possono essere visti come punti di pausa nella nostra funzione, dove smetterà di funzionare e potenzialmente produrrà alcuni risultati intermedi. Quando cediamo, veniamo fermati finché il codice chiamante non decide di riavviarci per qualsiasi motivo. Ciò significa che il nostro codice chiamante controlla la pianificazione di quando verrà eseguito.

Kotlin ha il supporto nativo per le co-routine incorporate nella sua libreria standard. Ci sono molte altre librerie Java che possiamo usare per implementarle anche se lo desideri.

6. Conclusione

Abbiamo visto diverse alternative per il multi-tasking nel nostro codice, che vanno dai tradizionali thread nativi ad alcune alternative molto leggere. Perché non provarli la prossima volta che un'applicazione necessita di concorrenza?