В этом уроке разберем, что такое Continuation и как suspend функция приостанавливает код, не блокируя поток.

 

В прошлом уроке мы выяснили что Continuation помогает suspend функциям приостанавливать код, но не блокировать поток. Давайте разберем, как именно это происходит.

  • Подробно

    Continuation

     

    В качестве примера используем корутину:

    launch {
        val url = buildUrl()
    
        download(url) //suspend function
    
        toast("File is downloaded: " + url)
    }

    download - это suspend функция, которая загружает файл. Позже мы разберем, как создавать такие функции. 

    Пока же я напомню, что suspend функция download не заблокирует поток, в котором она будет вызвана. Потому что свою работу она будет выполнять в отдельном потоке. Но при этом она приостановит выполнение кода. И функция toast будет вызвана только после того, как download загрузит файл.

    Сейчас мы подробно рассмотрим, как такое возможно. 

     

    Как вы знаете, Kotlin код будет при компиляции преобразован в Java классы. И корутина - не исключение. Более того, для нее будет создан отдельный класс. Именно в этом классе и содержится механизм приостановки корутины suspend функцией. 

    Т.е. то, что в Kotlin выглядит как магия корутин и suspend функций, под капотом (в Java) реализовано обычным кодом в обычном классе. Но разобраться в коде этого класса - непросто. Поэтому мы начнем с очень упрощенной версии, из которой я убрал почти всю логику. По мере объяснения, я буду логику возвращать, получая код все более похожий на оригинал. 

     

    Итак, в результате преобразования кода корутины в Java будет создан Continuation класс. Его максимально упрощенная версия выглядит так:

    class GeneratedContinuationClass extends SuspendLambda {
        String url;
    
        @Override
        void invokeSuspend() {
            url = buildUrl();
    
            download(url); // suspend function
    
            toast("File is downloaded: " + url);
        }
    }

    В метод invokeSuspend переехал весь код, который корутина должна выполнить. Этот метод будет вызван при старте корутины. 

    Напомню, что мы обсуждаем, как магия Kotlin реализована под капотом, т.е. - Java код. Здесь уже нет никакой корутины или suspend функции. Они превратились в метод invokeSuspend и асинхронную функцию download (я продолжу называть ее suspend).

    Основная задача Continuation - сделать так, чтобы toast был выполнен только после того, как функция download в фоновом потоке загрузит файл. Обычно в Java в таких случаях используется колбэк, но Continuation идет другим путем. Код в методе invokeSuspend делится свитчем на две части. И добавляется переменная label.

    int label;
    
    void invokeSuspend() {
        switch (label) {
            case 0: {
                url = buildUrl();
    
                download(url); // suspend function
                return;
            }
            case 1: {
                toast("File is downloaded: " + url);
                return;
            }
        }
    }

    Точка разделения кода на две части - это suspend функция. Она и весь код перед ней попадет в первую часть. А весь код после нее - во вторую.

    Теперь при вызове метода invokeSuspend от переменной label зависит, какая из двух частей будет выполнена. Если label = 0, то выполнится первая часть кода (buildUrl + download). А если label = 1, то вторая (toast). Это дает возможность отделить вызов download от toast.

     

    Напомню, что первый раз метод invokeSuspend вызывается при старте корутины. При этом вызове отработает первая часть кода (в case 0) и запустится suspend функция, после чего метод invokeSuspend завершится (return). После того, как suspend функция закончит свою работу, надо сделать второй вызов метода invokeSuspend, чтобы отработала вторая часть кода, т.е. toast. 

    Но перед этим вторым вызовом надо поменять значение label с 0 на 1. Это может сделать  первая часть кода перед тем, как вызвать suspend функцию.

    int label;
    
    void invokeSuspend() {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                download(url); // suspend function
                return;
            }
            case 1: {
                toast("File is downloaded: " + url);
                return;
            }
        }
    }
    

     

    А второй вызов метода invokeSuspend будет поручен suspend функции, т.к. этот вызов должен быть выполнен по ее завершении.

    Когда suspend функция проходит преобразование из Kotlin в Java, она получает дополнительный входной параметр с типом Continuation. Это интерфейс. И класс, который мы сейчас разбираем, наследует этот интерфейс. Т.е. в suspend функцию передается текущий Continuation объект - this.

    int label;
    
    void invokeSuspend() {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                download(url, this); // suspend function
                return;
            }
            case 1: {
                toast("File is downloaded: " + url);
                return;
            }
        }
    }

    Когда suspend функция закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend метод (подробно о том, как это происходит, мы поговорим в уроке про создание suspend функций). Значение label было установлено в 1 (еще до запуска suspend функции), поэтому switch идет во вторую ветку и метод toast будет выполнен.

    Получается, что Continuation является колбэком для suspend функции. Если в корутине есть несколько suspend функций, то Continuation будет колбэком для всех них. Чуть дальше мы рассмотрим пример с двумя функциями.

     

    Давайте еще раз посмотрим на всю цепочку вызовов, чтобы стало понятнее. Я записал видео-презентацию. Используйте паузу, если вам необходимо время, чтобы обдумать какой-либо слайд подробно.

    Теперь вы примерно представляете себе схему, используемую корутинами и suspend функциями.

     

     

     

    Возврат значения suspend функцией

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

    Перепишем ранее рассмотренный пример корутины:

    launch {
        val url = buildUrl()
    
        val file = download(url) // suspend function
    
        toast("File is downloaded: $url")
    
        unzip(file) // suspend function
    
        toast("File is unzipped")
    }

     

    Функция download теперь возвращает файл, который мы используем в другой suspend функции - unzip. После выполнения unzip нам надо показать сообщение.

    Код будет разделен на три части: 
    1) download и все, что перед ней
    2) unzip и все, что между ней и download
    3) все, что после unzip

    Точки разделения - это suspend функции.

     

    Упрощенный код Continuation мог бы выглядеть примерно так:

    File file;
    
    void invokeSuspend() {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                file = download(url, this); // suspend function
                return;
            }
            case 1: {
                toast("File is downloaded: " + url);
                label = 2;
    
                unzip(file, this); // suspend function
                return;
            }
            case 2: {
                toast("File is unzipped");
                return;
            }
        }
    }

    Но тут есть проблема. Этот код не будет работать корректно. Обратите внимание на передачу файла между функциями download и unzip.

    Мы пишем:

    file = download(url, this);

    Но download при запуске только начнет загрузку файла в фоновом потоке, но не вернет файл сразу как результат вызова. Поэтому такой путь не сработает. Нужен другой способ получить результат от suspend функции download. 

    Давайте вспомним, что suspend функция вызывает invokeSuspend по завершении своей работы. А если она делает этот вызов, то пусть заодно и передает туда результат своей работы, т.е. File. Для этого у invokeSuspend есть входной параметр типа Object:

    void invokeSuspend(Object result) {
        ...
    } 

     

    Осталось этот Object привести к типу File. Continuation знает, что функция download вызовет метод invokeSuspend и передаст туда File, как результат своей работы. Во время этого вызова label уже будет равен 1, а значит будет вызван код в ветке case 1. А значит именно там надо делать приведение типа Object к типу File .

    File file;
    
    void invokeSuspend(Object result) {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                download(url, this); // suspend function
                return;
            }
            case 1: {
                file = (File) result;
                toast("File is downloaded: " + url);
                label = 2;
    
                unzip(file, this); // suspend function
                return;
            }
            case 2: {
                toast("File is unzipped");
                return;
            }
        }
    }

    Приводим result к типу File и используем полученный file в функции unzip. Таким образом мы получили результат работы suspend функции download в invokeSuspend.

     

    Давайте немного усложним пример и добавим выходной параметр функции unzip:

    suspend fun unzip(path: File): Long 

    unzip возвращает размер распакованного содержимого.

     

    Будем использовать это значение в сообщении, которое показываем после выполнения unzip:

    launch {
        val url = buildUrl()
    
        val file = download(url) // suspend function
    
        toast("File is downloaded: $url")
    
        val size = unzip(file) // suspend function
    
        toast("File is unzipped, size = $size")
    }

     

    Смотрим Java код:

    File file;
    Long size;
    
    void invokeSuspend(Object result) {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                download(url, this); // suspend function
                return;
            }
            case 1: {
                file = (File) result;
                toast("File is downloaded: " + url);
                label = 2;
    
                unzip(file, this); // suspend function
                return;
            }
            case 2: {
                size = (Long) result
                toast("File is unzipped, size = " + size);
                return;
            }
        }
    }
    

    Используется та же самая схема, что и с download. Функция unzip по завершении вызовет метод invokeSuspend и передаст туда результат своей работы типа Long. А в ветке case 2 результат будет приведен к типу Long и далее использован в сообщении.

     

    Если подытожить, то получается следующая схема взаимодействия Continuation и suspend функций. Метод invokeSuspend вызывает suspend функции, передает туда Continuation и завершает свою работу. А suspend функции по завершении своей работы снова вызывают invokeSuspend и возвращают поток выполнения кода в блок кода, который в корутине шел после этой функции. При этом они передают туда же результаты своей работы.

  • Кратко

    Рассмотрим корутину:

    launch {
        val url = buildUrl()
    
        download(url) //suspend function
    
        toast("File is downloaded")
    }

    download - это suspend функция, которая загружает файл. Она не заблокирует поток, в котором выполняется корутина, т.к. для своей работы использует фоновый поток. Но при этом функция toast будет вызвана только после того, как отработает download.

     

    Чтобы понять, что такое Continuation, мы взглянем на Java класс, который получится в результате преобразования Kotlin кода корутины. Но сразу разбираться в конечном результате этого преобразования будет слишком сложно. Поэтому мы начнем с упрощенной версии и далее будем добавлять в нее код, делая его более похожим на реальный. 

    Continuation класс:

    class GeneratedContinuationClass extends SuspendLambda {
    
        int label;
        String url;
    
        void invokeSuspend() {
            switch (label) {
                case 0: {
                    url = buildUrl();
                    label = 1;
    
                    download(url, this); // suspend function
                    return;
                }
                case 1: {
                    toast("File is downloaded: " + url);
                    return;
                }
            }
        }
    }

    Обратите внимание на то, что код корутины переехал в отдельный класс. Далее мы будем оперировать объектом этого класса.

    Нас интересует метод invokeSuspend, где и располагается код корутины. Этот метод будет вызван при старте корутины.

    Основная задача Continuation - сделать так, чтобы код, расположенный после вызова функции download, был выполнен только когда метод download завершил работу. Для этого код делится свитчем на две части. И добавляется переменная label. Точка разделения кода на две части - это suspend функция. Она и весь код перед ней идет в первую часть. А весь код после нее и до конца корутины идет во вторую часть. От значения переменной label зависит, какая из двух частей будет выполнена при вызове invokeSuspend.

    Первый раз метод invokeSuspend вызывается при старте корутины. Он выполнит первую часть кода, поменяет значение label на 1 и вызовет suspend функцию. Второй раз метод invokeSuspend будет вызван уже из suspend функции, когда она завершит свою работу. Для этого suspend функция при преобразовании из Kotlin в Java получает дополнительный входной параметр с типом Continuation. Это интерфейс. И класс, который мы сейчас разбираем, наследует этот интерфейс. Т.е. в suspend функцию мы передаем текущий Continuation объект - this.

    Когда suspend функция закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend метод. label был установлен в 1 (еще до запуска suspend функции), поэтому switch пойдет во вторую ветку и метод toast будет выполнен.

    Таким образом Continuation является колбэком для suspend функции. Если в корутине есть несколько suspend функций, то Continuation будет колбэком для всех них.

     

     

     

    Возврат значения suspend функцией

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

    Перепишем ранее рассмотренный пример корутины:

    launch {
        val url = buildUrl()
    
        val file = download(url) // suspend function
    
        toast("File is downloaded: $url")
    
        val size = unzip(file) // suspend function
    
        toast("File is unzipped, size = $size")
    }

    Функция download теперь возвращает файл, который мы используем в еще одной suspend функции - unzip. А unzip в свою очередь возвращает размер распакованного содержимого. Будем использовать это значение в сообщении, которое показываем после выполнения unzip.

    При преобразовании в java, код будет разделен на три части: 
    1) download и все, что перед ней
    2) unzip и все, что между ней и download
    3) все, что после unzip

    Точки разделения - это suspend функции.

     

    Взглянем на полученный из этой корутины метод invokeSuspend. Он стал немного сложнее, т.к. теперь я не стал убирать из него логику получения значений от suspend функций. 

    File file;
    Long size;
    
    void invokeSuspend(Object result) {
        switch (label) {
            case 0: {
                url = buildUrl();
                label = 1;
    
                download(url, this); // suspend function
                return;
            }
            case 1: {
                file = (File) result;
                toast("File is downloaded: " + url);
                label = 2;
    
                unzip(file, this); // suspend function
                return;
            }
            case 2: {
                size = (Long) result
                toast("File is unzipped, size = " + size);
                return;
            }
        }
    }

    Как мы ранее уже обсудили, suspend функция по завершении своей работы вызывает метод invokeSuspend. И именно сюда же она и передает результат своей работы. Для этого у invokeSuspend есть входной параметр типа Object.

    Suspend функция download при вызове invokeSuspend передаст файл (File). А в ветке case 1 будет выполнено приведение типа Object к типу File. 

    Suspend функция unzip при вызове invokeSuspend передаст размер (Long). А в ветке case 2 будет выполнено приведение типа Object к типу Long.


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Комментарии   

# RE: Урок 2. Корутины. ContinuationМихаил 04.12.2019 08:53
В 23 строчке последнего snippet кода опечатка — toast("File is unzipped, size =" + size);
# RE: Урок 2. Корутины. ContinuationВиталий 05.12.2019 10:03
неа, не опечатка, это фича такая в котлине :-)
# RE: Урок 2. Корутины. ContinuationМихаил 08.01.2020 14:39
так код в snipet на java
# RE: Урок 2. Корутины. ContinuationDmitry Vinogradov 25.02.2020 22:39
Верно, спасибо!
# RE: Урок 2. Корутины. ContinuationМихаил 13.04.2020 01:05
Как хранится переменная url? Среди file, size? Просто она инициализируется в case 0, и используется в case 1
# RE: Урок 2. Корутины. ContinuationМихаил 13.04.2020 01:06
Еще вопрос про передачу обычных функций из тела программы
# RE: Урок 2. Корутины. ContinuationDmitry Vinogradov 14.04.2020 09:49
Насколько я помню, для этих целей Continuation хранит ссылку на внешний класс, в котором он был создан. Он использует эту ссылку для доступа к полям и методам этого класса, если необходимо.
# RE: Урок 2. Корутины. ContinuationDmitry Vinogradov 14.04.2020 09:44
Да, среди file и size. В фрагменте кода Continuation класса в начале урока я ее показывал.
Далее в фрагментах кода я показывал только те переменные, которые нужны для объяснений.
# RE: Урок 2. Корутины. ContinuationИван 11.07.2020 18:33
Отличный урок, теперь всё стало намного понятнее))
# ИгорьИгорь 02.08.2020 18:55
Окей. Как работает на высоком уровне - понятно.
Но что используется для выполнения кода асинхронно внутри самого метода async?
Каким образом достигается та самая "дишевизна" создания корутин, что их можно плодить безболезненно плодить тысячами?
# ИгорьИгорь 02.08.2020 18:55
Пардон.
"Внутри самого метода async" -> внутри suspend функции.
# RE: ИгорьDmitry Vinogradov 09.08.2020 20:48
Подробно о suspend функциях я рассказываю в следующих уроках.
А дешевизна корутин достигается тем, что для их запуска используется пул потоков, а не отдельный поток на каждую корутину.
# RE: Урок 2. Корутины. Continuationmaks kazantsev 26.12.2020 06:28
Правильно ли я понимаю, что в java suspend функция запускается в фоновом потоке и дальше сразу идёт вызов return, не дожидаясь окончания работы suspend функции?
# RE: Урок 2. Корутины. ContinuationDmitry Vinogradov 11.01.2021 17:49
suspend функция должна запустить фоновый поток, в котором будут выполняться операции. А сама она как функция выполнится сразу и после нее будет return.
# ОчепяткаЕвгений 23.02.2021 10:32
"Будем использовать это значение в сообщении, который..." которОЕ
# RE: ОчепяткаDmitry Vinogradov 25.02.2021 13:18
Спасибо, пофиксил!
# RE: Урок 2. Корутины. ContinuationАлександр 29.08.2021 13:08
А каким образом механизм разбивает код на cases? После каждой suspend функции создается новый case?
# RE: Урок 2. Корутины. ContinuationDmitry Vinogradov 16.09.2021 17:18
Примерно так, да. Потому что, suspend функция является окончанием предыдущего case, и на этом месте работа метода invoke метода завершается, чтобы позже возобновиться в следующем case.
# RE: Урок 2. Корутины. ContinuationАндрей 29.08.2022 00:12
Получается, механику корутин можно реализовать на Java и пользоваться их преимуществами?
# RE: Урок 2. Корутины. ContinuationЛют 28.02.2023 20:47
Мы программисты, мы можем всё.
# Перевод корутины из Kotlin в JavaMike 11.07.2024 00:15
Подскажите, а как это код на котлине (вызов корутины) переводится в Java код?
Ведь компилятор kotlinc переводит сразу в байткод, без промежуточного преобразования в Java!
# RE: Перевод корутины из Kotlin в JavaDmitry Vinogradov 05.08.2024 19:47
Kotlin пишет байт-код, да. Но чтобы не разбираться тут в байт-коде, мы его конвертим обратно в читабельную Java.

Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal