{"id":326,"date":"2012-08-21T16:26:14","date_gmt":"2012-08-21T14:26:14","guid":{"rendered":"http:\/\/localhost\/blog\/?p=326"},"modified":"2013-03-07T15:37:22","modified_gmt":"2013-03-07T14:37:22","slug":"tutorial-multitasking-e-cpu-multicore","status":"publish","type":"post","link":"https:\/\/www.ilbytecidio.it\/?p=326","title":{"rendered":"Tutorial: multitasking e CPU multicore"},"content":{"rendered":"<p style=\"text-align: justify;\">I processori in commercio per i Personal Computer raramente ormai hanno meno di due core, ed \u00e8 piuttosto comune che ne abbiano quattro o pi\u00f9. I nostri computer sono quindi equipaggiati con due o pi\u00f9 unit\u00e0 di elaborazione e sono perci\u00f2 in grado di svolgere pi\u00f9 di un compito per volta<br \/>\nSe questo gi\u00e0 \u00e8 un vantaggio per chi ha molte applicazioni aperte contemporaneamente, potrebbe non essere un gran beneficio per chi invece esegue un programma alla volta, magari piuttosto pesante, se i suoi programmi non sono ottimizzati per sfruttare le qualit\u00e0 degli odierni calcolatori.<img loading=\"lazy\" decoding=\"async\" class=\"wp-image-478 aligncenter\" style=\"border: 0px none; margin-top: 2px; margin-bottom: 2px;\" alt=\"Utilizzo parallelo CPU \" src=\"http:\/\/www.ilbytecidio.it\/wp-content\/uploads\/2012\/08\/CPU.png\" width=\"254\" height=\"100\" \/><\/p>\n<p><!--more--><\/p>\n<p><strong>Indice<\/strong><\/p>\n<ul>\n<li>Multitasking, multithreading<\/li>\n<li>Programmazione e librerie software<\/li>\n<li>I problemi<\/li>\n<li>Gli esempi<\/li>\n<li>Esempio 1: Multitasking in C su sistemi POSIX con pipe<\/li>\n<li>Esempio 2: Multitasking in C su sistemi POSIX con memoria condivisa<\/li>\n<li>Esempio 3: Multitasking in C su Windows con pipe<\/li>\n<li>Esempio 4: Multithreading in C con PThread<\/li>\n<li>Esempio 5: Multithreading in Java<\/li>\n<li>Esempio 6: Multithreading in C# su piattaforma .NET<\/li>\n<\/ul>\n<p><strong>Multitasking, multithreading<\/strong><br \/>\nLa maggior parte dei sistemi operativi \u00e8 capace da tempo di eseguire pi\u00f9 di un programma alla volta dandoci l&#8217;illusione che questi vengano eseguiti in parallelo anche con processori a singolo core e danno la possibilit\u00e0 a questi programmi di comunicare, se necessario. Questi sistemi sono, insomma, <em>multitasking<\/em>. Senza questa possibilit\u00e0 saremmo fermi all&#8217;MS-DOS in cui lanciavamo solo un programma alla volta e anche l&#8217;antivirus in tempo reale sarebbe solo un sogno irrealizzato! Quella che vedremo qui \u00e8 un&#8217;estensione di questa possibilit\u00e0, pure molto comune. Infatti i programmi a loro volta possono essere divisi in pi\u00f9 &#8220;task&#8221; da eseguire parallelamente, dividendo fra essi il lavoro da svolgere. Questo porta benefici indiscutibili anche su CPU a singolo core, dato che anche senza una vera esecuzione parallela, un task che viene eseguito durante i tempi morti di un altro incrementa comunque le prestazioni.<br \/>\nQuesti &#8220;task&#8221; hanno nomi e peculiarit\u00e0 diverse e possono essere offerti dal sistema operativo, dal linguaggio di programmazione, da una libreria software o anche da tutti questi, con diverse corrispondenze fra l&#8217;uno e l&#8217;altro. Vediamo i principali:<\/p>\n<ul>\n<li>Processo: \u00e8 l&#8217;entit\u00e0 principale considerata dal sistema operativo, composta da un certo numero di istruzioni da eseguire e dalle risorse assegnate per la loro esecuzione. A ogni programma in esecuzione \u00e8 associato almeno un processo e viceversa. Ogni processo ha risorse separate dagli altri (salvo alcune eccezioni) ed in particolare ha un suo spazio di memoria assegnato. A ogni processo viene assegnato a turno uno dei processori (o dei core) disponibili per un certo quanto di tempo, cos\u00ec che ci sia un&#8217;esecuzione parallela, vera o simulata.<\/li>\n<li>Thread: \u00e8 un&#8217;entit\u00e0 che esiste all&#8217;interno di un processo. Ciascun thread all&#8217;interno di un processo condivide le risorse con gli altri ma esegue proprie istruzioni in modo parallelo. Non tutti i sistemi operativi offrono nativamente i thread, ma nei sistemi in cui non sono presenti possono comunque essere aggiunti dal linguaggio di programmazione o da una libreria software. Si parla di <em>kernel thread<\/em> per indicare un thread gestito direttamente dal sistema e di <em>user thread<\/em> per indicarne uno gestito dal software al di sopra di esso. Ciascuno ha i suoi vantaggi: un user thread di solito viene creato pi\u00f9 velocemente perch\u00e9 non ha bisogno di effettuare chiamate al sistema operativo, mentre un kernel thread ha il vantaggio di poter essere eseguito da un processore (o core) separato come avviene per i processi e quindi di essere potenzialmente pi\u00f9 veloce sulle nuove CPU nonch\u00e9 di essere gestito in modo pi\u00f9 efficiente perch\u00e9 il sistema operativo pu\u00f2 conoscere le risorse richieste di ogni thread e non solo quelle globali del processo. Questo vale anche in alcuni casi di deadlock, poich\u00e9 un thread bloccato non necessariamente blocca tutto il processo.<\/li>\n<\/ul>\n<p><strong>Programmazione e librerie software<\/strong><br \/>\nQuanto detto fin qui dovrebbe essere chiaro, ad eccezione di una cosa: se volessimo scrivere un programma diviso in pi\u00f9 task, dovremmo farlo sfruttando i processi o i thread? Ebbene, non c&#8217;\u00e8 una risposta unica: dipende dai requisiti del nostro programma, ma anche da cosa offre il sistema o i sistemi in cui intendiamo farlo girare ed anche cosa offre il linguaggio nel quale vorremmo scriverlo. Alcuni linguaggi, come Java, offrono la possibilit\u00e0 di dividere il programma in pi\u00f9 thread ma non in pi\u00f9 processi e non ci \u00e8 dato sapere a priori se la macchina virtuale li mapper\u00e0 in kernel thread, se disponibili. Certi sistemi operativi, come Windows, ci offrono kernel thread e user thread (chiamati <em>fiber<\/em>). Per quanto riguarda il mondo Unix, dipende dallo specifico sistema: Linux offre i kernel thread, Solaris no. Inoltre, linguaggi come il C non hanno sempre lo stesso set di chiamate, che dipendono esclusivamente dalle librerie, quindi un programma C scritto usando le chiamate specifiche di Linux o Windows per i thread sar\u00e0 impossibile da compilare su un altro sistema senza modifiche. Sono tutti problemi da valutare prima di iniziare a scrivere il nostro software ed \u00e8 il caso di documentarci bene per non dover modificare tutto a cose fatte. Se il nostro linguaggio (ad es. Java) ci impone dei paletti non abbiamo scelta, ma se per qualche motivo dobbiamo usarne uno come il C pu\u00f2 venirci in aiuto qualche libreria software esistente su diversi sistemi operativi. Un esempio \u00e8 POSIX Thread, che specifica un insieme di funzioni per la gestione dei thread uguali per ogni sistema operativo nel quale \u00e8 implementata. Questa libreria esiste nella maggior parte dei sistemi conformi alle specifiche POSIX ed \u00e8 disponibile anche per Windows. L&#8217;unico neo \u00e8 che non sappiamo di preciso se una specifica implementazione usa kernel o user thread, sebbene abbia notato che quando sono disponibili si preferiscono gli ultimi. Un&#8217;altra libreria \u00e8 GNU Portable Thread, estremamente portabile su sistemi POSIX ed utilissima se vogliamo scrivere programmi per sistemi Unix-like senza curarci del fatto che avremo o meno la libreria POSIX Thread (con la quale \u00e8 fornita compatibilit\u00e0) al momento della compilazione. I thread tuttavia sono implementati esclusivamente in modo utente. Un&#8217;altra piccola nota per i linguaggi C e C++: lo standard del 2011 (C11 e C++11) definisce un set di chiamate standard per i thread. Tuttavia ancora questo standard non \u00e8 ancora molto diffuso e la maggior parte dei compilatori non lo implementa del tutto, quindi per il momento bisogna comunque ricorrere a delle librerie aggiuntive. Ce ne sono tante, tutte con vantaggi e svantaggi.<\/p>\n<p><strong>I problemi<\/strong><br \/>\nQuesto modo di concepire i programmi non \u00e8 privo di insidie. Dobbiamo mettere in chiaro alcune cose:<\/p>\n<ol>\n<li>Due o pi\u00f9 task potrebbero condividere delle risorse, come delle porzioni di memoria.<\/li>\n<li>Come sappiamo, un programma \u00e8 composto da istruzioni. Le istruzioni che noi scriviamo nel nostro linguaggio possono essere a loro volta composte da pi\u00f9 istruzioni macchina.<\/li>\n<li>Il passaggio da un task all&#8217;altro pu\u00f2 avvenire in qualsiasi momento, anche fra un&#8217;istruzione macchina e l&#8217;altra, e quindi nel bel mezzo dell&#8217;esecuzione di una delle nostre istruzioni.<\/li>\n<\/ol>\n<p>Cosa accadrebbe se un task venisse interrotto mentre scrive una variabile condivisa e un altro cercasse di leggerla? La variabile avrebbe un valore inconsistente!<br \/>\nAbbiamo gi\u00e0 accennato al fatto che insieme alla possibilit\u00e0 di avere pi\u00f9 task abbiamo anche quella di farli comunicare. Questo pu\u00f2 avvenire per esempio creando una porzione di memoria comune, che \u00e8 di fatto una risorsa condivisa. Fortunatamente insieme a questa possibilit\u00e0 abbiamo anche diversi meccanismi di protezione che consentono l&#8217;accesso in <em>mutua esclusione<\/em> a una risorsa, cio\u00e8 di consentire l&#8217;accesso a un task per volta, e in generale di sincronizzare l&#8217;accesso stesso a tali risorse. Vedremo diversi di questi meccanismi negli esempi. I concetti generali che vi suggerisco di approfondire sono i seguenti:<\/p>\n<ol>\n<li>Il problema per il quale il risultato dell&#8217;elaborazione da parte di pi\u00f9 task dipende dall&#8217;ordine di esecuzione dei task stessi si chiama <em>race condition<\/em><\/li>\n<li>Per prevenire le race condition garantiamo la mutua esclusione creando delle <em>sezioni critiche<\/em> per proteggere il codice che accede alle risorse condivise fra pi\u00f9 task. Creare una sezione critica significa vincolare una porzione di codice ad essere eseguita da un task per volta<\/li>\n<li>Le sezioni critiche devono essere il meno possibile ed anche pi\u00f9 brevi possibile perch\u00e9 la loro presenza limita l&#8217;esecuzione parallela dei task degradando le prestazioni. Viene da s\u00e9 che dobbiamo condividere solo il minimo di risorse necessario.<\/li>\n<li>Dobbiamo badare attentamente che il modo in cui definiamo le sezioni critiche non possa creare <em>deadlock<\/em>. Per spiegare cos&#8217;\u00e8 un deadlock, immaginiamo due task, T1 e T2. Entrambi hanno bisogno delle risorse A e B. In un primo momento T1 impegna A e T2 impegna B. In un secondo momento T1 richiede B e T2 A senza che queste risorse siano state prime rilasciate. La nostra elaborazione \u00e8 bloccata per sempre! Questo \u00e8 un deadlock.<\/li>\n<\/ol>\n<p><strong>Gli esempi<\/strong><br \/>\nVi mostrer\u00f2 ora alcuni esempi di utilizzo di processi e thread su sistemi e linguaggi diversi, in modo che possiate rendervi conto delle differenze. Aggiunger\u00f2 qualche commento per illustrare vantaggi e svantaggi, nonch\u00e9 una brevissima descrizione delle chiamate utilizzate, senza la pretesa di fornire una spiegazione dettagliata, per la quale vi rimando alla loro documentazione ufficiale.<br \/>\nI programmi degli esempi faranno tutti la stessa identica cosa, ma la faranno di volta in volta utilizzando caratteristiche diverse del sistema operativo o del linguaggio di programmazione utilizzato. Si tratta del classico problema del produttore e del consumatore: un produttore genera dei dati e li invia al consumatore che li elabora in qualche modo. Sebbene l&#8217;esecuzione parallela possa servire in moltissimi altri casi, questo esempio pu\u00f2 diventare &#8220;reale&#8221; ed \u00e8 perfetto anche per mostrare la comunicazione e la sincronizzazione fra processi o thread. Eseguendo questi programmi vedremo in output i messaggi del produttore e del consumatore senza un ordine preciso, perch\u00e9 non ci \u00e8 dato sapere a priori come il sistema operativo o la libreria decidono quale fra i processi o i thread pronti deve essere eseguito in un determinato momento. L&#8217;unico vincolo sar\u00e0 la sincronizzazione che daremo per evitare che il produttore scriva pi\u00f9 dati di quelli che il buffer pu\u00f2 contenere e che il consumatore cerchi di leggere il buffer quando \u00e8 vuoto.<br \/>\nChi avesse bisogno di ulteriori spiegazioni o di un esempio con un altro linguaggio pu\u00f2 sentirsi libero di richiedermeli! Ne arriveranno altri man mano che mi verr\u00e0 in mente di provare.<br \/>\nDovrete avere gi\u00e0 dimestichezza col linguaggio che andare a considerare perch\u00e9 essendo questo un tutorial di programmazione avanzata non mi perder\u00f2 in spiegazioni sulle basi.<br \/>\n<a href=\"http:\/\/www.ilbytecidio.it\/altri_file\/esempi-multi.zip\">Scarica gli esempi<\/a><\/p>\n<p><strong>Esempio 1: Multitasking in C su sistemi POSIX con pipe<\/strong> (cartella <em>c-fork-pipe<\/em>)<br \/>\nQuesto esempio \u00e8 il pi\u00f9 semplice. Facciamo uso della chiamata fork dei sistemi Unix. Questa chiamata crea un processo identico a quello che la effetua. Gerarchicamente, il secondo processo sar\u00e0 considerato &#8220;figlio&#8221; del primo. I due processi, come da definizione, sono completamente distinti ed anche se sono identici hanno fra l&#8217;altro la loro copia delle variabili. Per la comunicazione fra i due usiamo una pipe. La pipe si comporta come un tubo: da un&#8217;apertura inseriamo un numero arbitrario di byte e da un&#8217;altra li estraiamo. La pipe va creata prima di sdoppiare il processo, in modo che sia condivisa fra padre e figli. La pipe garantisce automaticamente la mutua esclusione, inoltre il processo che scrive viene bloccato se la pipe \u00e8 piena e quello che legge se la pipe \u00e8 vuota, cos\u00ec non dobbiamo occuparcene esplicitamente.<br \/>\nQuesto metodo garantisce la massima portabilit\u00e0 fra sistemi Unix-like, perch\u00e9 tutti hanno la chiamata fork e le pipe. I processi ovviamente sfruttano le CPU multicore. Il maggiore svantaggio \u00e8 il consumo di risorse: un processo occupa pi\u00f9 memoria del sistema operativo rispetto a un thread, inoltre se anche il processo figlio deve svolgere poco lavoro \u00e8 una copia esatta del padre e la copia stessa occupa tempo, quindi la creazione sar\u00e0 pi\u00f9 lenta di quella di un thread.<\/p>\n<p><strong>Esempio 2: Multitasking in C su sistemi POSIX con memoria condivisa<\/strong> (cartella <em>c-fork-sysv<\/em>)<br \/>\nIn questo esempio usiamo ancora la chiamata fork per creare dei nuovi processi, ma per la comunicazione utilizziamo un&#8217;area di memoria condivisa creata usando delle chiamate di sistema introdotte per la prima volta su UNIX System V ed ora standard POSIX. Se vi state chiedendo perch\u00e9 non abbiamo dichiarato un normale array per implementare il buffer, significa che non avete ancora capito niente sui processi :-). Infatti la fork creerebbe una copia di tale array per ogni processo e comunque i processi figli non potrebbero puntare la copia del padre perch\u00e9 si troverebbe in uno spazio di memoria riservato a quest&#8217;ultimo! Un vantaggio della memoria condivisa \u00e8 che pu\u00f2 essere usata anche fra processi non &#8220;imparentati&#8221;.<br \/>\nQuesta volta dobbiamo gestire esplicitamente la sincronizzazione e la mutua esclusione per l&#8217;accesso al buffer. Lo facciamo usando il sistema pi\u00f9 classico: i semafori! Brevemente, il semaforo \u00e8 un tipo di dato astratto sul quale possiamo fare tre operazioni: inizializzazione, wait e signal (nell&#8217;esempio si chiama post). L&#8217;inizializzazione da al semaforo un valore intero. La wait controlla il valore del semaforo. Se \u00e8 maggiore di zero lo decrementa. Se \u00e8 zero il processo viene bloccato e messo in una coda. La signal controlla se ci sono processi in coda per aver chiamato una wait. Se ce ne sono sblocca il primo, altrimenti incrementa il valore del semaforo. La particolarit\u00e0 delle primitive per l&#8217;utilizzo dei semafori \u00e8 che sono atomiche. Ci\u00f2 significa che non possono essere interrotte: se il sistema operativo passa da un processo all&#8217;altro lo fa prima o dopo l&#8217;esecuzione di una di queste istruzioni, ma mai durante, come pu\u00f2 avvenire per tutte le altre.<br \/>\nI semafori creati sono tre: uno che indica se il buffer ha spazio libero, uno che indica se vi sono elementi dentro e uno \u00e8 un semaforo binario, detto anche mutex, che serve a garantire l&#8217;accesso in mutua esclusione al buffer. Questo sistema consente un accesso molto efficiente alla memoria condivisa. Da notare che se fossimo stati disattenti avremmo messo la wait sul mutex prima di quella sull&#8217;altro semaforo e questo presto o tardi avrebbe causato un deadlock, riflettete voi sul come!<\/p>\n<p><strong>Esempio 3: Multitasking in C su Windows con pipe<\/strong> (cartella <em>c-win-pipe<\/em>)<br \/>\nQuesta \u00e8 una specie di versione per Windows dell&#8217;esempio 1. In realt\u00e0 la similitudine \u00e8 un po&#8217; forzata perch\u00e9 la funzione CreateProcess esige un file eseguibile come parametro, quindi siamo costretti a compilare il programma principale, il produttore e il consumatore separatamente. Venendo a mancare un handle condiviso per la pipe come avviene su Unix dobbiamo collegare la pipe agli stream di I\/O dei processi che creiamo. Per mantenere l&#8217;esempio semplice e pi\u00f9 simile possibile a tutti gli altri ho dovuto fare ricorso a dei trucchi abbastanza brutti, ma l&#8217;importante \u00e8 vedere i meccanismi. Esiste un&#8217;altro tipo di pipe, la Named Pipe, a cui ci si riferisce chiamandola per nome come un file e che \u00e8 comoda per certi utilizzi o anche la memoria condivisa.<\/p>\n<p><strong>Esempio 4: Multithreading in C con PThread<\/strong> (cartella <em>c-pthread<\/em>)<br \/>\nIn questo esempio utilizziamo la libreria POSIX Thread (pthread) di cui abbiamo parlato nella parte teorica. Il funzionamento \u00e8 semplicissimo: si chiama la funzione pthread_create e le si passa una funzione da eseguire in un thread separato con gli eventuali parametri. In questo caso possiamo creare il buffer come semplice array perch\u00e9 i thread condividono tutti lo stesso spazio di indirizzamento, quindi niente &#8220;strane&#8221; chiamate per l&#8217;allocazione! Questa versione ha il vantaggio di poter essere eseguita anche su Windows senza modifiche: basta scaricare la libreria pthread-win32. Come gi\u00e0 detto, inoltre, la creazione di un thread \u00e8 un&#8217;operazione meno costosa della creazione di un processo e ci sono diversi altri motivi per cui potremmo preferirli. Anche qui usiamo i semafori per la sincronizzazione. Si veda l&#8217;esempio n.2 per la spiegazione sui semafori.<\/p>\n<p><strong>Esempio 5: Multithreading in Java<\/strong> (cartella <em>java<\/em>)<br \/>\nJava ci offre la possibilit\u00e0 di creare Thread separati.<br \/>\nIl codice che andr\u00e0 a eseguire il lavoro in un thread distinto dovr\u00e0 essere messo in una classe che implementi l&#8217;interfaccia Runnable. L&#8217;istanza di questa classe poi sar\u00e0 passata a un oggetto Thread. Per la comunicazione fra produttore e consumatore ho creato un&#8217;ulteriore classe synchronizedBuffer. Questa, attraverso l&#8217;uso della parola chiave synchronized nella dichiarazione dei metodi, si assicura automaticamente che i due thread non cerchino di operare contemporaneamente sul buffer. Inoltre genera un&#8217;eccezione se si cerca di leggere da buffer vuoto o scrivere su buffer pieno. In questo caso chi raccoglie l&#8217;eccezione aspetta un breve intervallo prima di riprovare. Forse ci sono metodi pi\u00f9 efficienti di gestire queste incombenze, ma cos\u00ec l&#8217;ho mantenuto pi\u00f9 semplice. Ho gi\u00e0 accennato al fatto che non possiamo sapere a priori se questi thread saranno gestiti dal sistema operativo o se si tratta di user thread. Tuttavia, a mio parere, questo pu\u00f2 essere trascurato: se avete scelto Java pi\u00f9 delle prestazioni vi interesser\u00e0 il fatto di poter eseguire il programma su qualsiasi sistema dotato di una Java Virtual Machine.<\/p>\n<p><strong>Esempio 6: Multithreading in C# su piattaforma .NET<\/strong> (cartella <em>c#-thread<\/em>)<br \/>\nLa piattaforma .NET della Microsoft ci offre una grande variet\u00e0 di strumenti per rendere pi\u00f9 agevole la nostra programmazione. Uno di questi \u00e8 la possibilit\u00e0 di creare pi\u00f9 Thread, che ove possibile (su Windows, ad esempio) saranno gestiti direttamente dal kernel e quindi si avvantaggeranno delle CPU multicore.<br \/>\nL&#8217;esempio somiglia a quello Java. Per eseguire un qualunque metodo su un thread separato basta passarlo a un&#8217;istanza della classe Thread e invocare su essa il metodo Start. Per la sincronizzazione .NET offre molti oggetti, ma noi andiamo sul classico usando i semafori! Si veda l&#8217;esempio n.2 per la spiegazione. La classe Semaphore (e la controparte pi\u00f9 leggera SemaphoreSlim) \u00e8 molto semplice da usare, anche se la documentazione non \u00e8, a mio parere, molto chiara in alcuni punti. Li creiamo dando il massimo numero di Thread che possono accedervi ed il valore iniziale. La differenza rispetto a come li abbiamo gi\u00e0 visti \u00e8 che al posto del metodo Post o Signal abbiamo Release, ma l&#8217;uso \u00e8 uguale. In quest&#8217;esempio al posto di usare esplicitamente un Mutex facciamo uso dell&#8217;istruzione lock che garantisce l&#8217;accesso in mutua esclusione al buffer. Come per l&#8217;esempio in Java, la gestione del buffer \u00e8 completamente incapsulata nella classe SynchronizedBuffer, con la differenza che il funzionamento \u00e8 pi\u00f9 trasparente in quanto non genera eccezioni, ma blocca il thread chiamante se si cerca di leggere il buffer vuoto o si cerca di scrivere scrive sul buffer pieno. Da un lato la gestione \u00e8 pi\u00f9 semplice, ma dall&#8217;altro c&#8217;\u00e8 maggior rischio di deadlock, se non si sta attenti!<br \/>\nQues&#8217;esempio vale per tutti i linguaggi che supportano .NET, dal momento che le classi sono le stesse e cambiano solo le parole chiave (per esempio per Visual Basic si veda SyncLock al posto di lock). Per quanto riguarda la portabilit\u00e0, ci sono molte implementazioni alternative del Framework .NET, di cui una della stessa Microsoft per Mac OS X e BSD. Per quanto riguarda Linux, ho eseguito con successo questo stesso esempio con Mono.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I processori in commercio per i Personal Computer raramente ormai hanno meno di due core, ed \u00e8 piuttosto comune che ne abbiano quattro o pi\u00f9. I nostri computer sono quindi equipaggiati con due o pi\u00f9 unit\u00e0 di elaborazione e sono &hellip; <a href=\"https:\/\/www.ilbytecidio.it\/?p=326\">Continua a leggere<span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[15],"tags":[],"class_list":["post-326","post","type-post","status-publish","format-standard","hentry","category-programmazione"],"views":178,"_links":{"self":[{"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/posts\/326","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=326"}],"version-history":[{"count":16,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/posts\/326\/revisions"}],"predecessor-version":[{"id":489,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=\/wp\/v2\/posts\/326\/revisions\/489"}],"wp:attachment":[{"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=326"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=326"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ilbytecidio.it\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=326"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}