Как коллбеки позволяют достичь асинхронного выполнения кода?

В процессе изучения корутин в Котлин я узнал, что под капотом там работают коллбеки. Сперва я принял как должное, что коллбеки позволяют достичь асинхронности, но не понимаю, за счёт чего.

Например, сейчас я прохожу обучалку по корутинам от 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 тоже должен заблокироваться, только в конце выполнения этой функции, типа мы просто выполняем тот же блокирующий вызов, только в конце функции. В чем секрет, если всё это делается в одном потоке?


Ответы (0 шт):