Programação Assíncrona com Coroutines & Channels

Natan Streppel
- Software Developer com 5 anos de experiência atualmente atuando com Go & Kotlin
- Atualmente trabalha na bornlogic em Curitiba (http://www.bornlogic.com)



Programação Assíncrona
Coroutines são uma ferramenta para programação assíncrona
Assincronia
=
eficiência
not exactly...
o mundo real da TI atualmente é mais concorrente que paralelo
Assincronismo
!=
Paralelismo
Apesar de conseguirmos atingir paralelismo com coroutines...
... esta não é sua principal finalidade
Concorrência: composição de processos (sent. figurado) independentes
Paralelismo: execução de processos
ao mesmo tempo
"[...] concurrency is the occurrence of events independent of the main program flow."

Coroutines
"lightweight threads"
*(kind of)
Threads podem custar 1MiB ~ 2MiB*
Coroutines usam algumas centenas de bytes**
Além disso, o modo de processamento é diferente


Coroutines: state machines
-
Dados do estado atual
-
Habilidade de "dormir"
-
Index do estado atual
Coroutines são, conceitualmente, pedaços de código que podem se suspender ("dormir") voluntariamente

standard library
kotlinx-coroutines
launch, async, runBlocking, future, delay, Job, Deferred...
Não estamos nos livrando de Threads...
... estamos distribuindo pequenos "objetos" para melhor aproveitar elas
Podemos resolver assincronia usando callbacks
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}mas se precisarmos alterar o fluxo...
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}fun postItem(item: Item) {
validateWithExternalService { item ->
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
}fun postItem(item: Item) {
validateWithExternalService { item ->
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
}- Callback hell; crescimento horizontal
- Bug-friendly
- Tende somente à crescer
- Não lê "naturalmente"
Ou, também, com Futures/Promises/Etc
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun postItem(item: Item) {
validateWithExternalService(item)
.thenCompose { item ->
preparePostAsync(item)
}
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}fun postItem(item: Item) {
validateWithExternalService(item)
.thenCompose { item ->
preparePostAsync(item)
}
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}- Ainda é um modo diferente de pensar
- .thenCompose() et al. podem mudar de library para library
- Não trabalhamos mais com o tipo desejado, mas sim "futures"
Ou, também, com coroutines
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
fun postItem(item: Item) {
launch {
val item = validateWithExternalService(item)
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
fun postItem(item: Item) {
launch {
val item = validateWithExternalService(item)
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
- Lê de modo sequencial e natural
- error handling limpo e models se mantém intactos
- Os tipos se mantém; preparePost() retorna um token e não uma promise
Coroutines

Problema: fetches no Github
Temos um endpoint que retorna os repositórios de uma organization
https://api.github.com/orgs/{org}/repos
o retorno é um json paginado com infos dos repositórios da org
[
{
"id": 8856204,
"node_id": "MDEwOlJlcG9zaXRvcnk4ODU2MjA0",
"name": "kotlin-examples",
"full_name": "Kotlin/kotlin-examples",
"private": false,
"owner": {
"login": "Kotlin",
"id": 1446536,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE0NDY1MzY=",
"avatar_url": "https://avatars3.githubusercontent.com/u/1446536?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Kotlin",
"html_url": "https://github.com/Kotlin",
"followers_url": "https://api.github.com/users/Kotlin/followers",
"following_url": "https://api.github.com/users/Kotlin/following{/other_user}",
"gists_url": "https://api.github.com/users/Kotlin/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Kotlin/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Kotlin/subscriptions",
"organizations_url": "https://api.github.com/users/Kotlin/orgs",
"repos_url": "https://api.github.com/users/Kotlin/repos",
"events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
"received_events_url": "https://api.github.com/users/Kotlin/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Kotlin/kotlin-examples",
"description": "Various examples for Kotlin",
"fork": false,
"url": "https://api.github.com/repos/Kotlin/kotlin-examples",
"forks_url": "https://api.github.com/repos/Kotlin/kotlin-examples/forks",
"keys_url": "https://api.github.com/repos/Kotlin/kotlin-examples/keys{/key_id}",
"repos_url": "https://api.github.com/users/Kotlin/repos",
"events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
"received_events_url": "https://api.github.com/users/Kotlin/received_events",[
{
"id": 8856204,
"node_id": "MDEwOlJlcG9zaXRvcnk4ODU2MjA0",
"name": "kotlin-examples",
"full_name": "Kotlin/kotlin-examples",
"private": false,
"owner": {
"login": "Kotlin",
"id": 1446536,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE0NDY1MzY=",
"avatar_url": "https://avatars3.githubusercontent.com/u/1446536?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Kotlin",
"html_url": "https://github.com/Kotlin",
"followers_url": "https://api.github.com/users/Kotlin/followers",
"following_url": "https://api.github.com/users/Kotlin/following{/other_user}",
"gists_url": "https://api.github.com/users/Kotlin/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Kotlin/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Kotlin/subscriptions",
"organizations_url": "https://api.github.com/users/Kotlin/orgs",
"repos_url": "https://api.github.com/users/Kotlin/repos",
"events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
"received_events_url": "https://api.github.com/users/Kotlin/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Kotlin/kotlin-examples",
"description": "Various examples for Kotlin",
"fork": false,
"url": "https://api.github.com/repos/Kotlin/kotlin-examples",
"forks_url": "https://api.github.com/repos/Kotlin/kotlin-examples/forks",
"keys_url": "https://api.github.com/repos/Kotlin/kotlin-examples/keys{/key_id}",
"repos_url": "https://api.github.com/users/Kotlin/repos",
"events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
"received_events_url": "https://api.github.com/users/Kotlin/received_events",Para cada repo, vamos pegar os contribuintes...
... e somar suas contribuições para gerar um relatório no final
PRA DEMITIR QUEM COMMITOU MENOS DE 50 VEZES SEMANA PASSADA
[
{
"login": "jonnyzzz",
"id": 256431,
"node_id": "MDQ6VXNlcjI1NjQzMQ==",
"avatar_url": "https://avatars3.githubusercontent.com/u/256431?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/jonnyzzz",
"html_url": "https://github.com/jonnyzzz",
"followers_url": "https://api.github.com/users/jonnyzzz/followers",
"following_url": "https://api.github.com/users/jonnyzzz/following{/other_user}",
"gists_url": "https://api.github.com/users/jonnyzzz/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jonnyzzz/starred{/owner}{/repo}",
"repos_url": "https://api.github.com/users/jonnyzzz/repos",
"events_url": "https://api.github.com/users/jonnyzzz/events{/privacy}",
"received_events_url": "https://api.github.com/users/jonnyzzz/received_events",
"type": "User",
"site_admin": false,
"contributions": 54
},
{
"login": "yanex",
"id": 95996,
"node_id": "MDQ6VXNlcjk1OTk2",
"avatar_url": "https://avatars2.githubusercontent.com/u/95996?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/yanex",
"html_url": "https://github.com/yanex",
"followers_url": "https://api.github.com/users/yanex/followers",
"following_url": "https://api.github.com/users/yanex/following{/other_user}",
"gists_url": "https://api.github.com/users/yanex/gists{/gist_id}",
"starred_url": "https://api.github.com/users/yanex/starred{/owner}{/repo}",
"repos_url": "https://api.github.com/users/yanex/repos",
"events_url": "https://api.github.com/users/yanex/events{/privacy}",
"received_events_url": "https://api.github.com/users/yanex/received_events",
"type": "User",
"site_admin": false,
"contributions": 47
},
{
"login": "yole",
"id": 46553,Solução inicial: síncrona
(utilizaremos o HTTP client retrofit)
Interface HTTP
interface GitHubService {
@GET("orgs/{org}/repos?per_page=100")
fun getOrgReposCall(
@Path("org") org: String
): Call<List<Repo>>
@GET("repos/{org}/{repo}/contributors?per_page=100")
fun getRepoContributorsCall(
@Path("org") org: String,
@Path("repo") repo: String
): Call<List<User>>
}E resolvemos as chamadas assim
val users = loadContributorsBlocking(service, req)
updateResults(users)Onde loadContributorsBlocking() é....
fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
val repos: list<Repo> = service
.getOrgReposCall(req.org)
.execute() // blocks current thread
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributorsCall(req.org, repo.name)
.execute() // blocks current thread
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}Tempo com 46 requests:
~14 sec
Podemos melhorar?
Podemos tentar fazer as outras 46 requisições concorrentemente
Então vamos suspender essas chamadas enquanto não retornam!
Mas como um código pode ser suspenso?
keyword suspend
!!!
keyword suspend aplicada antes de uma função
interface GitHubService {
@GET("orgs/{org}/repos?per_page=100")
fun getOrgReposCall(
@Path("org") org: String
): Call<List<Repo>>
@GET("repos/{org}/{repo}/contributors?per_page=100")
fun getRepoContributorsCall(
@Path("org") org: String,
@Path("repo") repo: String
): Call<List<User>>
}interface GitHubService {
@GET("orgs/{org}/repos?per_page=100")
suspend fun getOrgReposCall(
@Path("org") org: String
): Call<List<Repo>>
@GET("repos/{org}/{repo}/contributors?per_page=100")
suspend fun getRepoContributorsCall(
@Path("org") org: String,
@Path("repo") repo: String
): Call<List<User>>
}fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}e vamos chamar loadContributorsSuspend de uma coroutine agora
Mas como de fato construímos uma coroutine?
Coroutine Builders

Por default, temos 3 builders disponíveis
- Launch - fire and forget
- Async - fire mas quero saber o retorno (utilizando deferreds ("promises"))
- runBlocking - fire e tranca a thread em que está rodando até todas as coroutines "filhas" terminarem
Sempre que tivermos um código que pode ser suspenso, podemos usar um desses builders pra lançar!
Subtitle
val users = loadContributorsSuspend(service, req)
updateResults(users)launch {
val users = loadContributorsSuspend(service, req)
updateResults(users, startTime)
}agora vai que vai segura peão

Tempo com 46 requests e suspend functions:
~13 sec
why

Nós criamos suspending functions
E chamamos elas de uma coroutine diferente
Mas as chamadas HTTP não foram feitas de modo concorrente
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
}o objeto deferred tem o método await()
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
}uma list de deferreds tem o método awaitAll()
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
deferreds.awaitAll()
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
deferreds.awaitAll().flatten().aggregate()
}suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
// deferreds é uma List<Deferred<List<User>>>
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
return deferreds.awaitAll().flatten().aggregate()
}E agora?
Tempo com 46 requests e suspend functions assíncronas:
~5 sec
Uma melhora de ~65% no tempo de execução!
Estamos lidando com conexões HTTP pela internet (com a wifi da minha casa)
Em uma rede cloud gigabit ethernet a diferença pode (provavelmente vai) ser ainda maior
Show!
Mas podemos melhorar um pouco mais ainda...
a tela ainda fica vazia enquanto atualiza
poderíamos atualizar a GUI com os retornos on the fly, né?
Temos coroutines executando ao mesmo tempo da GUI
E a GUI pode rodar (e provavelmente roda) em outra thread
Como podemos comunicar o resultado das coroutines para a GUI?
Comunicação por channels ✨
Channels são canais de comunicação entre coroutines

Podem receber e enviar entre diferentes produces e consumers

Como podemos implementar channels aqui?
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
return deferreds.awaitAll().flatten().aggregate()
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData): List<User> {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
/*val deferreds = repos.map { repo ->
async {
service
.getRepoContributors(req.org, repo.name)
.also { logUsers(repo, it) }
.bodyList()
}
}
return deferreds.awaitAll().flatten().aggregate()*/
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) {
launch {
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users)
}
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive()
allUsers = (allUsers + users).aggregate()
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive() // pra cada mensagem recebida
allUsers = (allUsers + users).aggregate() // somamos os resultados
// mas como atualizamos a GUI??
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive() // pra cada mensagem recebida
allUsers = (allUsers + users).aggregate() // somamos os resultados
// vamos chamar a função de atualização aqui, recebendo ela como parametro nessa função aqui!
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive()
allUsers = (allUsers + users).aggregate()
//TODO: update GUI
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData, updateResults: suspend (List<User>, completed: Boolean)) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive()
allUsers = (allUsers + users).aggregate()
//TODO: update GUI
}
}suspend fun loadContributorsChannels(service: GitHubService, req: RequestData, updateResults: suspend (List<User>, completed: Boolean)) {
val repos = service
.getOrgRepos(req.org)
.also { logRepos(req, it) }
.body() ?: listOf()
val channel = Channel<List<User>>()
for (repo in repos) { // pra cada repo
launch { // lançamos uma coroutine nova
val users = service
.getRepoContributors(req.org, repo.name)
.bodyList()
channel.send(users) // e enviamos o result aqui
}
}
var allUsers = emptyList<User>()
repeat(repos.size) {
val users = channel.receive()
allUsers = (allUsers + users).aggregate()
updateResults(allUsers, it == repos.lastIndex)
}
}launch {
val users = loadContributorsChannels(service, req)
updateResults(users, startTime)
}launch {
val users = loadContributorsChannels(service, req) { users, completed ->
updateResults(users, completed)
}
}launch {
loadContributorsChannels(service, req) { users, completed ->
updateResults(users, completed)
}
}
Essa talk foi inspirada na página Introduction to Coroutines & Channels
Coroutines & Channels (K/E)
By Natan Streppel
Coroutines & Channels (K/E)
presentation for the Kotlin Everywhere event based in Curitiba (2019)
- 296