Primeiro, é importante saber a diferença entre threads e filas e o que o GCD realmente faz. Quando usamos filas de despacho (por meio de GCD), estamos realmente enfileirando, não encadeando. A estrutura do Dispatch foi projetada especificamente para nos afastar do threading, já que a Apple admite que "implementar uma solução de threading correta [pode] se tornar extremamente difícil, se não [às vezes] impossível de alcançar." Portanto, para realizar tarefas simultaneamente (tarefas que não queremos travar a IU), tudo o que precisamos fazer é criar uma fila dessas tarefas e entregá-la ao GCD. E o GCD trata de todo o threading associado. Portanto, tudo o que realmente estamos fazendo é uma fila.
A segunda coisa a saber imediatamente é o que é uma tarefa. Uma tarefa é todo o código dentro desse bloco de fila (não dentro da fila, porque podemos adicionar coisas a uma fila o tempo todo, mas dentro do fechamento onde adicionamos à fila). Uma tarefa às vezes é referida como um bloco e um bloco às vezes é referido como uma tarefa (mas eles são mais comumente conhecidos como tarefas, particularmente na comunidade Swift). E não importa quanto código ou pouco, todos os códigos entre chaves são considerados uma única tarefa:
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
}
E é óbvio mencionar que simultâneo simplesmente significa ao mesmo tempo com outras coisas e serial significa um após o outro (nunca ao mesmo tempo). Serializar algo, ou colocar algo em serial, significa apenas executá-lo do início ao fim na ordem da esquerda para a direita, de cima para baixo, sem interrupções.
Existem dois tipos de filas, seriais e simultâneas, mas todas as filas são simultâneas entre si . O fato de você querer executar qualquer código "em segundo plano" significa que você deseja executá-lo simultaneamente com outro encadeamento (geralmente o encadeamento principal). Portanto, todas as filas de despacho, seriais ou simultâneas, executam suas tarefas simultaneamente em relação a outras filas . Qualquer serialização realizada por filas (por filas seriais), tem a ver apenas com as tarefas dentro dessa única fila de despacho [serial] (como no exemplo acima, onde há duas tarefas dentro da mesma fila serial; essas tarefas serão executadas uma após o outro, nunca simultaneamente).
AS SERIAL QUEUES (também conhecidas como filas de despacho privadas) garantem a execução de tarefas uma por vez, do início ao fim, na ordem em que foram adicionadas a essa fila específica. Esta é a única garantia de serialização em qualquer lugar na discussão de filas de despacho--que as tarefas específicas dentro de uma fila serial específica são executadas em série. No entanto, as filas seriais podem ser executadas simultaneamente com outras filas seriais se forem filas separadas porque, novamente, todas as filas são concorrentes umas em relação às outras. Todas as tarefas são executadas em threads distintos, mas nem todas as tarefas são executadas no mesmo thread (não é importante, mas é interessante saber). E a estrutura do iOS não vem com nenhuma fila serial pronta para usar, você deve criá-la. As filas privadas (não globais) são seriais por padrão, portanto, para criar uma fila serial:
let serialQueue = DispatchQueue(label: "serial")
Você pode torná-lo simultâneo por meio de sua propriedade de atributo:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Mas, neste ponto, se você não estiver adicionando nenhum outro atributo à fila privada, a Apple recomenda que você use apenas uma de suas filas globais prontas para uso (que são todas simultâneas). No final desta resposta, você verá outra maneira de criar filas seriais (usando a propriedade target), que é como a Apple recomenda fazer (para um gerenciamento de recursos mais eficiente). Mas, por enquanto, rotulá-lo é suficiente.
CONCURRENT QUEUES (frequentemente conhecidas como filas de despacho global) podem executar tarefas simultaneamente; as tarefas, no entanto, têm a garantia de iniciar na ordem em que foram adicionadas a essa fila específica, mas, ao contrário das filas seriais, a fila não espera que a primeira tarefa termine antes de iniciar a segunda tarefa. Tarefas (como com filas seriais) são executadas em threads distintos e (como com filas seriais) nem todas as tarefas têm garantia de execução no mesmo thread (não é importante, mas é interessante saber). E a estrutura do iOS vem com quatro filas simultâneas prontas para usar. Você pode criar uma fila simultânea usando o exemplo acima ou usando uma das filas globais da Apple (o que geralmente é recomendado):
let concurrentQueue = DispatchQueue.global(qos: .default)
RETAIN-CYCLE RESISTANT: As filas de despacho são objetos contados por referência, mas você não precisa reter e liberar filas globais porque são globais e, portanto, a retenção e a liberação são ignoradas. Você pode acessar as filas globais diretamente, sem precisar atribuí-las a uma propriedade.
Existem duas maneiras de despachar filas: de forma síncrona e assíncrona.
SYNC DISPATCHING significa que o encadeamento onde a fila foi despachada (o encadeamento de chamada) pausa após despachar a fila e espera que a tarefa nesse bloco de fila termine de ser executada antes de continuar. Para despachar de forma síncrona:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING significa que o thread de chamada continua a ser executado após o despacho da fila e não espera que a tarefa nesse bloco de fila termine de ser executada. Para despachar de forma assíncrona:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Agora, pode-se pensar que, para executar uma tarefa em série, uma fila serial deve ser usada, o que não é exatamente correto. Para executar várias tarefas em série, deve-se usar uma fila serial, mas todas as tarefas (isoladas por si mesmas) são executadas em série. Considere este exemplo:
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)
}
}
Não importa como você configura (serial ou simultâneo) ou despacha (sincroniza ou assíncrona) esta fila, esta tarefa sempre será executada em serial. O terceiro loop nunca será executado antes do segundo loop e o segundo loop nunca será executado antes do primeiro. Isso é verdade em qualquer fila usando qualquer despacho. É quando você introduz várias tarefas e / ou filas que a série e a simultaneidade realmente entram em jogo.
Considere essas duas filas, uma serial e outra simultânea:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Digamos que despachamos duas filas simultâneas em assíncrono:
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
Sua saída é confusa (como esperado), mas observe que cada fila executou sua própria tarefa em série. Este é o exemplo mais básico de simultaneidade - duas tarefas em execução ao mesmo tempo em segundo plano na mesma fila. Agora vamos fazer o primeiro serial:
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
A primeira fila não deveria ser executada em série? Foi (e assim foi o segundo). O que quer que tenha acontecido em segundo plano não é motivo de preocupação para a fila. Dissemos à fila serial para executar em série e assim fez ... mas demos apenas uma tarefa. Agora vamos dar duas tarefas:
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 este é o exemplo mais básico (e apenas possível) de serialização - duas tarefas em execução em série (uma após a outra) em segundo plano (para o thread principal) na mesma fila. Mas se fizermos delas duas filas seriais separadas (porque no exemplo acima elas são a mesma fila), sua saída será confundida novamente:
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 é isso que eu quis dizer quando disse que todas as filas são simultâneas umas em relação às outras. Essas são duas filas seriais executando suas tarefas ao mesmo tempo (porque são filas separadas). Uma fila não conhece ou se preocupa com outras filas. Agora vamos voltar para duas filas seriais (da mesma fila) e adicionar uma terceira fila, uma concorrente:
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
Isso é meio inesperado, por que a fila simultânea esperou que as filas seriais terminassem antes de ser executada? Isso não é simultaneidade. Seu playground pode mostrar uma saída diferente, mas o meu mostrou isso. E mostrou isso porque a prioridade da minha fila simultânea não era alta o suficiente para o GCD executar sua tarefa mais cedo. Portanto, se eu mantiver tudo igual, mas alterar a QoS da fila global (sua qualidade de serviço, que é simplesmente o nível de prioridade da fila) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, a saída será a esperada:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
As duas filas seriais executaram suas tarefas em série (como esperado) e a fila simultânea executou sua tarefa mais rapidamente porque recebeu um nível de alta prioridade (um alto QoS ou qualidade de serviço).
Duas filas simultâneas, como em nosso primeiro exemplo de impressão, mostram uma impressão confusa (como esperado). Para fazer com que eles imprimam perfeitamente em serial, teríamos que fazer com que ambos tenham a mesma fila serial (a mesma instância dessa fila, não apenas a mesma etiqueta) . Então, cada tarefa é executada em série em relação à outra. Outra maneira, no entanto, de fazer com que eles imprimam em série é mantê-los simultâneos, mas alterando seu método de envio:
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
Lembre-se de que o envio de sincronização significa apenas que o thread de chamada espera até que a tarefa na fila seja concluída antes de prosseguir. A ressalva aqui, obviamente, é que o thread de chamada é congelado até que a primeira tarefa seja concluída, o que pode ou não ser como você deseja que a IU funcione.
E é por esta razão que não podemos fazer o seguinte:
DispatchQueue.main.sync { ... }
Esta é a única combinação possível de filas e métodos de despacho que não podemos realizar - despacho síncrono na fila principal. E isso porque estamos pedindo que a fila principal congele até que executemos a tarefa entre as chaves ... que despachamos para a fila principal, que acabamos de congelar. Isso é chamado de deadlock. Para vê-lo em ação em um playground:
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
Uma última coisa a mencionar são os recursos. Quando atribuímos uma tarefa a uma fila, o GCD encontra uma fila disponível em seu pool gerenciado internamente. Até a redação desta resposta, existem 64 filas disponíveis por qos. Isso pode parecer muito, mas eles podem ser consumidos rapidamente, especialmente por bibliotecas de terceiros, especialmente estruturas de banco de dados. Por esse motivo, a Apple tem recomendações sobre gerenciamento de filas (mencionadas nos links abaixo); sendo um:
Em vez de criar filas simultâneas privadas, envie tarefas para uma das filas de despacho simultâneas globais. Para tarefas seriais, defina o destino de sua fila serial para uma das filas simultâneas globais.
Dessa forma, você pode manter o comportamento serializado da fila enquanto minimiza o número de filas separadas criando threads.
Para fazer isso, em vez de criá-los como fizemos antes (o que você ainda pode), a Apple recomenda a criação de filas seriais como este:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Para ler mais, recomendo o seguinte:
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