
-
Adote os recursos de concorrência do Swift
Saiba mais sobre os principais conceitos de concorrência no Swift. A concorrência ajuda a melhorar a resposta e o desempenho dos apps, e o Swift facilitar a escrita correta de códigos assíncronos e concorrentes. Abordaremos as etapas que você precisa realizar para transformar um app de thread única em concorrente. Também ajudaremos você a determinar como e quando usar melhor os recursos de concorrência do Swift, seja ao tornar seu código mais assíncrono, movê-lo para o segundo plano ou compartilhar dados em tarefas concorrentes.
Capítulos
- 0:00 - Introdução
- 3:17 - Código de thread única
- 6:00 - Tarefas assíncronas
- 7:24 - Interleaving
- 10:22 - Introdução à concorrência
- 11:07 - Funções concorrentes
- 13:10 - Código não isolado
- 14:13 - Pool de threads concorrentes
- 14:58 - Compartilhar dados
- 15:49 - Tipos de valor
- 17:16 - Tipos isolados de ator
- 18:30 - Classes
- 23:18 - Atores
- 26:12 - Conclusão
Recursos
Vídeos relacionados
WWDC25
WWDC23
-
Buscar neste vídeo...
Olá! Sou Doug, da equipe do Swift. Vou mostrar como fazer o melhor uso da concorrência do Swift no seu app. Com a concorrência, o código pode executar várias tarefas ao mesmo tempo. Você pode usá-la no app para deixá-lo mais responsivo ao aguardar dados, como ler arquivos do disco ou fazer uma solicitação de rede. Também é possível mover tarefas pesadas para segundo plano, como processar imagens grandes. O modelo de concorrência do Swift foi criado para facilitar a escrita correta de código concorrente. Ele torna explícita a introdução da concorrência e identifica quais dados são compartilhados entre tarefas concorrentes. Ele usa essas informações para identificar possíveis corridas de dados em tempo de compilação para você introduzir a concorrência conforme necessário, sem medo de criar corridas de dados difíceis de corrigir.
Muitos apps só precisam usar a concorrência com moderação, e alguns não precisam. O código concorrente é mais complexo do que o código de thread única, e só deve ser usado quando necessário.
Seus apps devem começar executando todo o código na thread principal, e você pode ir longe com o código de thread única. A thread principal é onde o app recebe eventos relacionados à interface e pode atualizá-la em resposta. Se você não está fazendo muita computação no seu app, pode manter tudo na thread principal! É provável que você precise introduzir um código assíncrono, talvez para buscar conteúdo pela rede. Seu código pode esperar que o conteúdo entre na rede sem fazer com que a interface trave. Se essas tarefas levarem muito tempo para serem executadas, poderemos movê-las para uma thread em segundo plano executada concorrentemente com a principal.
À medida que o app cresce, manter todos os dados na thread principal pode prejudicar o desempenho. Aqui, podemos introduzir tipos de dados para fins específicos que sempre são executados em segundo plano.
A concorrência do Swift fornece ferramentas como agentes e tarefas para expressar esses tipos de operações concorrentes. É provável que um grande app tenha uma arquitetura um pouco parecida com esta. Mas você não começa por aí, e nem todo app precisa acabar aqui. Nesta sessão, vamos falar sobre as etapas de um app nessa jornada para passar de thread única para concorrente. Vamos ajudar você a entender quando dar cada passo, quais recursos de linguagem do Swift usar, como usá-los de forma eficaz e por que eles funcionam assim. Primeiro, descreveremos como o código de thread única funciona com a concorrência do Swift. Depois, apresentaremos tarefas assíncronas para ajudar em operações de alta latência, como acesso à rede. Depois, mostraremos como mover tarefas para segundo plano e compartilhar dados entre threads com segurança. Por fim, vamos retirar os dados do tópico principal com os agentes. Vamos começar com o código de thread única. Quando você executa um programa, o código começa a ser executado na thread principal. Qualquer código adicionado permanece na thread principal até você introduzir explicitamente a concorrência para executar o código em outro lugar. O código de thread única é mais fácil de escrever e manter porque faz apenas uma coisa de cada vez. Se você introduzir concorrência depois, o Swift protegerá o código na thread principal.
A thread principal e todos os seus dados são representados pelo agente principal. Não há concorrência no agente principal porque há apenas uma thread principal que pode executá-lo. Podemos especificar que os dados ou o código estão no agente principal usando a notação @MainActor. O Swift garantirá que o código do agente principal seja executado somente na thread principal e que os dados sejam acessados somente a partir dele. Dizemos que tal código é isolado do agente principal. O Swift protege seu código de thread principal usando o agente principal por padrão. Isso é como o compilador do Swift escrevendo @MainActor para você em tudo naquele módulo. Assim, podemos acessar variáveis estáticas compartilhadas de qualquer lugar no código. No modo de agente principal, não precisamos nos preocupar com o acesso concorrente até começarmos a introduzir a concorrência. A proteção do código com o agente principal por padrão é orientada por um ajuste de compilação. Use isso principalmente para seu módulo de app principal e módulos focados em interações da interface. Esse modo é ativado por padrão em novos apps criados com o Xcode 26. Neste vídeo, assumiremos que o modo de agente principal está ativado em todos os exemplos de código.
Vamos adicionar um método no nosso modelo de imagem para buscar e exibir uma imagem de um URL. Queremos carregar uma imagem de um arquivo local. Depois, vamos decodificar e exibir essa imagem na interface. Nosso app não tem nenhuma concorrência. Há apenas uma única thread principal fazendo todo o trabalho. Essa função roda inteira na thread principal de uma vez só. Se todas as operações aqui forem rápidas o suficiente, tudo bem.
No momento, só podemos ler arquivos localmente. Se quisermos permitir que nosso app busque uma imagem pela rede, precisaremos usar outra API.
Essa API URLSession nos permite buscar dados pela rede com um URL. No entanto, a execução desse método na thread principal congela a interface até que os dados sejam baixados da rede. Como desenvolvedor, é importante manter seu app responsivo. Ou seja, tome cuidado para não amarrar a thread principal por tanto tempo a ponto de a interface falhar ou travar. A concorrência do Swift fornece ferramentas para ajudar: Tarefas assíncronas podem ser usadas ao aguardar dados, como uma solicitação de rede, sem bloquear a thread principal.
Para evitar travamentos como esse, o acesso à rede é assíncrono. Podemos alterar fetchAndDisplayImage para que lide com chamadas assíncronas tornando a função async e chamando o URL da sessão da API com await. A espera indica onde a função pode ser suspensa, ou seja, ela deixa de ser executada na thread atual até que o evento que está aguardando aconteça. Em seguida, ela pode retomar a execução.
Podemos pensar nisso como quebrar a função em duas partes: a parte que corre até começarmos a buscar a imagem e a que corre depois que a imagem foi buscada. Ao dividir a função dessa forma, permitimos que outros trabalhos sejam executados entre as duas partes, mantendo a interface responsiva.
Na prática, muitas APIs de biblioteca, como a URLSession, descarregam o trabalho em segundo plano para você. Ainda não introduzimos a concorrência no nosso próprio código porque não precisávamos! Melhoramos a capacidade de resposta do app com código assíncrono e APIs de biblioteca que já descarregam o trabalho em nosso nome. Tudo o que precisávamos fazer em nosso código era adotar async/await.
Até agora, nosso código está executando apenas uma função async. Uma função async é executada em uma tarefa. Uma tarefa é executada independentemente de outro código e deve ser criada para executar uma operação específica de ponta a ponta. É comum criar uma tarefa em resposta a um evento, como pressionar um botão. Aqui, a tarefa executa a operação completa de busca e exibição de imagens. Pode haver muitas tarefas assíncronas em um determinado app. Além da tarefa de buscar e exibir imagens de que estamos falando, adicionei uma segunda tarefa que busca as notícias, as exibe e aguarda uma atualização. Cada tarefa concluirá suas operações em ordem do início ao fim. A busca acontece em segundo plano, mas as outras operações em cada tarefa serão executadas na thread principal, e apenas uma operação poderá ser executada por vez. As tarefas são independentes umas das outras, portanto, cada tarefa pode se revezar na thread principal. A thread principal executará as partes de cada tarefa à medida que elas ficarem prontas para serem executadas. Uma única thread alternando entre várias tarefas é chamada de interleaving. Isso melhora o desempenho geral, fazendo o uso mais eficiente dos recursos do sistema. Uma thread pode começar a progredir em uma tarefa o mais rápido possível, em vez de ficar ociosa enquanto espera uma única operação. Se a busca da imagem for concluída antes de buscar a notícia, a thread principal começará a decodificar e exibir a imagem antes de exibir a notícia. Mas se a busca da notícia terminar primeiro, a thread principal poderá começar a exibir a notícia antes de decodificar a imagem.
Várias tarefas assíncronas são ótimas quando seu app precisa executar várias operações independentes ao mesmo tempo. Quando você precisa executar o trabalho em uma ordem específica, deve fazer isso em uma única tarefa.
Para tornar seu app responsivo quando houver operações de alta latência, como uma solicitação de rede, use uma tarefa assíncrona para ocultar essa latência. As bibliotecas podem ajudar aqui fornecendo APIs assíncronas que podem criar concorrência por você, enquanto seu próprio código permanece na thread principal. A API URLSession já introduziu concorrência para nós porque está gerenciando o acesso à rede em uma thread em segundo plano. Nossa própria operação de imagem de busca e exibição está sendo executada na thread principal. Podemos achar que a operação de decodificação está demorando muito. Isso pode acontecer quando a interface trava ao decodificar uma imagem grande.
Assíncrono e de thread única geralmente é suficiente para um app. Mas se você notar que seu app não está respondendo, é um indício de que muita coisa está acontecendo na thread principal. Uma ferramenta de criação de perfil, como Instruments, pode ajudar você a determinar onde está gastando muito tempo. Se o trabalho puder ser feito mais rapidamente sem concorrência, faça isso primeiro. Se não for possível fazê-lo mais rápido, talvez seja necessário introduzir a concorrência. É ela que permite que partes do seu código sejam executadas em segundo plano junto à thread principal, sem travar a interface. Ela também pode ser usada para fazer o trabalho mais rápido usando mais núcleos da CPU no sistema. Nosso objetivo é tirar a decodificação da thread principal para que o trabalho possa acontecer na thread em segundo plano. Como estamos no modo de agente principal por padrão, fetchAndDisplaylmage e decodelmage são isoladas para o agente principal. O código do agente principal pode acessar livremente todos os dados e códigos acessíveis apenas para a thread principal. Isso é seguro porque não há concorrência na thread principal.
Queremos descarregar a chamada para decodeImage. Para isso, aplicamos o atributo @concurrent à função decodeImage. @concurrent diz ao Swift para executar a função em segundo plano. Alterar onde decodeImage é executada também altera nossas suposições sobre qual estado a decodeImage pode acessar. Vamos dar uma olhada na implementação. A implementação está verificando um dicionário de dados de imagem em cache armazenados no agente principal, o que só pode ser feito com segurança na thread principal. O compilador do Swift mostra onde a função tenta acessar dados sobre o agente principal. É isso que precisamos saber para ter certeza de que não estamos introduzindo falhas ao adicionar concorrência. Você pode usar algumas estratégias ao romper relações com o agente principal para introduzir a concorrência com segurança. Em alguns casos, você pode mover o código do agente principal para um chamador que sempre é executado no agente principal. Essa será uma boa estratégia se você quiser garantir que o trabalho aconteça de forma síncrona. Ou você pode usar await para acessar o agente principal no código concorrente de forma assíncrona.
Se o código não precisar estar no agente principal, adicione a palavra-chave não isolada para separá-lo de qualquer agente. Vamos explorar a primeira estratégia agora e falar sobre as outras mais tarde. Vou mover o cache de imagens para fetchAndDisplayImage, que é executada no agente principal. Verificar o cache antes de fazer qualquer chamada assíncrona é bom para eliminar a latência. Se a imagem estiver no cache, fetchAndDisplayImage será concluída de forma síncrona sem suspensão. Portanto, os resultados serão entregues à interface imediatamente, e ela só será suspensa se a imagem ainda não estiver disponível.
E podemos remover o parâmetro de URL de decodeImage porque não precisamos mais dele. Agora, só precisamos aguardar o resultado da decodeImage.
Uma função @concurrent sempre se desligará de um agente para ser executada. Se quiser que a função permaneça em qualquer agente em que foi chamada, use a palavra-chave não isolada. O Swift tem outras maneiras de introduzir mais concorrência. Saiba mais em “Beyond the basics of structured concurrency”.
Se estivermos fornecendo APIs de decodificação como parte de uma biblioteca para muitos clientes usarem, usar @concurrent nem sempre é a melhor escolha de API. O tempo para decodificar dados depende do tamanho, e pequenas quantidades podem ser processadas na thread principal. Para bibliotecas, é melhor fornecer uma API não isolada e permitir que os clientes decidam se querem descarregar o trabalho.
O código não isolado é muito flexível porque você pode chamá-lo de qualquer lugar: se você o chamar do agente principal, ele ficará no agente principal. Se você o chamar de uma thread em segundo plano, ele permanecerá em uma thread em segundo plano. Isso o torna um ótimo padrão para bibliotecas de uso geral. Ao descarregar o trabalho para o segundo plano, o sistema gerencia o agendamento do trabalho a ser executado em segundo plano. O pool de threads concorrentes contém as threads em segundo plano do sistema, que podem envolver qualquer número de threads. Para dispositivos menores, como um relógio, pode haver apenas uma ou duas threads no pool. Sistemas grandes com mais núcleos terão mais threads em segundo plano no pool. Não importa em qual thread em segundo plano uma tarefa é executada, e você pode confiar no sistema para fazer o melhor uso dos recursos. Por exemplo, quando uma tarefa é suspensa, a thread original começa a executar outras tarefas que estão prontas. Quando a tarefa é retomada, ela pode começar a ser executada em qualquer thread disponível no pool concorrente, que pode ser diferente da thread em segundo plano no qual foi iniciada.
Agora que temos concorrência, vamos compartilhar dados entre várias threads. O compartilhamento de estado mutável em um código concorrente é propenso a erros que causam falhas de tempo de execução difíceis de corrigir. O Swift ajuda a detectar esses erros em tempo de compilação para que você possa escrever códigos de concorrência com confiança. Cada vez que alternamos entre o agente principal e o pool concorrente, compartilhamos dados entre threads diferentes. Quando obtemos o URL da interface, ele é passado do agente principal para fora da thread de segundo plano para buscar a imagem. A busca da imagem retorna dados, que são passados para a decodificação da imagem. Depois de decodificarmos a imagem, ela é passada de volta para o agente principal, junto com self. O Swift garante acesso seguro a todos esses valores no código concorrente. Vamos ver o que acontecerá se a atualização da interface criar tarefas adicionais que envolvem o URL. Felizmente, o URL é um tipo de valor. Isso significa que, quando copiamos o URL para a thread em segundo plano, ele tem uma cópia separada daquela que está na thread principal. Se o usuário inserir um novo URL pela interface, o código na thread principal estará livre para usar ou modificar seu texto, e as alterações não terão efeito sobre o valor usado na thread em segundo plano. Portanto, é seguro compartilhar tipos de valor, como o URL, porque ele não está realmente compartilhando: cada cópia é independente das outras.
Os tipos de valor são uma grande parte do Swift desde o início. Todos os tipos básicos, como strings, inteiros e datas, são tipos de valor.
Coleções de tipos de valor, como dicionários e matrizes, também são tipos de valor. E também são structs e enums que armazenam tipos de valor, como este Post struct. Chamamos tipos cujo compartilhamento é sempre seguro concorrentemente de tipos Sendable. O Sendable é um protocolo, e qualquer tipo que esteja em conformidade com ele é seguro para compartilhar. Coleções como Array definem conformidades condicionais para Sendable, portanto, elas são Sendable quando seus elementos são. Structs e enums podem ser marcados como Sendable quando todos os seus dados de instância são Sendable. E os tipos de agentes principais são implicitamente Sendable, então você não precisa dizer isso explicitamente. Agentes como o principal protegem o estado não Sendable, garantindo que só seja acessado por uma tarefa por vez. Os agentes podem armazenar valores passados para seus métodos, e o agente pode retornar uma referência ao estado protegido a partir dos métodos. Sempre que um valor é enviado para dentro ou para fora de um agente, o compilador do Swift verifica se o valor é seguro para enviar ao código concorrente. Vamos nos concentrar na chamada assíncrona para decodeImage.
Decodificar imagem é um método de instância, então estamos passando um autoargumento implícito.
Aqui, vemos dois valores sendo enviados para fora do agente principal e um valor de resultado sendo enviado de volta para o agente principal. self é minha classe de modelo de imagem, que é o agente principal isolado. O agente principal protege o estado mutável, portanto, é seguro passar uma referência à classe para a thread em segundo plano. E Dados são um tipo de valor, portanto são Sendable.
Isso deixa o tipo de imagem. Poderia ser um tipo de valor, como Dados, caso em que seria Sendable. Em vez disso, vamos falar sobre tipos que não são Sendable, como classes. As classes são tipos de referência, ou seja, ao atribuir uma variável a outra, elas apontam para o mesmo objeto na memória. Se você alterar algo no objeto por meio de uma variável, como dimensionar a imagem, essas alterações serão imediatamente visíveis por meio das outras variáveis que apontam para o mesmo objeto. fetchAndDisplayImage não usa o valor de imagem concorrentemente. decodeImage é executada em segundo plano, portanto, não pode acessar nenhum estado protegido por um agente. Ela cria uma nova instância de uma imagem a partir dos dados fornecidos. Essa imagem não pode ser referenciada por nenhum código concorrente, portanto, é seguro enviá-la ao agente principal e exibi-la na interface. Vamos ver o que acontece quando introduzimos alguma concorrência. Primeiro, esse método scaleAndDisplay carrega uma nova imagem na thread principal. A variável image aponta para esse objeto image, que contém a imagem do gato. Em seguida, a função cria uma tarefa em execução no pool concorrente e que obtém uma cópia da imagem. Por fim, a thread principal passa a exibir a imagem. Agora, temos um problema. A thread em segundo plano está alterando a imagem: mudando a largura e a altura e substituindo os pixels pelos de uma versão dimensionada. Ao mesmo tempo, a thread principal está iterando sobre os pixels com base na largura e altura antigas. Isso é uma corrida de dados. Você pode acabar com um erro na interface ou, provavelmente, acabará com uma falha quando o programa tentar acessar fora dos limites da matriz de pixels. A concorrência do Swift evitará corridas de dados com erros do compilador se o código tentar compartilhar um tipo não Sendable. Aqui, o compilador indica que a tarefa concorrente está capturando a imagem, que também é usada pelo agente principal para exibir a imagem. Para corrigir isso, precisamos evitar compartilhar o mesmo objeto concorrentemente. Se quisermos que o efeito de imagem seja mostrado na interface, o correto é aguardar a conclusão do dimensionamento antes de exibir a imagem. Podemos mover todas essas três operações para a tarefa para garantir que elas aconteçam em ordem. displayImage tem que ser executada no agente principal, então usamos await para chamá-la de uma tarefa concorrente. Se pudermos tornar scaleAndDisplay assíncrona diretamente, poderemos simplificar o código para não criar uma nova tarefa e executar as três operações em ordem na tarefa que chamar scaleAndDisplay. Depois de enviar a imagem para o agente principal para exibição na interface, ele é livre para armazenar uma referência à imagem, por exemplo, armazenando em cache o objeto de imagem. Se alterarmos a imagem após exibi-la na interface, o compilador mostrará erro de acesso concorrente inseguro. Podemos resolver o problema fazendo alterações na imagem antes de enviá-la ao agente principal. Se você estiver usando classes para seu modelo de dados, elas provavelmente começarão no agente principal para que você possa apresentar partes delas na interface. Se você decidir que precisa trabalhar com elas em uma thread em segundo plano, torne-as não isoladas. Mas elas provavelmente não devem ser Sendable. Evite situações em que parte do modelo está sendo atualizada na thread principal e outras estão sendo atualizadas na thread em segundo plano. Manter as classes de modelo não Sendable impede que esse tipo de modificação concorrente ocorra. Também é mais simples porque tornar uma classe Sendable geralmente exige mecanismos de sincronização de baixo nível, como cadeado. Assim como as classes, os fechamentos podem criar um estado compartilhado. Confira esta função semelhante a outra que vimos que dimensiona e exibe uma imagem. Ela cria um objeto de imagem. Depois, ela chama perform(afterDelay:) com um fechamento que redimensiona a imagem. Esse fechamento contém outra referência à mesma imagem. Chamamos isso de captura da variável de imagem. Assim como as classes não Sendable, um fechamento com estado compartilhado ainda é seguro, desde que não seja chamado de maneira concorrente. Só torne um tipo de função Sendable se precisar compartilhá-la concorrentemente.
A verificação Sendable ocorre sempre que dados passam entre agentes e tarefas. Ela está lá para garantir que não haja corridas de dados que possam causar falhas no seu app. Muitos tipos comuns podem ser Sendable e compartilhados livremente em tarefas concorrentes. Classes e fechamentos podem envolver um estado mutável que não é seguro compartilhar concorrentemente, portanto, use-os de uma tarefa por vez.
Você ainda pode enviar um objeto de uma tarefa para outra, mas faça todas as modificações nele antes de enviá-lo. Mover tarefas assíncronas para threads em segundo plano pode liberar a thread principal para manter seu app responsivo. Se você achar que tem muitos dados no agente principal que estão fazendo com que essas tarefas assíncronas façam verificações na thread principal com frequência, convém introduzir agentes.
À medida que seu app cresce com o tempo, você pode descobrir que a quantidade de estado no agente principal também cresce. Você introduzirá novos subsistemas para lidar com coisas como o gerenciamento do acesso à rede. Isso pode causar muito estado vivendo no agente principal, por exemplo, o conjunto de conexões abertas manipuladas pelo gerenciador de rede, que acessaríamos sempre que precisássemos buscar dados pela rede. Quando começamos a usar esses subsistemas extras, a tarefa de buscar e exibir imagens anteriores ficou mais complicada: ela está tentando ser executada na thread em segundo plano, mas precisa pular para a thread principal porque é onde estão os dados do gerenciador de rede. Isso pode levar a uma contenção, em que muitas tarefas estão tentando executar códigos no agente principal ao mesmo tempo. As operações individuais podem ser rápidas, mas muitas tarefas fazendo isso podem travar a interface. Antes, retiramos o código da thread principal colocando-o em uma função @concurrent. Aqui, todo o trabalho está no acesso aos dados do gerenciador de rede. Para mudar isso, podemos introduzir nosso próprio agente gerente de rede. Assim como o agente principal, os agentes isolam os dados de modo que você só pode acessá-los quando está em execução nele. Junto com o agente principal, você pode definir seus próprios tipos de agente. Um tipo de agente é semelhante a uma classe de agente principal. Como uma classe de agente principal, ele isolará os dados para que apenas uma thread possa acessar os dados por vez. Um tipo de agente também é Sendable para que você possa compartilhar livremente objetos de agente. Ao contrário do agente principal, pode haver muitos objetos de agente em um programa, e todos são independentes. Além disso, os objetos do agente não estão ligados a uma única thread como o agente principal. Portanto, mover parte do estado do agente principal para um objeto de agente permite que mais código rode em segundo plano, mantendo a thread principal livre para a interface.
Use agentes quando achar que armazenar dados no agente principal está fazendo com que muito código seja executado na thread principal. Nesse ponto, separe os dados de uma parte não interface do seu código, como o código de gerenciamento de rede, em um novo agente.
Esteja ciente de que a maioria das classes no seu app provavelmente não devem ser agentes: As classes voltadas para a interface devem permanecer no agente principal para interagir diretamente com o estado da interface. As classes de modelo geralmente devem estar no agente principal com a interface ou mantidas como não Sendable para você não incentivar muitos acessos concorrentes ao seu modelo.
Nesta sessão, começamos com o código de thread única. À medida que nossas necessidades aumentavam, introduzimos tarefas assíncronas para ocultar a latência, código concorrente para ser executado em uma thread em segundo plano e agentes para retirar o acesso aos dados da thread principal. Com o tempo, muitos apps seguirão esse mesmo caminho.
Use ferramentas de criação de perfil para identificar quando e qual código retirar da thread principal. A concorrência do Swift ajudará você a separar o código da thread principal, melhorando o desempenho e a capacidade de resposta do seu app.
Temos alguns ajustes de compilação recomendados para seu app para ajudar na introdução da concorrência. O ajuste Approachable Concurrency ativa um pacote de recursos futuros que facilitam o trabalho com concorrência. Recomendamos que todos os projetos adotem esse ajuste. Para módulos do Swift que interagem principalmente com a interface, como o módulo principal do app, também recomendamos definir Default Actor Isolation como MainActor. Isso coloca o código no agente principal, a menos que você tenha dito o contrário. Esses ajustes funcionam juntos para facilitar a criação de apps em thread única e proporcionam um caminho mais acessível para introduzir a concorrência quando você precisar. A concorrência do Swift é uma ferramenta criada para ajudar a melhorar seu app. Use-a para introduzir código assíncrono ou concorrente quando encontrar problemas de desempenho no seu app. O guia de migração do Swift 6 responde a mais perguntas sobre concorrência e o caminho para a segurança na corrida de dados. E para ver como os conceitos desta sessão se aplicam em um exemplo de app, assista à nossa sessão prática complementar. Obrigado.
-
-
3:20 - Single-threaded program
var greeting = "Hello, World!" func readArguments() { } func greet() { print(greeting) } readArguments() greet()
-
4:13 - Data types in a the app
struct Image { } final class ImageModel { var imageCache: [URL: Image] = [:] } final class Library { static let shared: Library = Library() }
-
4:57 - Load and display a local image
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let data = try Data(contentsOf: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
5:36 - Fetch and display an image over the network
import Foundation struct Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let (data, _) = try URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
6:10 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
7:31 - Creating a task to perform asynchronous work
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() var url: URL = URL("https://swift.org")! func onTapEvent() { Task { do { try await fetchAndDisplayImage(url: url) } catch let error { displayError(error) } } } func displayError(_ error: any Error) { } func fetchAndDisplayImage(url: URL) async throws { } } final class Library { static let shared: Library = Library() }
-
9:15 - Ordered operations in a task
import Foundation class Image { func applyImageEffect() async { } } final class ImageModel { func displayImage(_ image: Image) { } func loadImage() async -> Image { Image() } func onButtonTap() { Task { let image = await loadImage() await image.applyImageEffect() displayImage(image) } } }
-
9:38 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } }
-
10:40 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data, at: url) view.displayImage(image) } func decodeImage(_ data: Data, at url: URL) -> Image { Image() } }
-
11:11 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
11:30 - Implementation of decodeImage
final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { if let image = cachedImage[url] { return image } // decode image let image = Image() cachedImage[url] = image return image } }
-
12:37 - Correct implementation of fetchAndDisplayImage with caching and concurrency
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> Image { // decode image Image() } }
-
13:30 - JSONDecoder API should be non isolated
// Foundation import Foundation nonisolated public class JSONDecoder { public func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T { fatalError("not implemented") } }
-
15:18 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
16:30 - Example of value types
// Value types are common in Swift import Foundation struct Post { var author: String var title: String var date: Date var categories: [String] }
-
16:56 - Sendable value types
import Foundation // Value types are Sendable extension URL: Sendable {} // Collections of Sendable elements extension Array: Sendable where Element: Sendable {} // Structs and enums with Sendable storage struct ImageRequest: Sendable { var url: URL } // Main-actor types are implicitly Sendable @MainActor class ImageModel {}
-
17:25 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await self.decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
18:34 - MyImage class with reference semantics
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } } let image = MyImage() let otherImage = image // refers to the same object as 'image' image.scale(by: 0.5) // also changes otherImage!
-
19:19 - Concurrently scaling while displaying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start func scaleAndDisplay(imageName: String) { let image = loadImage(imageName) Task { @concurrent in image.scaleImage(by: 0.5) } view.displayImage(image) } // Slide content end func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
20:38 - Scaling and then displaying an image eliminates the data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() func scaleAndDisplay(imageName: String) { Task { @concurrent in let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
20:54 - Scaling and then displaying an image within a concurrent asynchronous function
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
21:11 - Scaling, then displaying and concurrently modifying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) image.applyAnotherEffect() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
21:20 - Applying image transforms before sending to the main actor
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) image.applyAnotherEffect() await view.displayImage(image) } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
22:06 - Closures create shared state
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async throws { let image = loadImage(imageName) try await perform(afterDelay: 0.1) { image.scale(by: 0.5) } await view.displayImage(image) } nonisolated func perform(afterDelay delay: Double, body: () -> Void) async throws { try await Task.sleep(for: .seconds(delay)) body() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }pet.
-
23:47 - Network manager class
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } final class NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } }
-
25:10 - Network manager as an actor
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } actor NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } }
-
-
- 0:00 - Introdução
A concorrência do Swift permite que os apps executem várias tarefas de maneira simultânea, melhorando a capacidade de resposta e enviando a computação para o segundo plano. O modelo de concorrência do Swift facilita a escrita correta de código concorrente ao tornar explícita a introdução da concorrência, identificando os dados que são compartilhados entre tarefas concorrentes e identificando possíveis corridas de dados em tempo de compilação. Os apps começam executando todo o código no thread principal. Conforme a complexidade aumenta, tarefas assíncronas podem ser introduzidas para operações de alta latência, como acesso à rede. Os threads em segundo plano podem ser utilizados para tarefas que consomem mais computação. O Swift oferece várias ferramentas, como atores e tarefas, para expressar essas operações concorrentes.
- 3:17 - Código de thread única
No Swift, código de thread único é executado no thread principal, que fica isolado para o ator principal. Não há concorrência no ator principal porque há apenas um thread principal que pode executá-lo. Dados ou códigos podem ser especificados no ator principal usando a notação @MainActor. O Swift garantirá que o código do ator principal seja executado apenas no thread principal e que os dados sejam acessados somente a partir dele. Dizemos que tal código é isolado do ator principal. Por padrão, o Swift protege o código do thread principal usando o ator principal, garantindo acesso livre ao estado compartilhado. A proteção do código com o ator principal por padrão é orientada por um ajuste de compilação. Use isso principalmente para seu módulo de app principal e módulos focados em interações da UI.
- 6:00 - Tarefas assíncronas
A concorrência do Swift utiliza async e await para fazer com que as funções não sejam bloqueadoras, permitindo que outros trabalhos, como solicitações de rede, sejam executados enquanto os dados são aguardados. Isso impede travamentos e melhora a capacidade de resposta da UI, dividindo as funções em partes que são executadas antes e depois do evento esperado.
- 7:24 - Interleaving
As funções assíncronas são executadas em uma tarefa, independentemente de outras tarefas. Um único thread pode alternar entre a execução de tarefas independentes à medida que elas ficam prontas, usando “interleaving”. Isso melhora o desempenho, evitando o tempo ocioso e fazendo uso eficiente dos recursos do sistema. Várias tarefas assíncronas são eficazes ao executar várias operações independentes ao mesmo tempo. Ao executar os trabalhos em uma ordem específica, utilize uma única tarefa. Usar tarefas assíncronas em um único thread costuma ser suficiente. Se o thread principal ficar muito sobrecarregado, as ferramentas de criação de perfil, como o Instruments, podem ajudar a identificar gargalos para otimização antes de implementar a concorrência.
- 10:22 - Introdução à concorrência
A concorrência possibilita que partes do código sejam executadas em um thread em segundo plano paralelamente ao thread principal, acelerando o trabalho ao usar mais núcleos de CPU no seu sistema. Para melhorar o desempenho, o exemplo implementa a concorrência para executar código em threads em segundo plano, liberando o thread principal.
- 11:07 - Funções concorrentes
Ao aplicar o atributo @concurrent, o Swift é instruído a executar uma função em segundo plano. O compilador do Swift destaca o acesso aos dados no ator principal para implementar a concorrência de maneira segura. Uma prática recomendada para garantir que o trabalho ocorra de maneira síncrona é mover o código do ator principal para um chamador que sempre seja executado no thread principal.
- 13:10 - Código não isolado
Uma função @concurrent sempre se desligará de um ator para ser executada. A palavra-chave “nonisolated” possibilita que os clientes escolham onde executar o código: no thread principal ou em segundo plano. Para bibliotecas de uso geral, recomenda-se fornecer uma API não isolada e deixar que os clientes decidam se desejam descarregar o trabalho. Para mais opções de concorrência, consulte “Além dos fundamentos da concorrência estruturada” da WWDC23.
- 14:13 - Pool de threads concorrentes
Ao colocar o trabalho em segundo plano, o sistema gerencia o agendamento do trabalho em threads em um pool de threads concorrentes. Dispositivos menores terão menos threads no pool, enquanto sistemas maiores com mais núcleos terão mais. As tarefas são atribuídas aos threads disponíveis no pool, que podem mudar à medida que as tarefas são suspensas e retomadas, otimizando a utilização dos recursos.
- 14:58 - Compartilhar dados
Ao trabalhar com concorrência e compartilhar dados entre diferentes threads, existe o risco de introduzir bugs de tempo de execução devido ao acesso ao estado mutável compartilhado. O design do Swift ajuda a mitigar esse problema ao fornecer verificações em tempo de compilação, possibilitando que os desenvolvedores escrevam códigos concorrentes com mais confiança.
- 15:49 - Tipos de valor
O uso de tipos de valores oferece uma vantagem significativa ao lidar com tarefas concorrentes. Quando um tipo de valor é copiado para um thread em segundo plano, é criada uma cópia independente, garantindo que quaisquer alterações feitas no thread principal não afetem o valor do thread em segundo plano. Essa independência torna os tipos de valores seguros para compartilhamento entre threads. Os tipos de valores que estão em conformidade com o protocolo “Sendable” são sempre seguros para serem compartilhados de maneira concorrente. Os tipos de ator principal são implicitamente “Sendable”.
- 17:16 - Tipos isolados de ator
Os atores do Swift protegem o estado “não Sendable”, garantindo o acesso a uma única tarefa. Quando os valores são enviados de e para os atores, o compilador do Swift verifica a segurança.
- 18:30 - Classes
No Swift, as classes são tipos de referência, o que significa que as alterações em um objeto por meio de uma variável são visíveis em todas as variáveis que apontam para esse objeto. Quando vários threads acessam e modificam o mesmo objeto “não Sendable” de maneira concorrente, isso pode resultar em corridas de dados, travamentos ou falhas. O sistema de concorrência do Swift evita isso em tempo de compilação, reforçando que somente os tipos “Sendable” são compartilhados entre atores e tarefas. Para evitar corridas de dados, é fundamental garantir que os objetos mutáveis não sejam compartilhados de forma concorrente. Conclua as modificações nos objetos antes de enviá-los a outra tarefa ou ator para exibição ou processamento. Quando um objeto precisar ser modificado em um thread em segundo plano, torne-o “nonisolated”, mas não “Sendable”. Os fechamentos com estado compartilhado também podem ser seguros, desde que não sejam chamados de forma concorrente.
- 23:18 - Atores
À medida que um app cresce, o ator principal pode gerenciar muitos estados, o que leva à troca frequente de contexto. A implementação de atores pode reduzir isso. Os atores isolam seus dados, permitindo que um único thread os acesse por vez, evitando a contenção. Ao mover o estado do ator principal para atores dedicados, é possível executar mais código de forma concorrente em threads em segundo plano. Isso libera o thread principal a fim de manter a capacidade de resposta. As classes voltadas para a UI e as classes de modelos geralmente precisam permanecer no ator principal ou ser mantidas como “não Sendable”.
- 26:12 - Conclusão
Os apps geralmente começam com um único thread e evoluem para usar tarefas assíncronas, código concorrente e atores para melhorar o desempenho. A concorrência do Swift ajuda nessa transição, facilitando a transferência do código para fora do thread principal e melhorando a capacidade de resposta. As ferramentas de criação de perfil, como o Instruments, ajudam a identificar quando e qual código precisa ser transferido para fora do thread principal. Use as configurações de compilação recomendadas para ajudar a simplificar a implementação da concorrência e a configuração Approachable Concurrency para ativar um pacote de recursos futuros que vão facilitar o trabalho com a concorrência.