В предыдущей статье я сделал беглый обзор async-await в Android. Теперь пришло время погрузиться немного глубже в грядущий функционал kotlin версии 1.1.


Для чего вообще async-await?


Когда мы сталкиваемся с длительными операциями, такими как сетевые запросы или транзакции в базу данных, то надо быть уверенным, что запуск происходит в фоновом потоке. Если же забыть об этом, то можно получить блокировку UI потока еще до того, как задача закончится. А во время блокировки UI пользователь не сможет взаимодействовать с приложением.


К сожалению, когда мы запускаем задачу в фоне, то не можем использовать результат тут же. Для этого нам потребуется некая разновидность callback'а. Когда callback будет вызван с результатом, только тогда мы сможем продолжить, например запустить еще один сетевой запрос.


Простой пример того, как люди приходят к "callback hell": несколько вложенных callback'ов, все ждут вызова когда длогоиграющая операция закончится.


fun retrieveIssues() {
    githubApi.retrieveUser() { user ->
        githubApi.repositoriesFor(user) { repositories ->
            githubApi.issueFor(repositories.first()) { issues ->
                handler.post { 
                    textView.text = "You have issues!" 
                }
            }
        }
    }
}

Этот кусок кода представляет три сетевых запроса, где в конце отправляется сообщение в главный поток, чтобы обновить некий TextView.


Исправляем с помощью async-await


С помощью async-await можно привести этот код к более императивному стилю с той же функциональностью. Вместо отправки callback'а можно вызвать "замораживающий" метод await, который позволит использовать результат так же, словно он был вычислен в синхронном коде:


fun retrieveIssues() = asyncUI {
    val user = await(githubApi.retrieveUser())
    val repositories = await(githubApi.repositoriesFor(user))
    val issues = await(githubApi.issueFor(repositories.first()))
    textView.text = "You have issues!"
}

Этот код все еще делает три сетевых запроса и обновляет TextView в главном потоке, и не блокирует UI!


Погоди… Что?


Если мы будет использовать библиотеку AsyncAwait-Android, то получим несколько методов, два из которых async и await.


Метод async позволяет использовать await и изменяет способ получения результата. При входе в метод, каждая строка будет выполнена синхронно пока не достигнет точки "заморозки"(вызова метода await). По факту, это все, что делает async — позволяет не перемещать код в фоновый поток.


Метод await позволяет делать вещи асинхронно. Он принимает "awaitable" в качестве параметра, где "awaitable" — какая-то асинхронная операция. Когда вызывается await, он регистрируется в "awaitable", чтобы получить уведомление, когда операция закончится, и вернуть результат в метод asyncUI. Когда "awaitable" завершится, он выполнит оставшуюся часть метода, при этом передав туда результат.


Магия


Все это похоже на магию, но тут нет никакого волшебства. На самом деле компилятор котлина трансформирует coroutine (то, что находится в рамках async) в стейт-машину(конечный автомат). Каждое состояние которого — это часть кода из coroutine, где точка "заморозки"(вызов await) означает конец состояния. Когда код, переданный в await, завершается, выполнение переходит к следующему состоянию, и так далее.


Рассмотрим простую версию кода, представленного ранее. Мы можем посмотреть, какие создаются состояния, для этого отметим каждый вызов await:


fun retrieveIssues() = async {
    println("Retrieving user")
    val user = await(githubApi.retrieveUser()) 
    println("$user retrieved")
    val repositories = await(githubApi.repositoriesFor(user))
    println("${repositories.size} repositories")
}

Эта coroutin'a имеет три состояния:


  • Начальное состояние, до вызова await
  • После первого вызова await
  • После воторого вызова await

Этот код будет скомпилирован в такую стейт-машин(псевдо-байт-код):


class <anonymous_for_state_machine> {
    // The current state of the machine
    int label = 0

    // Local variables for the coroutine
    User user = null
    List<Repository> repositories = null

    void resume (Object data) {
        if (label == 0) goto L0
        if (label == 1) goto L1
        if (label == 2) goto L2

        L0:
          println("Retrieving user")

          // Prepare for await call
          label = 1
          await(githubApi.retrieveUser(), this) 
             // 'this' is passed as a continuation
          return

        L1:
          user = (User) data
          println("$user retrieved")  

          label = 2
          await(githubApi.repositoriesFor(user), this)
          return 

        L2:
          repositories = (List<Repository>) data
          println("${repositories.size} repositories")     

          label = -1
          return
    }
}

После захода в стейт-машину будут выполнены label ==0 и первый блок кода. Когда будет достигнут await, label обновится, и стейт-машина перейдет к выполнению кода, переданного в await. После этого выполнение продолжится с точки resume.


После завершения задачи, отправленной в await, будет вызван метод стейт-машины resume(data) для выполнения следующй части. И так будет продолжаться, пока не будет достигнуто последнее состояние.


Обработка исключений


В случае завершения "awaitable" с ошибкой, стейт-машина получит уведомление об этом. На самом деле метод resume принимает дополнительный Throwable параметр, и, когда выполняется новое состояние, этот параметр проверяется на равенство null. Если параметр null, то Throwable пробрасывается наружу.


Поэтому можно использовать оператор try/catch как обычно:


fun foo() = async {
    try {
        await(doSomething())
        await(doSomethingThatThrows())
    } catch(t: Throwable) {
        t.printStackTrace()
    }
}

Многопоточность


Метод await не гарантирует запуск awaitable в фоновом потоке, а просто регистрирует слушателя, которые реагирует на завершение awaitable. Поэтому awaitable должен сам заботиться о том, в каком потоке запускать выполнение.


Например, мы отправили retrofit.Call<Т> в await, вызовем метод enqueue() и зарегистрируем слушателя. Retrofit сам позаботится, чтобы сетевой запрос был запущен в фоновом потоке.


suspend fun <R> await(
    call: Call<R>,
    machine: Continuation<Response<R>>
) {
    call.enqueue(
          { response ->
              machine.resume(response)
          },
          { throwable ->
              machine.resumeWithException(throwable)
          }
    )
}

Для удобства существует один вариант метода await, который принимает функцию () –> R и запускает её в другом потоке:


fun foo() = async<String> {
   await { "Hello, world!" }
}

async, async<Т> и asyncUI


Существует три варианта метода async


  • async: ничего не возвращает (как Unit или void)
  • async<Т>: возвращает значение типа T
  • asyncUI: ничего не возвращает

При использовании async<Т>, необходимо вернуть значение типа T. Сам же метод async<Т> возвращает значение типа Task<Т>, которое, как вы наверно догадались, можно отправить в метод await:


fun foo() = async {
   val text = await(bar())
   println(text)
}
fun bar() = async<String> {
   "Hello world!"
}

Более того, метод asyncUI гарантирует, что продолжение(код между await) будет происходит в главном потоке. Если же использовать async или async<Т>, то продолжение будет происходить в том же потоке, в котором был вызван callback:


fun foo() = async {
  // Runs on calling thread
  await(someIoTask()) // someIoTask() runs on an io thread
  // Continues on the io thread
}
fun bar() = asyncUI {
    // Runs on main thread
    await(someIoTask()) // someIoTask() runs on an io thread
    // Continues on the main thread
}

В заключении


Как вы могли заметить, coroutin'ы предоставляют интересные возможности и могут улучшить читаемость кода, если ими пользоваться правильно. Сейчас они доступны в kotlin версии 1.1-M02, а возможности async-await, описанные в этой стате, вы можете использовать с помощью моей библиотеки на github.

Поделиться с друзьями
-->

Комментарии (11)


  1. x2bool
    09.11.2016 15:26

    В Kotlin очень чистая реализация async/await, смотрится намного приятнее чем тот же TPL в C#. И для платформы Android это еще одна киллер-фича.


    1. smaugfm
      09.11.2016 18:20
      +1

      Можете поподробнее обьяснить чистоту реализации в Kotlin в отличии от TPL?


      1. QtRoS
        09.11.2016 20:03

        Я полагаю, что x2bool имел ввиду визуальную простоту — в шарпе все выглядит гораздо менее понятным с первого взгляда. Хотя пример в статье может быть слишком упрощенным.


        1. Azoh
          09.11.2016 21:03
          +2

          В чем принципиальное отличие?


          fun retrieveIssues() = async {
              println("Retrieving user")
              val user = await(githubApi.retrieveUser()) 
              println("$user retrieved")
              val repositories = await(githubApi.repositoriesFor(user))
              println("${repositories.size} repositories")
          }
          

          async void RetrieveIssues()
          {
              Console.WriteLine("Retrieving user");
              var user = await githubApi.retrieveUser();
              Console.WriteLine($"{user} retrieved");
              var repositories = await githubApi.repositoriesFor(user);
              Console.WriteLine($"{repositories.size} repositories");
          }


          1. QtRoS
            09.11.2016 21:30
            -1

            Я подумал про разницу этого:

            [AsyncStateMachine(typeof(Program.<MethodTaskAsync>d__0))]
            		public Task<int> MethodTaskAsync()
            		{
            			Program.<MethodTaskAsync>d__0 <MethodTaskAsync>d__ = new Program.<MethodTaskAsync>d__0();
            			<MethodTaskAsync>d__.<>4__this = this;
            			<MethodTaskAsync>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
            			<MethodTask>d__.<>1__state = -1;
            			AsyncTaskMethodBuilder<int> <>t__builder = <MethodTaskAsync>d__.<>t__builder;
            			<>t__builder.Start<Program.<MethodTaskAsync>d__0>(ref <MethodTaskAsync>d__);
            			return <MethodTaskAsync>d__.<>t__builder.Task;
            		}
            

            и
            class <anonymous_for_state_machine> {
                // The current state of the machine
                int label = 0
            
                // Local variables for the coroutine
                User user = null
                List<Repository> repositories = null
            
                void resume (Object data) {
                    if (label == 0) goto L0
                    if (label == 1) goto L1
                    if (label == 2) goto L2
            
                    L0:
                      println("Retrieving user")
            
                      // Prepare for await call
                      label = 1
                      await(githubApi.retrieveUser(), this) 
                         // 'this' is passed as a continuation
                      return
            
                    L1:
                      user = (User) data
                      println("$user retrieved")  
            
                      label = 2
                      await(githubApi.repositoriesFor(user), this)
                      return 
            
                    L2:
                      repositories = (List<Repository>) data
                      println("${repositories.size} repositories")     
            
                      label = -1
                      return
                }
            }
            


      1. x2bool
        09.11.2016 21:30

        Реализация не завязана на конкретный тип, такой, как Task. Ну, и то, что async/await это не специальные ключевые слова, а всего лишь функции.


        1. creker
          09.11.2016 22:43
          +1

          Конкретный тип это как раз хорошо, потому что TPL появилось раньше и из async/await получился мощный инструмент, который базируется уже и так на мощном знакомом API, а не добавлено вообще сбоку непонятно к чему. При желании можно расширять поведение async/await и строить его на своих примитивах как раз благодаря тому, что он написан на том же .Net.

          А ключевые слова это вкусовщина.


    1. synmcj
      09.11.2016 19:39
      +2

      Поздравляю с киллер-фичей. Глядишь, через пару лет под Android станет наконец-то приятно писать.


      1. andreich
        09.11.2016 20:54

        да и сейчас вроде ничего так


  1. anton9088
    13.11.2016 20:10
    +1

    Очередное решение, которое не поддерживает жизненный цикл activity и fragment? И опять нужно будет делать костыли, как в rx, чтобы это хоть как-то работало?


    1. x2bool
      16.11.2016 18:04

      А как оно должно знать о жизненном цикле, если это фича языка, а не интеграции с Android?