Innanzitutto, è importante conoscere la differenza tra thread e code e cosa fa realmente GCD. Quando usiamo le code di invio (tramite GCD), stiamo davvero facendo la coda, non il threading. Il framework Dispatch è stato progettato specificamente per allontanarci dal threading, poiché Apple ammette che "implementare una soluzione di threading corretta [può] diventare estremamente difficile, se non [a volte] impossibile da ottenere". Pertanto, per eseguire attività contemporaneamente (attività che non vogliamo congelare l'interfaccia utente), tutto ciò che dobbiamo fare è creare una coda di tali attività e passarla a GCD. E GCD gestisce tutti i thread associati. Pertanto, tutto ciò che stiamo facendo è fare la fila.
La seconda cosa da sapere subito è cos'è un compito. Un'attività è tutto il codice all'interno di quel blocco di coda (non all'interno della coda, perché possiamo aggiungere cose a una coda tutto il tempo, ma entro la chiusura in cui lo abbiamo aggiunto alla coda). A volte ci si riferisce a un'attività come a un blocco e a un blocco a volte come a un'attività (ma sono più comunemente note come attività, in particolare nella comunità Swift). E non importa quanto codice o poco codice, tutto il codice all'interno delle parentesi graffe è considerato una singola attività:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
Ed è ovvio menzionare che simultaneo significa semplicemente allo stesso tempo con altre cose e significa seriale uno dopo l'altro (mai allo stesso tempo). Serializzare qualcosa, o mettere qualcosa in seriale, significa semplicemente eseguirlo dall'inizio alla fine nel suo ordine da sinistra a destra, dall'alto verso il basso, senza interruzioni.
Esistono due tipi di code, seriale e simultanea, ma tutte le code sono simultanee l'una rispetto all'altra . Il fatto che si desideri eseguire qualsiasi codice "in background" significa che si desidera eseguirlo contemporaneamente a un altro thread (di solito il thread principale). Pertanto, tutte le code di invio, seriali o simultanee, eseguono le loro attività contemporaneamente rispetto ad altre code . Qualsiasi serializzazione eseguita da code (da code seriali), ha solo a che fare con le attività all'interno di quella singola coda di invio [seriale] (come nell'esempio sopra dove ci sono due attività all'interno della stessa coda seriale; quelle attività verranno eseguite una dopo l'altro, mai contemporaneamente).
CODE SERIALI (spesso note come code di invio private) garantiscono l'esecuzione delle attività una alla volta dall'inizio alla fine nell'ordine in cui sono state aggiunte a quella coda specifica. Questa è l'unica garanzia di serializzazione ovunque nella discussione sulle code di invio--che le attività specifiche all'interno di una coda seriale specifica vengono eseguite in serie. Le code seriali possono, tuttavia, essere eseguite simultaneamente con altre code seriali se sono code separate perché, ancora una volta, tutte le code sono simultanee l'una rispetto all'altra. Tutte le attività vengono eseguite su thread distinti, ma non è garantito che tutte le attività vengano eseguite sullo stesso thread (non importante, ma interessante da sapere). E il framework iOS non viene fornito con code seriali pronte per l'uso, devi crearle. Le code private (non globali) sono seriali per impostazione predefinita, quindi per creare una coda seriale:
let serialQueue = DispatchQueue(label: "serial")
Puoi renderlo concorrente attraverso la sua proprietà attributo:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Ma a questo punto, se non stai aggiungendo altri attributi alla coda privata, Apple consiglia di utilizzare solo una delle loro code globali pronte all'uso (che sono tutte simultanee). In fondo a questa risposta, vedrai un altro modo per creare code seriali (utilizzando la proprietà target), che è il modo in cui Apple consiglia di farlo (per una gestione delle risorse più efficiente). Ma per ora, etichettarlo è sufficiente.
CODE CONCORRENTI (spesso note come code di invio globali) possono eseguire attività contemporaneamente; tuttavia, è garantito che le attività vengano avviate nell'ordine in cui sono state aggiunte a quella coda specifica, ma a differenza delle code seriali, la coda non attende il completamento della prima attività prima di avviare la seconda. Le attività (come con le code seriali) vengono eseguite su thread distinti e (come con le code seriali) non è garantito che tutte le attività vengano eseguite sullo stesso thread (non importante, ma interessante da sapere). E il framework iOS viene fornito con quattro code simultanee pronte per l'uso. Puoi creare una coda simultanea usando l'esempio sopra o usando una delle code globali di Apple (che di solito è consigliata):
let concurrentQueue = DispatchQueue.global(qos: .default)
RESISTENTE AL CICLO DI RETAIN: le code di invio sono oggetti conteggiati in riferimento ma non è necessario conservare e rilasciare le code globali perché sono globali, quindi la conservazione e il rilascio vengono ignorati. È possibile accedere direttamente alle code globali senza doverle assegnare a una proprietà.
Esistono due modi per inviare le code: in modo sincrono e asincrono.
SYNC DISPATCHING significa che il thread in cui è stata inviata la coda (il thread chiamante) si interrompe dopo l'invio della coda e attende che l'attività in quel blocco di coda termini l'esecuzione prima di riprendere. Per inviare in modo sincrono:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING significa che il thread chiamante continua a essere eseguito dopo l'invio della coda e non attende che l'attività in quel blocco di coda termini l'esecuzione. Per inviare in modo asincrono:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Ora si potrebbe pensare che per eseguire un'attività in seriale, dovrebbe essere utilizzata una coda seriale, e questo non è esattamente corretto. Per eseguire più attività in serie, è necessario utilizzare una coda seriale, ma tutte le attività (isolate da sole) vengono eseguite in serie. Considera questo esempio:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
Indipendentemente dal modo in cui configuri (seriale o simultaneo) o invii (sincronizza o asincrono) questa coda, questa attività verrà sempre eseguita in serie. Il terzo ciclo non verrà mai eseguito prima del secondo ciclo e il secondo ciclo non verrà mai eseguito prima del primo ciclo. Questo è vero in qualsiasi coda utilizzando qualsiasi invio. È quando si introducono più attività e / o code in cui la seriale e la concorrenza entrano davvero in gioco.
Considera queste due code, una seriale e una simultanea:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Supponiamo di inviare due code simultanee in modo asincrono:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Il loro output è confuso (come previsto) ma si noti che ogni coda ha eseguito la propria attività in serie. Questo è l'esempio più semplice di concorrenza: due attività in esecuzione contemporaneamente in background nella stessa coda. Ora creiamo il primo seriale:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
La prima coda non dovrebbe essere eseguita in serie? Lo era (e così è stato il secondo). Qualunque altra cosa sia accaduta in background non riguarda la coda. Abbiamo detto alla coda seriale di eseguire in seriale e lo ha fatto ... ma gli abbiamo assegnato solo un compito. Ora diamo due compiti:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
E questo è l'esempio più semplice (e l'unico possibile) di serializzazione: due attività in esecuzione in serie (una dopo l'altra) in background (sul thread principale) nella stessa coda. Ma se li abbiamo creati due code seriali separate (perché nell'esempio sopra sono la stessa coda), il loro output è di nuovo confuso:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
E questo è ciò che intendevo quando ho detto che tutte le code sono simultanee l'una rispetto all'altra. Si tratta di due code seriali che eseguono le loro attività contemporaneamente (perché sono code separate). Una coda non conosce o non si preoccupa delle altre code. Ora torniamo a due code seriali (della stessa coda) e aggiungiamo una terza coda, simultanea:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
È un po 'inaspettato, perché la coda simultanea ha atteso il termine delle code seriali prima di essere eseguita? Questa non è concorrenza. Il tuo parco giochi potrebbe mostrare un output diverso ma il mio lo ha mostrato. E lo ha dimostrato perché la priorità della mia coda simultanea non era abbastanza alta da consentire a GCD di eseguire il suo compito prima. Quindi, se mantengo tutto uguale ma cambio il QoS della coda globale (la sua qualità del servizio, che è semplicemente il livello di priorità della coda) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, l'output è come previsto:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Le due code seriali hanno eseguito le loro attività in serie (come previsto) e la coda simultanea ha eseguito la sua attività più rapidamente perché le è stato assegnato un livello di priorità elevato (un alto QoS o qualità del servizio).
Due code simultanee, come nel nostro primo esempio di stampa, mostrano una stampa confusa (come previsto). Per farli stampare ordinatamente in serie, dovremmo renderli entrambi la stessa coda seriale (la stessa istanza di quella coda, e non solo la stessa etichetta) . Quindi ogni attività viene eseguita in serie rispetto all'altra. Un altro modo, tuttavia, per farli stampare in serie è mantenerli entrambi simultanei ma cambiare il loro metodo di spedizione:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Ricorda, l'invio di sincronizzazione significa solo che il thread chiamante attende fino al completamento dell'attività nella coda prima di procedere. L'avvertenza qui, ovviamente, è che il thread chiamante viene bloccato fino al completamento della prima attività, il che può essere o meno il modo in cui desideri che l'interfaccia utente venga eseguita.
Ed è per questo motivo che non possiamo fare quanto segue:
DispatchQueue.main.sync { ... }
Questa è l'unica combinazione possibile di code e metodi di invio che non possiamo eseguire: invio sincrono sulla coda principale. E questo perché stiamo chiedendo alla coda principale di bloccarsi fino a quando non eseguiamo l'attività all'interno delle parentesi graffe ... che abbiamo inviato alla coda principale, che abbiamo appena congelato. Questo si chiama deadlock. Per vederlo in azione in un parco giochi:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
Un'ultima cosa da menzionare sono le risorse. Quando assegniamo un'attività a una coda, GCD trova una coda disponibile dal pool gestito internamente. Per quanto riguarda la scrittura di questa risposta, ci sono 64 code disponibili per qos. Può sembrare molto, ma possono essere consumati rapidamente, specialmente da librerie di terze parti, in particolare framework di database. Per questo motivo, Apple offre consigli sulla gestione delle code (menzionati nei collegamenti sottostanti); un essere:
Invece di creare code simultanee private, inviare le attività a una delle code di invio simultanee globali. Per le attività seriali, impostare la destinazione della coda seriale su una delle code simultanee globali.
In questo modo, puoi mantenere il comportamento serializzato della coda riducendo al minimo il numero di code separate che creano thread.
Per fare ciò, invece di crearli come abbiamo fatto prima (cosa che puoi ancora fare), Apple consiglia di creare code seriali come questa:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Per ulteriori letture, consiglio quanto segue:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue