Как коллбеки позволяют достичь асинхронного выполнения кода?
В процессе изучения корутин в Котлин я узнал, что под капотом там работают коллбеки. Сперва я принял как должное, что коллбеки позволяют достичь асинхронности, но не понимаю, за счёт чего.
Например, сейчас я прохожу обучалку по корутинам от Jetbrains. Задача заключается в том, чтобы при помощи GitHub API выводить в окно UI список контрибьюторов репозитория. В этой обучалке последовательно усовершенствуется способ получения данных их GH.
В первом задании используем блокирующий вызов, из-за чего на время загрузки данных UI блокируется:
fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
val repos = service
.getOrgReposCall(req.org)
.execute() // Executes request and blocks the current thread
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributorsCall(req.org, repo.name)
.execute() // Executes request and blocks the current thread
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}
И затем вызываем эту блокирующую функцию:
val users = loadContributorsBlocking(service, req)
updateResults(users, startTime)
Во втором задании мы используем коллбеки и тогда UI не блокируется:
fun loadContributorsCallbacks(service: GitHubService, req: RequestData, updateResults: (List<User>) -> Unit) {
service.getOrgReposCall(req.org).onResponse { responseRepos ->
logRepos(req, responseRepos)
val repos = responseRepos.bodyList()
val allUsers = Collections.synchronizedList(mutableListOf<User>())
val countDownLatch = CountDownLatch(repos.size)
for (repo in repos) {
service.getRepoContributorsCall(req.org, repo.name).onResponse { responseUsers ->
logUsers(repo, responseUsers)
val users = responseUsers.bodyList()
allUsers += users
countDownLatch.countDown()
}
}
countDownLatch.await()
updateResults(allUsers.aggregate())
}
}
И вызываем эту функцию, передавая туда коллбек:
loadContributorsCallbacks(service, req) { users ->
SwingUtilities.invokeLater {
updateResults(users, startTime)
}
}
Что бы я ни читал про коллбеки и асинхронность, везде эта связь подается как само собой разумеющееся. Можете, пожалуйста, объяснить, каким образом передача функции А в качестве параметра функции Б и её выполнение после завершения функции Б обеспечивает, грубо говоря, разделение выполнения кода на два параллельных "потока", когда где-то фоном выполняется запрос, а мы в это время можем крутить списки, нажимать кнопки, писать и т.д.? Для меня это не очевидно и интуитивно кажется, что UI тоже должен заблокироваться, только в конце выполнения этой функции, типа мы просто выполняем тот же блокирующий вызов, только в конце функции. В чем секрет, если всё это делается в одном потоке?