Danilo Lira

Mobile & Web Software Engineer

Swift 5.5 — Concurrency e Async/Await

Na WWDC deste ano, a Apple mostrou as atualizações do Swift, dentre elas as que me chamaram mais a atenção foram as relacionadas a programação concorrente e a adição do suporte ao async e await. Essas atualizações melhoraram bastante a performance do código e sua legibilidade, já que agora podemos dispensar aquele pirâmide de completion handlers que tornava o código desnecessariamente extenso e bastante complexo.

Caso você não tenha entendido nada que acabei de falar, não se preocupe, pois neste artigo quero mostrar o que é a programação assíncrona, o que é programação concorrente e comparar como era a utilização desses conceitos antes e depois da nova versão do Swift.

Mas e o que é programação assíncrona?

Síncrono ou assíncrono diz respeito ao fluxo de execução de um programa. Quando uma operação executa completamente antes de passar o controle à seguinte, a execução é síncrona. Caso a operação seja executada parcialmente e passe o controle à seguinte ela é considerada assíncrona.

Utilizamos do recursos do assincronismo, quando precisamos esperar pela resposta de uma chamada de função. Com programação síncrona, enquanto esperamos essa resposta iremos bloquear uma thread, ou seja, nossa aplicação fica ocupada esperando, já com uma chamada assíncrona, conseguimos liberar a thread para realizar outras ações enquanto aguardamos um resultado.

Ilustração do tempo de execução de uma função síncrona e assíncrona

Na ilustração acima, cada cor indica um processo sendo realizado, e a partir dela conseguimos ver como o tempo de execução da aplicação fica pouco eficiente nas chamadas síncronas. Também é possível perceber porque há um bloqueio de thread, pois a aplicação fica aguardando uma função acabar completamente para só assim chamar a próxima.

Imagina se cada vez que você rolasse a página do Instagram, enquanto ela estivesse baixando as novas publicações todo o aplicativo travasse, seria uma experiência horrorosa que não queremos para os nossos usuários. Isso aconteceria caso o download fosse executado de forma síncrona, o aplicativo todo estaria esperando pela resposta daquele download e não conseguiríamos realizar nenhuma ação até essa execução ser concluída.

Ok e concurrency, o que é?

Concurrency é a mistura de código assíncrono com programação paralela, que é forma de computação em que vários cálculos são realizados ao mesmo tempo. A programação concorrente, ou concurrency, é a habilidade de executar diferentes partes de um programa ao mesmo tempo e sem uma ordem estrita, de forma que isso não afete o resultado final.

A ilustração acima mostra a execução de duas funções ao mesmo tempo, a função 1 e 2, veja que há momentos em que a primeira função está sendo executada e há momento que a segunda está sendo executada e essa alternância ocorre mesmo sem as funções terem finalizado sua execução.

O que acontece aqui é que há momentos na execução dessas funções onde elas precisaram executar tarefas que podem ser demoradas como uma requisição de API por exemplo, e para que haja uma otimização dos recursos, a função libera a thread para outras funções serem executadas, enquanto ela aguarda a resposta da requisição.

Então quando vemos esses momentos de troca entre uma função e outra, em Swift há a utilização da palavra reservada await. O await serve exatamente para indicar ao código de que aquela operação pode demorar e a thread pode ser liberada enquanto isso.

Para que esse processo ocorra com sucesso nós precisaremos utilizar threads. As threads permitem que operações sejam executadas em paralelo, ou seja, de forma que consigamos fazer uma chamada de API e continuar mexendo na UI do aplicativo sem problemas. Um ponto positivo é que em Swift não precisamos nos preocupar com o gerenciamento de threads, já que isso é feito pela própria linguagem, mas é importante saber o que ocorre por trás.

Entendi, mas como isso faço isso em Swift?

Até o Swift 5.4 os completions handlers eram bastante utilizados, mas o que é isso?

O completion handler é uma closure que é utilizada quando precisamos ser notificados de que uma tarefa foi concluída, geralmente usamos funções que apenas retornam um valor, mas isso é útil apenas quando estamos lidando com funções síncronas. Porém quando estamos lidando com funções assíncronas, é necessário que um código seja executado apenas quando aquela função terminar sua execução e para isso passamos o completion handler.

Imagine que seu aplicativo precise baixar imagens de carros da internet e depois exibi-lás em uma tabela, sem o completion handler, você irá travar sua aplicação até que o download seja concluído para que a exibição das imagens seja feita. Com essa técnica você pode passar a função de exibir as imagens como um completion handler e chamá-la apenas quando o download estiver finalizado, assim você garante que o código continue sendo executado e que a alteração será executada na hora certa.

O exemplo abaixo mostra uma função utilizada para fazer o download de imagens a partir da API do Pixabay, nela eu quero executar um código depois que o download, então eu chamo o completion handler, após todas as operações serem realizadas

/media/ef806a70e7dab3685cfc21d72c805737

A função getData possui um parâmetro: nosso completion handler. Este tem como seu tipo uma função que recebe uma Response (classe que contém dados da requisição) e retorna vazio (Void), além disso há a palavra reservada escaping que define que este completion handler pode ser executado fora do escopo da função getData.

Neste pequeno exemplo vemos que há dois completion handlers sendo utilizados, o segundo sendo passando como parâmetro da função dataTask, essa cadeia de funções torna o código menos legível.

Também temos outro problema, pois com este método não conseguimos lidar com os erros da mesma forma que lidamos nas funções síncronas, pois precisamos lançar esse erro para o completion handler e isso é muito suscetível a falhas, como esquecer de chamar a função, por exemplo.

E como está agora?

Com a adição do async/await, as funções se tornaram muito mais legíveis e simples. Não precisamos mais utilizar o completion handler, já que ele servia para ser executado após uma função assíncrona finalizar seu trabalho.

porém ao utilizar a palavra reservada await antes da chamada de uma função, estamos deixando claro para o Swift que aquele bloco de código será executado assincronamente e que utilizaremos o seu resultado quando seu processamento for finalizado.

Uma função assíncrona é um tipo especial de função, ela diferente das outras pode ser pausada durante a execução para esperar o fim de alguma operação. Ao criar uma função podemos indicar que ela é assíncrona utilizando a palavra reservada async logo após seus parametros e dentro da função você vai indicar onde ela poderá ser pausada com a palavra reservada await.

Quando a execução chegar nesta parte do código ela irá mandar executar essa requisição em uma outra thread e a execução de outras partes do código continuará até que aquele valor requisitado seja necessário.

Sabendo disso, podemos criar uma versão atualizada da função anterior.

/media/a42d61c33e6ea45efd4bcd94de6b3cd9

O código acima tem a mesma função do código mostrado anteriormente, porém agora, utilizando o async/await, vemos que houve uma redução significativa da quantidade de linhas. O código também fica muito mais claro onde chamadas assíncronas estão sendo realizadas e lidar com o lançamento de erros, com esta atualização, será bem mais simples.

Conclusão

A adição dessas novas features facilita bastante a vida dos desenvolvedores na utilização de código assíncrono. Se você quiser se aprofundar mais no assunto e entender em detalhes vou deixar algumas referências.

Se tiver alguma dúvida ou observação, deixa ai nos comentários 😁