
Olá! Meu nome é Kiryl Famin e sou um desenvolvedor iOS.
Neste artigo, abordaremos o Grand Central Dispatch (GCD) de uma vez por todas. Embora o GCD possa parecer ultrapassado agora que o Swift Modern Concurrency existe, o código usando esse framework continuará a aparecer por muitos anos — tanto na produção quanto em entrevistas.
Hoje, focaremos somente no entendimento fundamental do GCD. Examinaremos somente os principais aspectos do multithreading em detalhes, incluindo o relacionamento entre filas e threads — um tópico que muitos outros artigos tendem a ignorar. Com esses conceitos em mente, será mais fácil para você entender tópicos como DispatchGroup
, DispatchBarrier
, semáforo, mutex e assim por diante.
Este artigo será útil tanto para iniciantes quanto para desenvolvedores experientes. Tentarei explicar tudo em linguagem clara, evitando sobrecarga de termos técnicos.
Thread – essencialmente, um contêiner onde um conjunto de instruções do sistema é colocado e executado. Na verdade, todo código executável roda em alguma thread. Nós distinguimos entre a thread principal e as threads de trabalho.
Multithreading – a habilidade de um sistema executar vários threads concorrentemente (ao mesmo tempo). Isso permite que múltiplos branches de código rodem em paralelo.
Grand Central Dispatch (GCD) – um framework que facilita o trabalho com threads (aproveitando os benefícios do multithreading). Seus principais primitivos são tarefas e filas.
Assim, GCD é uma ferramenta que facilita a escrita de código que é executado concorrentemente. Um exemplo simples é descarregar computações pesadas para um thread separado para não interferir nas atualizações de UI no thread principal.
Tarefa – um conjunto de instruções agrupadas pelo desenvolvedor. É importante entender que o desenvolvedor decide qual código pertence a uma tarefa específica.
Por exemplo:
print(“GCD”) // a task
let database = Database() let person = Person(age: 23) // also a task database.store(person)
Queue – o primitivo fundamental do GCD, é o lugar onde o desenvolvedor coloca tarefas para execução. A fila assume a responsabilidade de distribuir tarefas entre threads (cada fila tem acesso ao pool de threads do sistema).
Essencialmente, as filas permitem que você se concentre em organizar seu código em tarefas em vez de gerenciar threads diretamente. Quando você despacha uma tarefa para uma fila, ela será executada em uma thread disponível — geralmente diferente daquela usada para despachar a tarefa.
Você pode encontrar versões mp4 de todos os GIFs
Fila principal – uma fila que executa somente no thread principal. É serial (mais sobre isso depois).
let mainQueue = DispatchQueue.main
Filas globais – há 5 filas (uma para cada nível de prioridade) fornecidas pelo sistema. Elas são concorrentes.
let globalQueue = DispatchQueue.global()
Filas personalizadas – filas criadas pelo desenvolvedor. O desenvolvedor escolhe uma das 5 prioridades e o tipo: serial ou concurrent (por padrão, são seriais).
let userQueue = DispatchQueue(label: “com.kirylfamin.concurrent”, attributes: .concurrent).
Qualidade de Serviço (QoS) – um sistema para prioridades de fila. Quanto maior a prioridade da fila na qual uma tarefa é enfileirada, mais recursos são alocados a ela. Há 5 níveis de QoS no total:
.userInteractive
– a prioridade mais alta. É usado para tarefas que exigem execução imediata, mas não são adequadas para execução no thread principal. Por exemplo, em um aplicativo que permite retoque de imagem em tempo real, o resultado do retoque deve ser calculado instantaneamente; no entanto, se feito no thread principal, interferiria nas atualizações da IU e no tratamento de gestos que sempre ocorrem no thread principal (por exemplo, quando o usuário desliza o dedo sobre a área a ser retocada, e o aplicativo deve exibir instantaneamente o resultado “sob o dedo”). Assim, obtemos o resultado o mais rápido possível sem sobrecarregar o thread principal.
.userInitiated
– uma prioridade para tarefas que exigem feedback rápido, embora não tão crítico quanto tarefas interativas. É normalmente usado para tarefas em que o usuário entende que a tarefa não será concluída instantaneamente e terá que esperar (por exemplo, uma solicitação do servidor).
.default
– a prioridade padrão. É atribuída se o desenvolvedor não especificar um QoS ao criar uma fila – quando não há requisitos específicos para a tarefa e sua prioridade não pode ser determinada a partir do contexto (por exemplo, se você invocar uma tarefa de uma fila com uma prioridade .userInitiated, a tarefa herda essa prioridade).
.utility
– uma prioridade para tarefas que não exigem feedback imediato do usuário, mas são necessárias para a operação do aplicativo. Por exemplo, sincronizar dados com um servidor ou gravar um salvamento automático no disco.
.background
– a prioridade mais baixa. Um exemplo é a limpeza de cache.
Todas as filas são classificadas como Filas Seriais ou Filas Simultâneas
Filas seriais – como o nome indica, são filas onde as tarefas são executadas uma após a outra. Isso significa que a próxima tarefa começa somente após a atual terminar .
Filas simultâneas – Essas filas permitem que tarefas sejam executadas em paralelo – uma nova tarefa começa assim que os recursos são alocados, independentemente de tarefas anteriores terem sido concluídas. Observe que apenas a ordem de início é garantida (uma tarefa enfileirada anteriormente começará antes de uma posterior), mas a ordem de conclusão não é garantida.
É importante notar que agora estamos discutindo os métodos de execução de tarefas relativos ao thread de chamada . Em outras palavras, a maneira como você chama uma tarefa determina como os eventos se desenrolam no thread do qual você despacha a tarefa para uma fila.
async
)Uma chamada assíncrona é aquela em que o thread de chamada não é bloqueado, ou seja, ele não espera a tarefa que ele enfileira para ser executada.
DispatchQueue.main.async { print(“A”) } print(“B”)
Neste exemplo, enfileiramos de forma assíncrona a tarefa print("A")
na fila principal do thread principal (já que esse código não está dentro de nenhuma fila específica, ele é executado no thread principal por padrão). Assim, não esperamos pela tarefa no thread principal e continuamos a execução imediatamente. Neste exemplo em particular, a tarefa print("A")
é enfileirada na fila principal e então print("B")
é executada imediatamente no thread principal. Como o thread principal está ocupado executando o código atual (e tarefas da fila principal só podem ser executadas no thread principal), a tarefa atual print("B")
termina primeiro, e somente depois que o thread principal estiver livre a tarefa enfileirada na fila principal print("A")
é executada. A saída é: BA.
DispatchQueue.global().async { updateData() DispatchQueue.main.async { updateInterface() } Logger.log(.success) } indicateLoading()
Adicionamos de forma assíncrona uma tarefa à fila global com prioridade padrão do thread principal — para que o thread de chamada continue imediatamente e chame indicateLoading()
.
Depois de algum tempo, o sistema aloca recursos para a tarefa e a executa em um thread de trabalho livre do pool de threads, e updateData()
é chamado.
A tarefa que contém updateInterface()
é enfileirada de forma assíncrona na fila principal — o thread de trabalho que faz a chamada não espera sua conclusão e continua.
Como as tarefas são enfileiradas de forma assíncrona, não podemos ter certeza de quando os recursos serão alocados. Nesse caso, não podemos dizer com certeza se updateInterface()
(no thread principal) ou Logger.log(.success)
(em um thread worker) serão executados primeiro (nem podemos nas etapas 1-2: qual executa primeiro, indicateLoading()
no thread principal ou updateData()
em um thread worker). Embora o thread principal esteja ocupado manipulando atualizações de IU, processamento de gestos e outras tarefas subjacentes, ele sempre recebe o máximo de recursos do sistema. Por outro lado, recursos para execução em um thread worker também podem ser alocados quase imediatamente.
Observe que nesta animação uma fila global executa suas tarefas em algum thread de trabalho livre
sync
)Uma chamada síncrona é aquela em que o thread de chamada para e aguarda a conclusão da tarefa que ele colocou em uma fila.
let userQueue = DispatchQueue(label: "com.kirylfamin.serial") DispatchQueue.global().async { var account = BankAccount() userQueue.sync { account.balance += 10 } let balance = account.balance print(balance) }
Aqui, de um thread de trabalho executando uma tarefa na fila global, nós enfileiramos sincronicamente uma tarefa em uma fila personalizada para aumentar o saldo. O thread atual é bloqueado e aguarda a tarefa enfileirada terminar. Assim, o saldo é impresso somente após a tarefa na fila personalizada concluir o incremento.
Nota: Na animação acima, uma fila personalizada executa suas tarefas em algum thread de trabalho livre
No contexto de tarefas síncronas, é importante discutir deadlock — quando um thread ou threads esperam indefinidamente que eles mesmos ou uns aos outros prossigam. O exemplo mais comum é chamar DispatchQueue.main.sync {} do thread principal.
O thread principal está ocupado executando a tarefa atual, dentro da qual queremos executar algum código de forma síncrona. Assim, a chamada síncrona bloqueia o thread principal. A tarefa é enfileirada na fila principal, mas não pode ser iniciada porque o thread principal está bloqueado esperando a tarefa atual terminar — e as tarefas na fila principal só podem ser executadas no thread principal. Isso pode ser difícil de visualizar no início, mas a chave é entender que a tarefa enfileirada com DispatchQueue.main.sync
se torna parte da tarefa atual, e estamos enfileirando-a após a tarefa atual. Como resultado, o thread espera por uma parte da tarefa atual que não pode ser iniciada porque o thread está ocupado pela tarefa atual.
func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }
Observe que print("B")
da fila principal não pode ser executado porque o thread principal está bloqueado.
Nesta seção, com todo o conhecimento adquirido até agora, discutiremos exercícios de diferentes complexidades: de blocos de código simples que você encontrará em entrevistas a desafios avançados que desafiam sua compreensão de programação concorrente. A questão em todas essas tarefas é: O que será impresso no console?
Lembre-se de que a fila principal é serial, as filas globais() são simultâneas e, às vezes, o problema pode incluir filas personalizadas com atributos específicos.
Começaremos com tarefas de dificuldade normal – aquelas com pouca chance de incerteza no resultado. Essas tarefas são as mais propensas a aparecer em entrevistas; a chave é levar seu tempo e analisar cuidadosamente o problema.
Você pode encontrar o código completo de todos os exercícios aqui .
Tarefa 1
print(“A”) DispatchQueue.main.async { print(“B”) } print(“C”)
print("A")
é executado.print("B")
é enfileirada assincronamente na fila principal. Como o thread principal está ocupado, essa tarefa espera na fila.print("C")
é executado.print("B")
será executada.
Resposta : ACB
Tarefa 2
print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”)
print("A")
é executado.print("B")
é enfileirada na fila principal. A fila principal até que o thread principal fique disponível.print("C")
é enfileirada depois de print("B") e também aguarda.print("B")
— é executada.print("C")
é executada.
Resposta : ADBC
Devo mencionar imediatamente que, em alguns exemplos, simplificarei um pouco a explicação e omitirei o fato de que o sistema otimiza a execução de chamadas síncronas, o que discutiremos mais tarde.
Tarefa 3
print(“A”) DispatchQueue.main.async { // 1 print(“B”) DispatchQueue.main.async { print(“C”) } DispatchQueue.global().sync { print(“D”) } DispatchQueue.main.sync { // 2 print(“E”) } } // 3 print(“F”) DispatchQueue.main.async { print(“G”) }
print("A")
é executado no thread principal."F"
.print("G")
é enfileirada na fila principal após a tarefa anterior (etapas 1 a 3).print("B")
começa a ser executada.print("C")
é então enfileirada na fila principal (onde a tarefa atual ainda está em execução, e print("G")
a segue na fila). Como ela é adicionada de forma assíncrona, não esperamos por sua execução e seguimos em frente imediatamente.print("D")
é enfileirada na fila global. Como essa chamada é síncrona, esperamos até que a fila global a execute (ela pode ser executada em qualquer thread de trabalho disponível) antes de prosseguir.print("E")
é enfileirada na fila principal. Como essa chamada é síncrona, o thread atual deve ser bloqueado até que a tarefa seja concluída. No entanto, já há tarefas na fila principal, e a operação print("E")
é adicionada ao final, depois delas. Portanto, essas operações devem ser executadas primeiro antes que print("E")
possa ser executada. Mas o thread principal ainda está ocupado executando a operação atual, então ele não pode passar para as próximas operações enfileiradas. Mesmo se não houvesse operações para imprimir "G"
e "C"
após a operação atual, o thread ainda não poderia prosseguir porque a operação atual (etapas 1–3) ainda não foi concluída."G"
e "C"
.
Resposta : AFBD
Resposta alternativa (se a segunda chamada fosse async
): AFBDGCE
Tarefa 4
let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”) serialQueue.async { // 1 print(“A”) serialQueue.sync { print(“B”) } print(“C”) } // 2
Uma tarefa (etapas 1–2) é enfileirada de forma assíncrona em uma fila serial personalizada (por padrão, as filas são seriais, pois não usamos o atributo .concurrent
).
"A"
é impresso.print("B")
é enfileirada. Como a chamada é síncrona, o thread bloqueia esperando por sua execução.print("B")
não pode ser iniciada, resultando em um deadlock.
Resposta : A, impasse
Este exemplo mostra que um deadlock pode ocorrer em qualquer fila serial, seja na fila principal ou personalizada.
Tarefa 5
Vamos substituir a fila serial da tarefa anterior por uma simultânea.
DispatchQueue.global().async { // 1 print("A") DispatchQueue.global().sync { print("B") } print("C") } // 2
"A"
é impresso.print("B")
na mesma fila global é feita, o que bloqueia o thread de trabalho atual até que a tarefa seja concluída.print("B")
seja executada em outro thread de trabalho."C"
é impresso.Resposta : ABC
Tarefa 6
print("A") DispatchQueue.main.async { // 1 print("B") DispatchQueue.main.async { // 2 print("C") DispatchQueue.main.async { // 3 print("D") DispatchQueue.main.sync { print("E") } } // 4 } // 5 DispatchQueue.global().sync { // 6 print("F") DispatchQueue.global().sync { print("G") } } // 7 print("H") } // 8 print("I")
O thread principal imprime "A"
.
Uma tarefa assíncrona (etapas 1 a 8) é enfileirada na fila principal sem bloquear o thread atual.
O thread principal continua e imprime "I"
.
Mais tarde, quando o thread principal estiver livre, a tarefa enfileirada na fila principal inicia a execução e imprime "B"
.
Outra tarefa assíncrona (etapas 2 a 5) é enfileirada na fila principal – não bloqueando o thread atual.
Continuando a execução no thread atual, um despacho síncrono da operação 6–7 é feito para a fila global — isso bloqueia o thread atual (principal) até que a tarefa seja concluída.
A operação 6–7 começa a ser executada em outro thread, imprimindo "F"
.
A operação print("G")
é despachada de forma síncrona para a fila global, bloqueando o thread de trabalho atual até que seja concluída.
"G"
é impresso e o thread de trabalho do qual esta operação foi despachada é desbloqueado.
A operação 6–7 é concluída, desbloqueando o thread do qual foi despachada (o thread principal), e "H"
é impresso.
Após a conclusão da operação 1–2, a execução passa para a próxima operação na fila principal — operação 2–5 — que começa e imprime "C"
.
A operação 3–4 é enfileirada na fila principal sem bloquear o thread.
Assim que a operação atual (2–5) termina, a execução começa na próxima operação (3–4), imprimindo "D"
.
A operação print("G")
é despachada sincronicamente para a fila principal, bloqueando o thread atual.
O sistema então aguarda indefinidamente que a operação print("E")
seja executada no thread principal — como o thread está bloqueado, isso leva a um deadlock.
Resposta : AIBFGHCD, deadlock
Tarefas de dificuldade intermediária envolvem incerteza. Tais problemas também são encontrados em entrevistas, embora raramente.
Tarefa 7
DispatchQueue.global().async { print("A") } DispatchQueue.global().async { print("B") }
print("A")
é enfileirado de forma assíncrona na fila global, sem bloquear o thread atual.print("B")
. Neste caso em particular, a próxima tarefa é adicionada à fila primeiro, e somente então os recursos são alocados para a fila global. Isso acontece porque o thread principal é alocado com a maioria dos recursos, e a próxima operação no thread principal é muito leve (apenas a operação de adicionar uma tarefa) e, na prática, ocorre mais rápido do que a alocação de recursos na fila global. Discutiremos os cenários opostos na próxima seção.print("B")
é enfileirado na fila global."A"
possa começar antes de "B"
, não podemos garantir a ordem porque print não é uma operação atômica (o momento em que a saída aparece no console é próximo ao fim da operação).
Resposta : (AB)
Os parênteses indicam que as letras podem aparecer em qualquer ordem: AB ou BA.
Tarefa 8
print("A") DispatchQueue.main.async { print("B") } DispatchQueue.global().async { print("C") }
Aqui, só podemos ter certeza de que "A" é impresso primeiro. Não podemos determinar precisamente se a tarefa na fila principal ou a da fila global será executada mais rápido.
Resposta : A(BC)
Tarefa 9
DispatchQueue.global(qos: .userInteractive).async { print(“A”) } DispatchQueue.main.async { // 1 print(“B”) }
e
DispatchQueue.global(qos: .userInteractive).async { print(“A”) } print(“B”) // 1
Por um lado, em ambos os casos print("B")
é executado no thread principal. Além disso, não podemos determinar exatamente quando a fila global terá recursos alocados, então, teoricamente, "A"
pode ser impresso imediatamente antes de atingir o ponto marcado // 1 no thread principal. Na prática, no entanto, a primeira tarefa sempre imprime como AB, enquanto a segunda imprime como BA. Isso ocorre porque, no primeiro caso, print("B")
é executado pelo menos na próxima iteração RunLoop do thread principal (ou algumas iterações depois), enquanto no segundo caso, print("B")
é agendado para ser executado na iteração RunLoop atual no thread principal. No entanto, não podemos garantir a ordem.
Resposta para ambas as tarefas: (AB)
Tarefa 10
print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") }
É claro que o início da saída é "AB"
. Após enfileirar print("C")
, não podemos determinar exatamente quando os recursos serão alocados para ela — essa tarefa pode ser executada antes ou depois de print("D")
. Isso às vezes acontece na prática também.
Resposta : AB(CD)
Tarefa 11
let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { print(“A”) serialQueue.async { print(“B”) } print(“C”) }
Novamente, não podemos determinar precisamente quando os recursos serão alocados para print("B") na fila personalizada. Na prática, como o thread principal recebe a prioridade mais alta, "C" geralmente imprime antes de "B", embora isso não seja garantido.
Resposta : A(BC)
Tarefa 12
DispatchQueue.global().async { print("A") } print("B") sleep(1) print("C")
Aqui, é óbvio que a saída será BAC porque o sleep de um segundo garante que a fila global tenha tempo suficiente para alocar recursos. Enquanto o thread principal está bloqueado pelo sleep (o que você não deve fazer em produção), print("A")
executa em outro thread.
Resposta : BAC
Tarefa 13
DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C")
Neste caso, como print("A")
está enfileirado na fila principal, ele só pode ser executado no thread principal. No entanto, o thread principal continua executando o código — imprimindo "B"
, depois dormindo, depois imprimindo "C"
. Somente depois disso o RunLoop pode executar a tarefa enfileirada.
Resposta : BCA
É improvável que você encontre esses problemas em entrevistas, mas entendê-los ajudará você a entender melhor o MDC.
A classe Counter aqui é usada apenas para semântica de referência:
final class Counter { var count = 0 }
Tarefa 14
let counter = Counter() DispatchQueue.global().async { DispatchQueue.main.async { print(counter.count) } for _ in (0..<100) { // 1 counter.count += 1 } }
Aqui, qualquer número entre 0 e 100 pode ser impresso, dependendo de quão ocupada a thread principal está. Como sabemos, não podemos prever exatamente quando a tarefa assíncrona obterá recursos — pode acontecer antes, durante ou depois do loop na thread worker.
Resposta : 0-100
Tarefa 15
DispatchQueue.global(qos: .userInitiated).async { print(“A”) } DispatchQueue.global(qos: .userInteractive).async { print(“B”) }
QoS não garante que a fila com prioridade mais alta receberá recursos mais rápido, embora o iOS tente fazer isso. Na prática, a saída aqui é (AB).
Resposta : (AB)
Tarefa 16
var count = 0 DispatchQueue.global(qos: . userInitiated).async { for _ in 0..<1000 { count += 1 } print(“A”) } DispatchQueue.global(qos: .userInteractive).async { for _ in 0..<1000 { count += 1 } print(“B”) }
Como não podemos saber qual execução começa primeiro, mesmo em 1.000 operações não podemos determinar qual tarefa será concluída mais rápido.
Resposta : (AB)
Tarefa 16.2
Qual é a saída supondo que as operações comecem a ser executadas simultaneamente?
Como a fila .userInteractive recebe mais recursos, ao longo de 1000 operações a execução nessa fila sempre terminará mais rápido.
Resposta : BA
Tarefa 17
Usando uma abordagem semelhante, podemos modificar qualquer tarefa com incerteza da seção anterior (por exemplo, Tarefa 12):
let counter = Counter() let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { serialQueue.async { print(counter.count) } for _ in 0..<100 { counter.count += 1 } }
Qualquer número entre 0 e 100 pode ser impresso. O fato de que 0 pode ser impresso confirma que na Tarefa 12 não podemos garantir que a saída de "C"
sempre ocorrerá antes de "B"
, já que essencialmente nada mudou — apenas que o loop é um pouco mais intensivo em recursos do que um print (note que simplesmente iniciar o loop, mesmo antes de sua execução, resultou na prática em incerteza completa).
Resposta : 0-100
Tarefa 18
DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) }
Uma situação similar ocorre aqui. Em teoria, print("A")
pode ser executado mais rápido que print("B")
(se você substituir print("B")
por algo um pouco mais pesado). Na prática, "B"
sempre imprime primeiro. No entanto, o fato de executarmos print("B")
antes de enfileirar print("C")
aumenta muito a probabilidade de que "A"
seja impresso antes de "C"
, já que o tempo extra gasto em print("B")
no thread principal é frequentemente suficiente para a fila .userInitiated obter recursos e executar print("A")
. No entanto, isso não é garantido e às vezes "C"
pode ser impresso mais rápido. Assim, em teoria, há incerteza completa; na prática, tende a ser B(CA).
Resposta : (BCA)
Tarefa 19
DispatchQueue.global().sync { print(Thread.current) }
A documentação para sincronização afirma:
“Como uma otimização de desempenho, esta função executa blocos no thread atual sempre que possível, com uma exceção: blocos enviados para a fila de despacho principal sempre são executados no thread principal.”
Isso significa que, para fins de otimização, chamadas síncronas podem ser executadas no mesmo thread do qual foram chamadas (com exceção de main.sync
– tarefas que o usam sempre são executadas no thread principal). Assim, o thread atual (principal) é impresso.
Resposta : tópico principal
Tarefa 20
DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }
Somente "A"
é impresso porque ocorre um deadlock. Devido à otimização, a tarefa (rotulada 1) começa a ser executada no thread principal e, então, chamar main.sync
leva a um deadlock.
Resposta : A, impasse
Tarefa 21
DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") }
A otimização faz com que a tarefa print("B")
não seja enfileirada, mas seja “emendada” no thread de execução atual. Assim, o código:
DispatchQueue.global().sync { print("B") }
torna-se equivalente a:
print(“B”)
Resposta : ABC
A partir dessas tarefas, fica claro que você deve usar main.sync com muito cuidado, somente quando tiver certeza de que a chamada não é feita a partir do thread principal.
Neste artigo, focamos nos conceitos fundamentais de multithreading no iOS — threads, tarefas e filas — e seus inter-relacionamentos. Exploramos como o GCD gerencia a execução de tarefas nas filas principais, globais e personalizadas, e discutimos as diferenças entre execução serial e simultânea. Além disso, examinamos as distinções críticas entre despacho de tarefas síncronas (sync) e assíncronas (async), destacando como essas abordagens afetam a ordem e o tempo de execução do código. Dominar esses conceitos básicos é essencial para construir aplicativos estáveis e responsivos e para evitar armadilhas comuns, como deadlocks.
Espero que você tenha encontrado algo útil neste artigo. Se algo permanecer obscuro, sinta-se à vontade para entrar em contato comigo para uma explicação gratuita no Telegram: @kfamyn .
sync
- https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw