Partiamo da un presupposto elementare: il codice viene eseguito dall’alto verso il basso, istruzione per istruzione.

Questa regola, che come tale può essere infranta, sta alla base di qualsiasi linguaggio di programmazione, tra cui anche il nostro linguaggio Swift, e le conseguenze di questo assioma sono tante e non sempre positive.

La prima è quella diretta e scontata che ci permette sempre di conoscere il risultato del codice ancor prima della sua esecuzione (Se il codice parte alla riga n.1 e finisce alla n.3 sai che il risultato sarà la somma di tutte le istruzioni precedenti).

esecuzione lineare del codice swift

Peppe, serviva un tutorial per spiegarmi questo?

Assolutamente no. Però, vorrei portarti ad entrare nel merito di questa ovvietà. Perché da sviluppatori moderni e che utilizzano linguaggi di alto livello come il linguaggio Swift, tendiamo a dimenticare o sconoscere il meccanismo che sta alla base di determinati comportamenti del codice.

Sapevi che ogni istruzione impiega del tempo per essere eseguita? 

Nel caso dell’esempio da cui siamo partiti, cioè i tre print, potevamo essere indotti a pensare che l’esecuzione fosse stata istantanea.

L’istantaneità, per il semplice fatto che il codice viene eseguito da una macchina (che è quindi soggetta alle regole della fisica) è un concetto illusorio. Potremmo dire che è talmente veloce da non farcene accorgere, ma tra una riga e l’altra passa inesorabilmente del tempo.

Il tempo reale d’esecuzione non è esattamente quello per via dell’esecuzione su Playground e di diversi fattori. Però rende bene l’idea.

Ora, riesci ad immaginare cosa succederebbe se una riga di codice, invece di starci meno di un battito di ciglia, impiegasse 4 secondi per terminare la sua esecuzione? 

Per la regola che ho scritto all’inizio, se una riga non termina la sua esecuzione, automaticamente non potranno essere eseguite tutte le altre.

Tradotto: l’app si bloccherà per 4 secondi. Questa è la norma nei download dei contenuti da internet come le immagini, i video o, in generale, tutte quelle operazioni che hanno bisogno di un lasso di tempo per essere eseguite.

Peppe, vuoi dire che la mia app è destinata a bloccarsi?

Assolutamente no.

La risposta a questo problema è quella che mi permetterà di introdurti il Grand Central Dispatch con il linguaggio Swift il quale ti permetterà di eludere questo blocco e di ottimizzare i tempi d’esecuzione del codice della tua applicazione iOS.

Il tutorial è abbastanza lungo. Quindi tu, a differenza delle tue app, hai il diritto di bloccarti su alcune parti (sopratutto l’inizio che è la chiave per capire la parte finale).

Premesse fatta. Sei pronto ad entrare nei meandri oscuri del Grand Central Dispatch con il linguaggio Swift?
Allora cominciamo!

CPU, Processi e Thread

Se oggi i computer sono così semplici da utilizzare lo dobbiamo principalmente ad una persona: John Von Neumann. Neumann, matematico ungherese (poi trasferitosi negli USA), negli anni ’40, ovvero in piena II Guerra Mondiale, teorizzò quella che da lì a breve si apprestò a diventare l’architettura alla base dei computer programmabili.

L’architettura di Von Neumann stabilisce che un computer debba essere composto da quattro componenti:

  1. Dispositivo di IO (Input/Output). Nei nostri bei iPhone è il Display
  2. Memoria centrale (per semplicità, chiamiamola solo RAM). Qui vive l’app quando viene eseguita e qui vivono i dati che non sono persistenti
  3. Memoria di massa (per semplicità, l’hard disk). Il luogo dove risiedono i dati persistenti (quelli che non si perdono al riavvio dell’app o del telefono)
  4. CPU o unità di elaborazione. Il cuore pulsante di ogni computer, qui vengono eseguite tutte le istruzioni di codice
architettura di von_neumann
fonte wikipedia

Per farti entrare nel meccanismo, ti faccio un esempio con il cibo (e ci sta pure).

Fai finta di trovarti in cucina e di voler preparare una succulenta pietanza dal tuo ricettario preferito. Ipotizziamo che tu voglia preparare una carbonara.

Prendi il libro delle ricette (input) che si trova sullo scaffale (memoria di massa), lo sfogli e trovi la tua ricetta (la tua app). La ricetta ti dice solamente quali sono gli ingredienti e come utilizzarli (la ricetta è un modello dell’oggetto reale). Allora tu cominci a prenderli ed a seguire i passi per metterli insieme (stai elaborando gli ingredienti per creare la pietanza – CPU). Una volta creata la pietanza, questa la metti nel piatto che andrà a finire nel tavolo (memoria centrale).

Processo

L’applicazione che tu realizzerai e caricherai sul tuo telefonino è come una ricetta del tuo libro di cucina. Contiene tutto ciò che serve al sistema per poterla creare ed utilizzare.

L’applicazione utilizzata, cioè quella con cui interagiamo ed interagiscono gli utenti, prende il nome di processo. Un processo è l’esecuzione dell’applicazione.

Come per la cucina, la ricetta è una sola (il progetto dell’app è uno) mentre i piatti creati (cioè i processi) potrebbero essere uno o più.

Su iOS è permessa la creazione di un solo processo per ogni singola applicazione. Mentre, così si capisce meglio di che sto parlando, su macOS o su Windows, si possono creare più processi della stessa applicazione.

La stessa applicazione (Safari) con più processi (uno per ogni pagina web). Processo genitore (Safari) processi figli le pagine aperte

Il processo vive nella memoria centrale (la ram). Trovandosi in memoria centrale, tutto ciò che accade qui è soggetto alla volubilità della memoria stessa.

Ovvero una volta ucciso il processo (lo swipe verso l’alto dal task manager) tutti i contenuti delle variabili vengono distrutti. Questo concetto di variabili e memoria l’avevo introdotto velocemente nella lezione sulle variabili e costanti del linguaggio Swift.

Perché si trova qui e non in memoria di massa?

Le RAM sono infinitamente più veloci (in lettura e scrittura) di una memoria di archiviazione e, per quanto detto prima, l’app è un modello e modificarlo significherebbe ricompilare l’applicazione (più tante altre cose che non stiamo qui a spiegare).

Processo e CPU

Ipotizziamo il caso in cui, mentre stai cucinando la tua ricetta (ti ricordo che tu sei la CPU) arriva una chiamata urgente a cui devi obbligatoriamente rispondere.

Allora stacchi i fornelli, metti la pietanza che stai preparando (il processo) in attesa e passi ad un nuovo processo (rispondere) che ha un’importanza più alta. Una volta terminata la chiamata, riprendi la ricetta e ricominci dal punto in cui l’avevi lasciata.

Le CPU, di tipo single core, funzionano esattamente allo stesso modo. Elaborano solamente un processo per volta, passando a quelli a priorità più alta quando necessario.

Questo significa che, se stai utilizzando un’applicazione e devi switchare ad un’altra, l’applicazione messa da parte viene bloccata in quanto la CPU è occupata nello svolgere le operazioni del nuovo processo (cosa che accade, più o meno, su iOS passando da un’app all’altra).

Nella realtà, una CPU moderna come quella dei nostri iPhone, riesce ad elaborare più processi in pseudo-parallelismo passando da un processo all’altro talmente velocemente da non farci notare le esecuzioni degli altri processi.

Esempio: i client delle email hanno dei processi che vivono in background che controllano la ricezione di nuove email, questi vengono eseguiti anche quando stiamo utilizzando un’altra applicazione. La CPU passa dal processo in corso a quello dell’email con una velocità tale da non far notare il blocco sull’esecuzione del processo in corso.

Thread

Se l’applicazione in run l’abbiamo chiamata processo, che si intende tutto la sequenza di codice in esecuzione da parte della CPU, un thread è un processo dentro il processo. 

Un processo ha sempre un singolo thread e, quando si trova in questa forma, thread e processo sono sinonimi. Thread non è da confondere con i processi figli (quelli dell’immagine più sopra) perché questi ultimi hanno una vita separata rispetto al processo principale (per gli addetti ai lavori, hanno un Address Space diverso).

Un thread o più thread, vivono letteralmente all’interno del processo che li ha generati (quindi hanno accesso allo stesso Address Space). 

Perché dovremmo avere processi dentro processi?

Le CPU moderne, come quelle ormai montante sugli smartphone, presentano sempre più di un Core (processori multi core). 

Con una CPU single core, i processi vengono eseguiti uno per volta. Con una CPU multi core (ipotizziamo a due) si da il via al vero e proprio parallelismo permettendo l’esecuzione di più processi contemporaneamente (ogni core esegue il suo processo).

Ok, quindi se in un core c’è la mia applicazione in esecuzione, nell’altro cosa ci sta?

Esattamente. Il secondo core della CPU nella maggior parte delle applicazioni amatoriali viene sfruttato in minima parte.

I thread essendo dei particolari processi, come tali possono essere presi in consegna da un core della CPU indipendentemente dall’esecuzione del processo che li contiene. Quindi se un core sta eseguendo un thread per lo svolgimento di alcune operazioni, il mio processo può far partire in parallelo un altro thread che verrà eseguito nel core libero e che mi permetterà di eseguirà altri blocchi di istruzione.

Solo per correttezza di informazioni, i thread possono essere eseguiti sullo stesso Core “contemporaneamente” tramite l’utilizzo di architetture particolari e di algoritmi di scheduling che ruotano i thread (in base alle loro priorità e politiche di scheduling) talmente velocemente da non far notare il blocco dei thread non più in esecuzione da parte della CPU.

Esempio

La mia applicazione ha un database sul Core Data di 40.000 elementi. Ipotizziamo una lista di comuni. L’app, tra le tante funzionalità, permette di cercare un comune all’interno di questo array.

Al di là delle ottimizzazioni di codice che si potrebbero fare per semplificare il procedimento, vogliamoci male e immaginiamo la ricerca come la meno performate possibile, ovvero quella sequenziale (si esegue un ciclo su tutti gli elementi in maniera lineare dal primo all’ultimo. Algoritmo di ricerca sequenziale o lineare). In soldoni un ciclo che si comporta così:

var comuneDaCercare = "Pi" 
var comuniTrovati: [String] = []

for indice in 0...40000 {
    let comuneIterato = self.listaComuni[i]
    if comuneIterato.hasPrefix(comuneDaCercare) {
          comuniTrovati.append(comuneIterato)
    }
}

Per tutti i comuni che cominciano per “Pi”, il codice prende l’elemento che rispecchia questo pattern e lo piazza in un array di comuniTrovati. Il ciclo passerà in rassegna tutti gli elementi fino all’ultimo in posizione 40.000.

Con l’utilizzo del singolo thread di default (ti ricordo che processo = thread quando non si hanno altri thread all’interno dell’app), l’applicazione si bloccherà fin quando il ciclo non avrà raggiunto l’ultimo elemento.

Nel caso reale, dunque, l’utente scriverà “Pi” e poi non potrà più selezionare gli altri elementi dell’interfaccia, per esempio tornare indietro e via dicendo, perché la CPU è ancora ferma ad eseguire quel ciclo.

E con i Thread?

Grazie ad un thread potresti trasferire l’esecuzione di quel ciclo su un core libero della CPU permettendo al restante codice ed eventi di essere eseguiti ed intercettati:

// esegui il codice su un nuovo thread - PSEUDO CODICE
eseguiInUnThread {
   for indice in 0...40000 {
       let comuneIterato = self.listaComuni[i]
       if comuneIterato.hasPrefix(comuneDaCercare) {
           comuniTrovati.append(comuneIterato)
       }
   }
}

Se hai capito la teoria, la pratica ti risulterà estremamente semplice. Forse!

Grand Central Dispatch

Apple è stata sempre furba, anche negli aspetti della programmazione. Chi proviene dagli altri linguaggi lo sa benissimo: con i thread non si scherza. Perché, se non si sa realmente quello che si sta facendo, sono più i problemi che i benefici.

La grande mela, per questo motivo, ha offuscato tutta quella macchinosità che consiste nella creazione di un thread e relativa gestione (sincronia, accessi a risorse critiche ecc) creando un oggetto di astrazione superiore che ne facilita l’utilizzo

Il Grand Central Dispatch mette a disposizione un insieme di classi la quale caratteristica principale è quella di permettere allo sviluppatore di delimitare porzioni di codice, chiamati blocchi o task, che potranno essere eseguiti in parallelo su un nuovo thread.

Quindi il GCD (Grand Central Dispatch) allontana lo sviluppatore dalla gestione vera e propria dei thread ma ne permette il loro utilizzo.

Dispatch Queues

Sono diversi gli elementi presenti all’interno del Grand Central Dispatch e che ti permettono di utilizzare i thread, una delle classi presenti è la DispatchQueue.

Fonte: Wikipedia

Un blocco di codice, Task, viene preso in consegna dal Grand Central Dispatch che lo inserirà in una coda di task. Il primo Task inserito è quello che verrà eseguito per primo su un thread libero.

Il GCD troverà un thread libero (lo sviluppatore non sa qual è o quanti sono) e gli darà in consegna il task. Il thread eseguirà il task in parallelo all’esecuzione del thread principale dell’applicazione.

Qualora fosse presente un altro task o altri, ed in base a delle specifiche che sceglierà lo sviluppatore, il successivo potrà essere inserito in un nuovo thread libero parallelizzando e velocizzando le varie operazioni della tua applicazione. Altrimenti, la Dispatch Queue eseguirà un singolo task per volta (sempre su un thread differente rispetto al principale). 

Progetto d’esempio

Basta teoria! Cominciamo a mettere le mani in pasta. Crea un nuovo progetto Single View Application iOS. Seleziona il linguaggio Swift e, una volta completati i passaggi, spostati al file ViewController.swift.

Qui crea un semplice metodo someFunction() che richiamerai nel viewDidAppear:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        self.someFunction()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    // Una funzione che utilizzerà il GCD
    func someFunction() {
    
    }

}

All’interno della funzione someFunction scriveremo il codice per l’utilizzo del GCD e delle Dispatch Queues.

DispatchQueue con linguaggio Swift

let queue = DispatchQueue.init(label: "it.xcoding.nomeCoda")

Una DispatchQueue ha bisogno del solo parametro label per la sua costruzione. La label è un nome che identifica la coda. Questo nome deve essere univoco dato che, all’interno dell’applicazione, potresti aver bisogno di più DispatchQueue. Apple suggerisce di utilizzare la notazione dei Bundle (o reverse DNS notation) anche se puoi chiamarle come preferisci.

La DispatchQueue ti mette a disposizione due metodi:

  1. sync { codice } esegue il codice all’interno delle graffe bloccando il thread in cui viene eseguito il comando sync.
  2. async { codice } il codice viene avviato e poi eseguito in parallelo al restante codice fuori del metodo.

Più facile vedendo un esempio. Ipotizziamo d’avere questi due cicli:

for i in 0...4 {
    print("", i)
}

for y in 0...4 {
    print("", y)
}

Quello che ci aspettiamo è che prima venga eseguito il primo ciclo, stampando 5 volte il robot, e che successivamente venga eseguito il secondo ciclo.

Async

Proviamo ad utilizzare la DispatchQueue inserendo il primo ciclo all’interno di un async invocato su una queue creata appositamente.

let queue = DispatchQueue.init(label: "it.xcoding.queue")

queue.async {
    for i in 0...4 {
        print("", i)
    }
}

for y in 0...4 {
    print("", y)
}

Cos’è successo qui?

Quando viene eseguito il comando queue.async, il Grand Central Dispatch prende il task presente all’interno delle graffe (cioè tutto il codice) e lo invia ad un thread differente rispetto a quello principale ed in cui si trova il resto del codice.

A questo nuovo thread, inoltre, gli viene detto di eseguire il task in asincronia, cioè in parallelo, rispetto all’esecuzione del thread principale. Per questo motivo una volta eseguito il queue.async l’esecuzione non si blocca ma passa alla riga successiva che è rappresenta dal secondo ciclo.

Sulla console dovresti vedere un print delle faccine alternate. Non devi prenderlo per oro colato, in realtà non vengono eseguiti prima uno e poi l’altro (cioè prima un giro di ciclo del primo task e poi del secondo), nella realtà a livello di CPU i due cicli vengono eseguiti contemporaneamente. In console, dato che si scrive riga per riga, i due thread si contendono l’accesso alla console e per questo si alternano le emoji.

Quality of Services

Se durante la preparazione della tua pietanza ti chiama tuo figlio perché si è fatto male, a meno che non vuoi infierire, passi al task con priorità più alta (dare assistenza).

Anche le DispatchQueue ti permettono di definire la priorità d’esecuzione dei suoi task. 

Peppe, cosa significa?

Partiamo dal presupposto che il main thread ha la priorità più alta. Il main thread ti ricordo che è quello gestisce l’interfaccia e quello dove vive il codice a meno di queue create appositamente.

Questo comporta che una queue, come quella creata poc’anzi, non potrà mai competere con il main thread. Cioè il sistema tenderà sempre a dare maggiore importanza, cioè priorità d’esecuzione, al codice del main thread. Quindi se ti sta venendo la pazza idea di buttare tutto il codice dentro una queue, beh, non farlo.

Allora a cosa serve la priorità?

Dentro un’applicazione, generalmente le più complesse, possono esistere più queue. Ovvero puoi creare più oggetti DispatchQueue con label differenti.

Ora ipotizziamo d’avere un’applicazione tipo DropBox dove puoi conservare grossi file, modificarli se sono testuali e così via.

Il tuo utente deve scaricare un grosso file da 1 GB e contemporaneamente caricare delle immagini sul suo archivio. Ha bisogno però che le immagini vengano caricate il prima possibile ma non può neanche bloccare l’esecuzione del main thread perché vuole navigare dentro l’app.

Dato che conosci le queue e sai che puoi creare più di una queue, crei:

  • queueA: per il download del file
  • queueB: per il caricamento delle immagini
let queueA = DispatchQueue.init(label: "it.xcoding.queueA")
let queueB = DispatchQueue.init(label: "it.xcoding.queueB")

queueA.async {
    // download file
}

queueA.async {
    // upload immagini
}

Per poter definire la priorità di una queue devi aggiungere un nuovo attributo chiamato qos, Quality of Services, all’init della DispatchQueue. Il parametro qos o DispatchQoS è un Enum, puoi consultarlo qui, che ti da la possibilità di scegliere tra questi valori che sono ordinati per livello di priorità (il primo ha priorità più alta):

  1. userInteractive: I task all’interno dovrebbero essere quelli che hanno bisogno di un’esecuzione “istantanea”. Puoi paragonare questo qos a quello che rappresenta il main thread. Ovviamente non ti consiglio di utilizzarlo per interagire con la grafica dell’app (anche se non da problemi. Sotto vedrai il modo corretto per farlo)
  2. userInitiated: Leggermente più lento rispetto all’userInteractive. Ci si aspetta che i task all’interno debbano restituire dei risultati quasi istantaneamente. Per esempio, l’apertura e chiusura di un file dopo l’invio del comando dovrebbe utilizzare questa qos
  3. default: Quando non scelto, il qos della DispatchQueue sceglie un valore compreso tra la QoS Utility e la QoS UserInteractive. Non è consigliato l’utilizzo in quanto è il sistema a sceglierne la priorità.
  4. utility: Qui vanno i task che richiedono del tempo per essere portati a termine (download ed import per esempio). Quindi tutte quelle operazioni che richiederebbero qualche minuto. A livello di User Experience è consigliato utilizzare delle barre d’avanzamento o analoghi sistemi per avvisare l’utente sui progressi del task
  5. backgroundLa sincronizzazione di dati, i backup ed in generale operazioni lunghe (dalla decine di minuti alle ore) ed invisibili all’utente dovrebbero utilizzare questo QoS
  6. unspecified: Rappresenta l’assenza di QoS

In generale default ed unspecified non andrebbero utilizzate in quanto ti tolgono quel potere di cui hai bisogno per gestire correttamente le operazioni della tua applicazione. Quindi spazia tra tutte le altre QoS in base al tipo di lavoro che deve andare a svolgere il tuo codice.

Proviamo ad assegnare una priorità alle due DispatchQueue. Per quanto detto, potresti dare una Quality of Service Utility alla queue del download del file ed una QoS Background per l’upload delle immagini (abbiamo ipotizzato un upload di 1 GB, fosse stato della manciata di mb avresti potuto utilizzare anche una Utility).

Per semplicità utilizziamo sempre i due cicli:

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .utility)
let queueB = DispatchQueue.init(label: "it.xcoding.queueB", qos: .background)

queueA.async { 
    for i in 0...10 {
        print("", i)
    }
}

queueB.async {
    for y in 0...4 {
        print("", y)
    }
}

Quality of Services Dispatch Quelle linguaggio swift

Come previsto ed ipotizzato la queueA, che ha priorità utility, viene data più priorità rispetto alla queueB che ha un QoS background. Infatti alla queueB viene dato uno spazio d’esecuzione solamente all’inizio (ciclo zero), poi viene rallentano per dare spazio alla queueA ed infine viene ripreso al termine.

Tutto ciò non sarebbe stato possibile senza l’utilizzo delle Quality of Services e del Grand Central Dispatch con il linguaggio Swift.

Serial e Concurrent

Ti ho mostrato come le DispatchQueue ed i metodi sync ed async ti permettono di parallelizzare le operazioni della tua applicazioni e come l’utilizzo della Quality of Services ti permetta di assegnare una priorità d’esecuzione alle tue queue. 

Ma cosa accade, invece, a più task inseriti all’interno di una queue?

Sembra ovvio, ma lo ribadisco, che la creazione di una queue è un’operazione onerosa per il sistema (overhead). In pratica, certe volte, è più il tempo perso per creare la queue ed i relativi thread che il tempo d’esecuzione del task stesso.

Per questo motivo è consigliato utilizzare un numero ridotto di DispatchQueue alle quali verranno dati in pasto i vari task da eseguire.

Esecuzione in serie

Ipotizziamo d’avere sempre quei due famosissimi cicli e di volerli eseguire in parallelo. Prima ti ho fatto utilizzare due queue, adesso devi ottimizzare il codice e pertanto decidi di crearne una sola.

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .userInitiated)

queueA.async {
    for i in 0...4 {
        print("", i)
    }
}

queueA.async {
    for y in 0...4 {
        print("", y)
    }
}

La DispatchQueue è una coda (un array per capirci) i quali task (il codice dentro gli async o sync) vengono messi uno dietro l’altro in attesa di essere eseguiti.

Di default, quindi, la DispatchQueue eseguirà i task uno per volta. Questa tipologia di architettura della queue viene detta seriale. Cioè i task vengono eseguiti in serie, uno dopo l’altro.

Ma non avevamo messo async?

Vero! Ma ti ricordo che l’async è il comando che va a definire il metodo d’esecuzione del task rispetto agli altri thread o DispatchQueue presenti nell’app e che quindi non interferisce con i task presenti all’interno della coda.

Esecuzione in parallelo

Per poter parallelizzare l’esecuzione dei task all’interno della stessa queue bisogna introdurre un nuovo parametro al momento della costruzione della coda. Questo parametro si chiama attributes.

L’attributes è un enum con due solo valori:

  1. concurrent: Permette l’esecuzione in parallelo dei task all’interno della queue. Non è possibile assegnargli una priorità, quindi verranno eseguiti tutti un po’ per volta.
  2. initiallyInactive: La queue nascerà come inattiva. Quindi non eseguirà i task quando invocherai il metodo async o sync. L’esecuzione comincerà solamente quando verrà invocato il metodo .activate() sulla queue.

concurrent

Cominciamo a vedere il primo, cioè il concurrent:

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .userInitiated, attributes: .concurrent)

queueA.async {
    for i in 0...4 {
        print("", i)
    }
}

queueA.async {
    for y in 0...4 {
        print("", y)
    }
}

Un ottimo sistema per poter parallelizzare i task all’interno della queue. Questi vengono prima inseriti all’interno della coda e poi uno per uno eseguiti in maniera alternata.

InitiallyInactive

Questa opzione è utilissima in tutti quei casi in cui si voglia invocare un blocco di codice asincrono o comunque gestito da una DispatchQueue quando si verifica un determinato evento (un trigger potrebbe essere l’arrivo di una notifica).

Prima di procedere, però, è necessario modificare leggermente il codice.

Per prima cosa la creazione della queue o la definizione della variabile/costante deve essere spostata all’esterno. Quindi la queue deve diventare una proprietà della classe.

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .userInitiated, attributes: .initiallyInactive)

func someFunction() {

    queueA.async {
        for i in 0...4 {
            print("", i)
        }
    }

    queueA.async {
        for y in 0...4 {
            print("", y)
        }
    }
    
    var something = true // il nostro trigger event
    
    if something {
        queueA.activate()
    }
    
}

In questo modo, solo quando la variabile something sarà uguale a true verrà attivata la queue ed i relativi task all’interno. Da notare che i task verranno eseguiti in seriale e non in parallelo.

Concurrent ed initiallyInactive

Posso far partire la queue in differita ed i task in concorrenza tra loro?

Ovviamente si.

Per farlo ti basta inserire .concurrent e .initiallyInactive all’interno di un array che poi passerai al parametro attributes.

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .userInitiated, attributes: [.initiallyInactive, .concurrent])

Esecuzione differita

L’ultimo tassello che completa il primo quadro sul Grand Central Dispatch con il linguaggio Swift è quello che riguarda l’avvio differito di un task. Al momento, infatti, una volta invocati i metodi async e sync, l’esecuzione del codice al loro interno parte “istantaneamente”.

Esiste un metodo chiamato asyncAfter che permette di far partire l’esecuzione del task N tempo dopo l’invocazione del metodo stesso.

Questo è un sistema interessante e veloce che ti permette di eseguire un codice una sola volta ed in differita rispetto ad un evento.

Il metodo classico prevederebbe l’utilizzo dei Timer che, per quanto semplici possano essere, portano con se alcune complicazioni che derivano dalla struttura che li rappresenta a basso livello. Per esempio, un problema dei Timer, è quello che devono essere invalidati quando non più utilizzati onde evitare un abuso di risorse (Qui un resoconto Apple sul perché o non dovresti utilizzare i Timer).  Ad ogni modo i Timer dovrebbero essere utilizzati quando c’è da eseguire un codice che si ripete ciclicamente nel tempo.

Il metodo asyncAfter vuole come parametro un oggetto di tipo DispatchTime che rappresenterà la data inteso come istante in cui dovrà partire il codice inserito. Noi lo costruiremo partendo dal DispatchTimeInterval che puoi benissimo paragonare ad un valore di tipo Double, che sommeremo all’istante di tempo corrente, il quale ci darà il DispatchTime finale. 

let time: DispatchTime = DispatchTime.now() + DispatchTimeInterval.seconds(5) // t + 5 secondi

I case che mette a disposizione il DispatchTimeInterval sono quattro: seconds, milliseconds, microseconds e nanoseconds e ti serviranno per poter definire l’istante di tempo, in una delle quattro grandezze, in cui dovrà avviarsi il task. 

A questo punto ti basterà creare una nuova DispatchQueue ed utilizzare il metodo asyncAfter a cui passerai il time d’esecuzione:

let queueA = DispatchQueue.init(label: "it.xcoding.queueA", qos: .userInitiated)

let time: DispatchTime = DispatchTime.now() + DispatchTimeInterval.seconds(5)

queueA.asyncAfter(deadline: time) { 
    print("dopo 5 secondi dall'esecuzione")
}

Main e Global Queue

Prima di passare ad un esempio reale è necessario che io ti metta a conoscenza dell’esistenza di due DispatchQueue di default. Sono rispettivamente la Main Queue e la Global Queue.

Come già ti avevo accennato l’operazione di creazione di una DispatchQueue ha un costo non indifferente paragonato alla mole di lavoro che andrà a compiere nella maggior parte delle situazioni.

Per questo motivo, il Grand Central Dispatch, mette sempre a disposizione due queue di default a cui poter assegnare i task dell’applicazione.

Le due queue in questione sono gestite da due proprietà della classe DispatchQueue (in realtà la global viene restituita da un metodo della classe). Accedendovi potrai direttamente invocare i metodi sync, async e asyncAfter.

  • DispatchQueue.main ti permette di operare sul main thread dell’applicazione che è anche quello, lo ripeto, che gestisce l’interfaccia grafica dell’applicazione. Task onerosi su questa queue comporteranno il rallentamento o il blocco dell’interfaccia. Può essere utile per ritornare sul main da un’altra queue (tipo la global o le tue personali) che non interagiscono con la main.
  • DispatchQueue.global(qos = .default) è una seconda queue con QoS Default e scollegata dal main thread. Puoi utilizzarla per piccole operazioni asincrone o sincrone che non hanno bisogno di interagire con il main e che non richiedono di una queue apposita con un Quality of Services diverso dal default. 
    • Se necessario, puoi passare un QoS come parametro del metodo per utilizzarla come una queue custom a tutti gli effetti.
DispatchQueue.main.async {
    // lavora sul main thread
}

DispatchQueue.global().async {
    // queue custom con qos default
}

DispatchQueue.global(qos: .utility).async {
    // global con qos
}

DispatchQueue.global().async {
    // eseguito sulla global
    DispatchQueue.main.async {
        // ritorno sul main dalla global
    }
}

Nella maggior parte dei casi finirai per utilizzare una delle due sopra citate. Ma noi siamo developer e ci piace complicarci la vita!

Esempio con URLSession

Per evitare di mettere nuova carne sul fuoco, voglio farti vedere una semplice applicazione di quanto visto sopra. Per semplicità d’apprendimento e per evitare di uscire dal topic di questo tutorial ti mostrerò solamente il codice (do per scontato che tu sappia già sviluppare a questo punto del tutorial, altrimenti esci da questo corpo finché sei in tempo).

Cioè che voglio farti vedere è un semplice download di un file json che contiene una lista degli ultimi film usciti al cinema.

La lista dei film la prenderemo dal servizio The Movie Database a cui se volete potete registrarvi per ottenere la apiKey da utilizzare per le chiamate. Altrimenti utilizza la mia:

func nowPlaying(page: Int = 1) {
    
    let apiKey = "e3ad70a80239a24126bbd592bc17e1fd"
    let url = URL.init(string: "https://api.themoviedb.org/3/movie/now_playing?api_key=\(apiKey)&language=it-IT&page=\(page)&region=it")!


}

Faremo un semplice get sulla prima pagina del metodo now_playing che ci restituirà gli ultimi film (pagina 1) al cinema nel nostro paese.

Con JsonFormatter analizza la struttura del JSON restituito dall’url della funzione e di conseguenza creiamo una struct che ci faciliterà l’operazione di mapping dei valori. Per velocizzare e rimanere concentrati sul GCD i dati che prenderemo saranno solamente:

struct Film {
    var title: String
    var author: String
    var release_date: String
    var overview: String
}

Dunque cosa faremo?

  • Scaricheremo il json in background
  • Mapperemo ogni elemento del json con un oggetto Film
  • Per ogni elemento aggiorneremo l’interfaccia dell’applicazione (ipotizziamo una tabella)

Il download lo eseguiremo con una classe che alle sue spalle fa uso del Grand Central Dispatch e che, al contempo, fornisce alcuni metodi utili nelle connessioni http. Tale classe si chiama URLSession (te ne parlerò nel dettaglio nelle altre puntate).

Il metodo dataTask di un oggetto URLSession, noi utilizzeremo lo shared (cioè il singleton), ha una completionHandler  (cioè una closureche viene attivata quando la funzione ha finito di scaricare il dato associato all’URL a cui si connetterà.

URLSession.shared.dataTask(with: url) { (data, response, error) in
    
}

Quando l’esecuzione arriva alla riga d’avvio del dataTask, questo fa partire una nuova queue nel quale viene inserito il task di download e ricezione del contenuto presente dentro l’url. Quando il task viene completato, in background ed in asincronia rispetto il resto dell’app, viene svegliato il codice presente dentro la completionHandler. 

Ora, se hai capito un po’, il codice all’interno della closure non si trova sul main thread, ma su un thread in background.

L’errore che commettono tutti, quando utilizzano una funzione della URLSession, è pensare che il codice all’interno della callback possa andare ad interagire direttamente con l’interfaccia dell’app.

Quindi all’interno della completion, se volessi aggiornare la View, cosa dovrei fare?

Semplice. Il Grand Central Dispatch ci mette a disposizione la queue main che lavora sul main thread, quindi dovrai utilizzare questa per poter ritornare sull’interfaccia.

URLSession.shared.dataTask(with: url) { (data, response, error) in
    // eseguito su un thread diverso dal main
    
    // Qui eseguo il mapping
    
    DispatchQueue.main.async {
        // ritorno sul main thread ed aggiorno la view
        
        // qui aggiorno la view
    }
}

A questo punto il resto è una semplice trasformazione del data restituito dal JSONSerialization. Passaggio che può benissimo avvenire all’interno della closure, quindi sul back thread, ed infine passare il nuovo dato all’interfaccia da un async invocato sulla main queue.

func nowPlaying(page: Int = 1) {
    
    let apiKey = "e3ad70a80239a24126bbd592bc17e1fd"
    let url = URL.init(string: "https://api.themoviedb.org/3/movie/now_playing?api_key=\(apiKey)&language=it-IT&page=\(page)&region=it")!

    URLSession.shared.dataTask(with: url) { (data, response, error) in
        // eseguito su un thread diverso dal main
        // Qui eseguo il mapping
        self.arrayFilm = self.mapDataToFilms(data)
        
        
        DispatchQueue.main.async {
            // ritorno sul main thread ed aggiorno la view
            self.tableView.reloadData()
        }
    }

}

Lo stesso principio può essere utilizzato per scaricare immagini ed aggiornare una UIImageView. 

Ti faccio notare che la mancanza di tale passaggio avrebbe implicato il non aggiornamento dell’interfaccia. Con questa semplice conoscenza del Grand Central Dispatch niente più sembrerà così strano.

Una piccola miglioria con un grande impatto sull’utilizzo dell’applicazione.

DispatchWorkItem

Spesso capiterà di dover eseguire diverse volte ed in diverse parti dell’applicazione, le stesse identiche operazioni. Questo comporta una ridondanza enorme nella scrittura del codice che, vuoi o non vuoi, diventa un copia incolla costante.

Se poi ci metti nel mezzo il Grand Central Dispatch il codice da ripetere può diventare enorme.

La classe DispatchWorkItem ti permette di incapsulare, in un oggetto, un codice che dovrà essere dato in pasto ad una DispatchQueue o, in alternativa, direttamente al main thread.

func someFunction() {
    
    var value: Int = 0
    
    let dispatchItem = DispatchWorkItem.init {
        value += 5
    }
    
    dispatchItem.perform() // avvia il DispatchWorkItem

    let queue = DispatchQueue.global(qos: .utility)
        
    queue.async(execute: dispatchItem) // esegue il DispatchWorkItem sulla queue in async
        
    queue.async {
        dispatchItem.perform() // analogo al codice precedente, ma con la sintassi classica
    }

}

Così come te lo sto presentando potresti essere indotto a pensare che il vantaggio sta solamente nella semplicità di scrittura. In effetti siamo riusciti a sommare +5 alla variabile value per tre volte, due delle quali da una queue parallela alla main.

Il vero vantaggio viene dalla presenza di alcuni metodi accessori presenti all’interno della classe DispatchWorkItem. Tra questi troviamo:

  • notify: che permette di eseguire un codice su una queue qualsiasi al termine dell’esecuzione del DispatchWorkItem
  • cancelti da la possibilità di cancellare l’esecuzione del DipatchWorkItem. Una volta cancellato non potrà più riprendere quindi dovrai ricrearne uno nuovo

Proviamo a fare un esempio leggermente più complesso per capire meglio l’utilità del DispatchWorkItem con il linguaggio Swift.

Ipotizziamo di voler scaricare 6 immagini da internet in maniera concorrente. Cioè con ogni task eseguito in parallelo. Quindi all’interno di un DispatchWorkItem aggiungerai il codice per il download dell’immagine e con un ciclo reitererai l’operazione in modo da creare 6 DispatchWorkItem. Questi DispatchWorkItem li conserveremo in un array per poi poterli eseguire successivamente.

var workItems = [DispatchWorkItem]()

for i in 1...6 {
    var currentImage = ""
    
    let dispatchItem = DispatchWorkItem.init {
        let randomSleep = arc4random_uniform(2) // 1
        sleep(randomSleep)
        currentImage = "image-\(i).jpg" // ipotizziamo che qui sia avvenuto il download
    }
    
    dispatchItem.notify(queue: DispatchQueue.main, execute: { // 2
        print(i, currentImage)
    })
    
    workItems.append(dispatchItem) // 3
}
  1. Ho inserito uno sleep(randomSleep) solo per simulare il download di immagini con diverso peso (bloccheranno il thread in cui verrà invocato per un valore compreso tra 0 e 2 secondi).
  2. Dopo che ho creato il DispatchItem ho invocato su questo il metodo notify sulla DispatchQueue.main. Il notify verrà attivato solamente quando il DispatchWorkItem avrà concluso la sua esecuzione permettendoci così di aggiornare la View con il dato scaricato.
  3. Infine ho aggiunto il dispatchItem all’array workItems. 

A questo punto ti basta creare una queue con attributo .concurrent:

let queue = DispatchQueue.init(label: "it.xcoding.queue", attributes: .concurrent)

for i in 0...workItems.count-1 {
    queue.async {
        let currentWorkItem = workItems[i]
        
        if i == 2 { currentWorkItem.cancel() } // simuliamo un errore sulla workItem 2 e cancelliamo la sua esecuzione

        if !currentWorkItem.isCancelled {
            currentWorkItem.perform()
        }
    }
}

Con un ciclo vado ad iterare tutti gli elementi dell’array workItems prendendo l’indice. Dentro il ciclo esegue un async sulla queue dove viene eseguito il DispatchWorkItem del ciclo corrente.

Quando il ciclo raggiunge la posizione i == 2 simulo un errore e blocco quel workItem. 

Vediamo cosa succede a livello d’esecuzione:

dispatchworkitem-linguaggio-swift

Dato che si tratta di una queue concurrent i task all’interno, creati dal ciclo, vengono invocati istantaneamente. Una volta finiti i DispatchWorkItem, questi notificano la main queue che provvederà a stampare in console il nome del file scaricato.

Caso particolare è il DispatchWorkItem 3 (cioè indice uguale a 2) che viene arrestato prima della sua esecuzione. Da notare che il metodo notify viene comunque invocato ma, dato che non è stata valorizzata la variabile con il nome del file, viene stampato solo l’indice ed una stringa vuota.

Quindi, in definitiva, quando conviene utilizzare i DispatchWorkItem?

I DispatchWorkItem, rispetto ad un’esecuzione ciclica del metodo async (avremmo potuto ottenere lo stesso risultato con un ciclo di queue.async), permettono di controllare l’esecuzione di ogni singolo task e di organizzare meglio il flusso d’esecuzione di un processo.

Infatti io ho scritto tutto all’interno della stessa funzione per semplificare le spiegazioni. In un progetto reale avrei potuto dividere la fase di definizione dei task rispetto a quella dell’esecuzione.

DispatchWorkGroup

Ipotizziamo d’avere un insieme di operazione asincrone, tipo quelle viste poco fa per il download di N immagini, e di voler eseguire un codice solamente alla fine di tutte le N operazioni.

Il Grand Central Dispatch mette a disposizione un oggetto particolare chiamato Dispatch Work Group che ti permetterà di raggruppare operazioni asincrone all’interno di un singolo oggetto.

Una volta creato un DispatchGroup, potrai inserire i task al suo interno utilizzando il metodo async, invocato su una queue, che ha per parametro group: DispatchGroup:

let workItemA = DispatchWorkItem.init {
    sleep(1)
    print("operation A")
}

let workItemB = DispatchWorkItem.init { 
    sleep(2)
    print("operation B")
}

let dispatchGroup = DispatchGroup.init()

let queue = DispatchQueue.init(label: "it.xcoding.queue")

queue.async(group: dispatchGroup, execute: workItemA)
queue.async(group: dispatchGroup, execute: workItemB)


dispatchGroup.notify(queue: DispatchQueue.main) {
    print("group operation ended")
}

Così facendo il notify verrà invocato solamente quando i due workItem termineranno.

Una cosa importante da dire è che il DispatchGroup può gestire DispatchWorkItem che lavorano su DispatchQueue differenti. Nel frattempo ne approfitto per farti vedere l’utilizzo del DispatchGroup senza l’utilizzo dei DispatchItem e del metodo d’inserimento automatico nel group.

let dispatchGroup = DispatchGroup.init()

let queueA = DispatchQueue.init(label: "it.xcoding.queueA")
let queueB = DispatchQueue.init(label: "it.xcoding.queueB")

dispatchGroup.enter()
queueA.async {
    sleep(1)
    print("dispatch su queueA")
    dispatchGroup.leave()
}

dispatchGroup.enter()
queueB.async {
    sleep(2)
    print("dispatch su queueB")
    dispatchGroup.leave()
}


dispatchGroup.notify(queue: DispatchQueue.main) {
    print("group operation ended")
}

Quando non vuoi utilizzare i DispatchWorkItem, ma vuoi comunque usufruire del DispatchGroup, puoi generare l’accesso manuale al gruppo.

Per capire il funzionamento del codice devi pensare che all’interno DispatchGroup c’è una variabile di tipo Int che di default è 0. Questa tiene il conto di quanti task sta gestendo. Se è 0 vuol dire che non c’è nessun task all’interno.

Quando la variabile aumenta di 1, e aumenta quando entra un DispatchWorkItem o quando si utilizza il metodo enter(), il DispatchGroup non può notificare l’esterno.

Per notificare l’esterno questo valore deve ritornare a zero. Per decrementare il valore si utilizza il metodo leave().

Leave() ed enter() sono di default inseriti quando si utilizza il metodo sync con il passaggio del group. In tutti gli altri casi questo step deve essere svolto manualmente.

Importante è capire che un enter deve essere sempre seguito da un leave perché il DispatchGroup non può trovarsi, a fine esecuzione, con il conteggio maggiore o minore di zero.

Nel codice ho inserito gli enter fuori dal metodo async perché volevo definire prima dell’avvio del task l’incremento del DispatchGroup. Se ci pensi è logico per in genere prima si avvisa dell’ingresso (bussando la porta) e poi successivamente si entra.

Lo stesso vale per l’uscita. Questi sono stati inseriti all’interno del task, ed alla fine, per notificare correttamente il DispatchGroup dell’effettiva uscita solamente a fine task.

Considerazioni

Il Grand Central Dispatch con il linguaggio Swift è uno degli argomenti più complessi da comprendere fino in fondo. Non tanto perché il codice è complesso, in questo Swift ci aiuta parecchio, ma tanto perché entrare nei meccanismi del multithreading e multicore richiede tempo per essere assimilato.

Nella maggior parte dei casi siamo abituati a pensare e ragionare proceduralmente (faccio A, poi B, poi C…). Poche volte proviamo a fare cose contemporaneamente e quando lo facciamo non vanno sempre bene (solo le donne hanno questa abilità).

Le macchine, vuoi o non vuoi, hanno la capacità di eseguire più compiti contemporaneamente e questo se sfruttato a pieno significa ridurre drasticamente tutti i tempi dei processi produttivi.

Quindi, pian piano e senza abusarne, comincia a modernizzare il comportamento del tuo codice sfruttando il parallelismo datoti dal Grand Central Dispatch. Se trovi qualcosa che appesantisce l’app (la rallenta), buttala su una Queue in background senza esitazioni (male che va ripristini il progetto).

Da oggi potrai dare il via alle danze con il Grand Central Dispatch!

Buona Programmazione!

P.S. Alcuni esempi sono una trasposizione di quanto fatto nel tutorial pubblicatodal sito web Appcoda.com che ho trovato ottimi per introdurre i concetti del Grand Central Dispatch con il linguaggio Swift.

Changelog

  • 21/03/2017 – Aggiunto il changelog. Prima versione del tutorial.