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

 

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

  

 

Как создать suspend функцию

 

Сейчас мы будем рассматривать, как создать suspend функцию из асинхронного кода. Если же у вас есть какой-то синхронный метод и его надо сделать suspend, то там просто используется билдер withContext. Об этом мы поговорим, когда начнем тему билдеров.

 

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

suspend fun download(url: String): File {

}

Ключевое слово здесь - suspend. Это даст Котлину понять, что это suspend функция и она будет приостанавливать корутину. В конце урока я дам более подробное объяснение, зачем нужно использовать слово suspend. Но перед этим нам нужно понять, как такая функция работает.

 

Предположим, у нас есть некий NetworkService, который асинхронно умеет загружать файлы. И мы хотим обернуть его в suspend функцию download:

suspend fun download(url: String): File {
    networkService.download(url, object: NetworkService.Callback {
        override fun onSuccess(result: File) {

        }
    })
}

В колбэк onSuccess придет загруженный файл.

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

suspend fun download(url: String): File {
    networkService.download(url, object: NetworkService.Callback {
        override fun onSuccess(result: File) {
            continuation.resume(result)
        }
    })
}

Метод resume содержит определенную логику, часть которой - это вызов метода invokeSuspend с передачей туда результата.

Осталось где-то взять continuation. Для этого мы используем функцию suspendCoroutine.

suspend fun download(url: String): File {
    return suspendCoroutine { continuation ->
        networkService.download(url, object: NetworkService.Callback {
            override fun onSuccess(result: File) {
                continuation.resume(result)
            }
        })
    }
}

Весь наш код уходит в блок suspendCoroutine. Этот блок дает нам доступ к continuation, в который мы передаем результат работы networkService.download.

Получается, что suspend функция выполнила асинхронную работу и результат передала в continuation.resume, а тот уже передаст его в continuation.invokeSuspend.

 

 

 

Возврат ошибки

Кроме успешного результата, continuation может принять данные об ошибке.

Пусть у NetworkService.Callback есть метод onFailure. Он будет вызван, если при загрузке произошла ошибка. В нем мы вызовем метод continuation.resumeWithException и передадим туда исключение.

suspend fun download(url: String): File {
    return suspendCoroutine { continuation ->
        networkService.download(url, object: NetworkService.Callback {
            override fun onSuccess(result: File) {
                continuation.resume(result)
            }

            override fun onFailure(error: Exception) {
                continuation.resumeWithException(error)
            }
        })
    }
}

В этом случае ошибка не пойдет в метод invokeSuspend, а будет обработана корутиной. Т.е. в этом случае continuation не продолжит свое выполнение и код, который в корутине находится после suspend функции не будет выполнен.

 

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

 

 

 

Можно ли сразу вернуть результат

Я все время упоминаю, что suspend функция должна сначала выполнить свою работу, а потом вернуть результат. Но возможен и другой вариант в случае, если результат уже готов на момент вызова. В примере с функцией загрузки файла - это может быть использование кэша. suspend функция сначала проверяет кэш. Если файл уже есть в кэше, то функция сразу может вернуть его. Для этого ей надо просто сразу вызвать continuation.resume. А если файла в кэше нет, то действуем по старой схеме - стартуем асинхронную работу и по ее завершению вызываем continuation.resume.

Давайте посмотрим как в Kotlin добавить в suspend функцию такую возможность:

suspend fun download(url: String): File {
    return suspendCoroutine { continuation ->

        val file = getFileFromCache(url)

        if (file != null) {
            continuation.resume(file)
        } else {
            networkService.download(url, object : NetworkService.Callback {
                override fun onSuccess(result: File) {
                    continuation.resume(result)
                }

                override fun onFailure(error: Exception) {
                    continuation.resumeWithException(error)
                }

            })
        }
    }
}

Мы пытаемся получить файл из кэша. Если он там есть, то сразу передаем его в continuation, иначе запускаем асинхронную работу.

 

 

 

Блокирование потока

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

Давайте рассмотрим пример некорректной suspend функции. Представим, что используемый в примере выше NetworkService теперь работает синхронно. Его метод download не уходит в фоновый поток и не просит колбэк, а загружает файл в текущем потоке и возвращает как результат вызова метода. Т.е. метод networkService.download блокирует поток, в котором он вызван. Если мы используем его напрямую в suspend функции, то получится неправильная suspend функция: 

// wrong suspend function, don't do that!
suspend fun download(url: String): File {
    return suspendCoroutine { continuation ->
        val file = networkService.download(url)
        continuation.resume(file)
    }
}

Важно понимать, что ни метод suspendCoroutine, ни слово suspend в описании функции не cделают наш код асинхронным. Метод networkService.download будет выполнен в потоке корутины и заблокирует его. Поэтому от нас требуется обеспечить асинхронность кода в suspend функции, а в качестве колбэка использовать continuation.

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

 

 

Потерянная корутина 

Что будет если в suspend функции не вызвать continuation.resume? Это очень важный момент! Если мы не вызовем ни один из методов continuation.resume*, то корутина просто не продолжит выполняться. Никогда. Потому что она ждет, что suspend функция продолжит ее через вызов Continuation. Поэтому тут будьте аккуратны, не теряйте вызов Continuation - передайте ему результат или ошибку, чтобы корутина могла продолжиться или завершиться.

 

  

Зачем нужно слово suspend?

Теперь мы знаем достаточно про suspend функции, чтобы поговорить о значении слова suspend. Само по себе это слово не добавляет к функции никакой магии. Оно не сделает так, чтобы синхронный код вдруг перестал блокировать поток. Оно вообще ничего не делает. Это просто маркер того, что данная функция умеет (и должна) работать с Continuation, чтобы приостановить выполнение корутины не блокируя поток.

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

 

Создание

Чтобы suspend функция могла приостановить код не блокируя поток, ей нужен Continuation, выполнение которого она возобновит по завершению своей работы. Чтобы получить Continuation, используется функция suspendCoroutine. Возникает вопрос, почему любая другая функция без слова suspend не может этого сделать? Давайте допустим, что мы можем создать обычную (не suspend) функцию и вызвать в ней suspendCoroutine, который предоставит нам Continuation. Если мы вызовем такую функцию в корутине, то все отработает нормально, потому что у корутины есть Continuation. Его мы и получим. Но если мы вызовем ее вне корутины, то suspendCoroutine не сможет предоставить нам Continuation, потому что его просто нет.

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

 

Использование

Если мы в корутине собираемся использовать долго работающую функцию, которая помечена как suspend, то мы точно знаем, что эта функция приостановит код и не заблокирует при этом поток, в котором выполняется корутина. Если конечно, она написана корректно)

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

 

 

 

Что происходит внутри suspendCoroutine?

Об этом я подробно расскажу в пятом уроке.


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

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

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

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




Комментарии   

# DelphianVadim 13.12.2019 10:48
используется билдер withContext - это suspend функция, а не билдер
# А в чём проблема ненадолго заблокировать поток корутины?Konopko 11.01.2020 13:47
А в чём проблема ненадолго заблокировать поток корутины?
# RE: А в чём проблема ненадолго заблокировать поток корутины?Артём Афанасьев 25.01.2020 00:24
в том, что корутина может выполнятся в главном потоке
# RE: А в чём проблема ненадолго заблокировать поток корутины?Игорь 16.02.2024 01:34
В том, что у вас к примеру UI зависнет(если он у вас есть). А если мы говорим о дроиде - тогда приложение вылетит с соответствующим исключением.
# RE: Урок 3. Корутины. Suspend функцииАнна 02.05.2020 02:07
А если выполнять в новом потоке, но синхронно?
# RE: Урок 3. Корутины. Suspend функцииАнна 02.05.2020 02:15
"suspend функция download не заблокирует поток, в котором она будет вызвана. Потому что свою работу она будет выполнять в отдельном потоке" - действительно или в отдельном потоке? Если это так, то какая разница синхронно или асинхронно? А вот если suspend выполняется в главном потоке, то вопросов нет
# RE: Урок 3. Корутины. Suspend функцииDmitry Vinogradov 03.05.2020 18:09
suspend функция точно должна выполнять свою работу в отдельном потоке, чтобы не блокировать поток, в котором она была вызвана. Это контракт.
Вызывающий поток точно знает, что он не будет заблокирован ею. А значит можно безопасно делать такие вызовы даже в основном потоке.
# RE: Урок 3. Корутины. Suspend функцииSergey 14.09.2020 10:04
Получается что можно безопасно делать suspend Retrofit запросы(добавлены в версии 2.6) из main без указания withContext(Dispatchers.IO) { retroCall() }?
# RE: Урок 3. Корутины. Suspend функцииDmitry Vinogradov 17.09.2020 12:14
Да, ретрофит сам уйдет в IO поток и вернется с результатом в поток корутины.
# continuation.resume(result)redectoo 15.10.2020 12:59
Dmitry Vinogradov, в уроках вы говорите, что всегда нужно не забывать вызвать в suspend функции continuation.resume(result), иначе корутина не пойдет дальше работать, но в документации:

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html#sequential-by-default

В самом первом примере они возвращают результат из suspend функций без continuation.resume(result) и все работает. Не могли бы вы разъяснить этот момент. Заранее спасибо!
# RE: continuation.resume(result)Dmitry Vinogradov 26.10.2020 18:14
В тех примерах все просто. Там нет асинхронного кода и поэтому не используется suspendCoroutine.


continuation.resume надо не забывать, когда используем suspendCoroutine.
# Вопрос по синтаксисуМихаил 13.11.2020 12:52
Я хочу купить курс, но боюсь не получить ответы на некоторые вопросы. Я достаточно хорошо знаком с Java и когда-то именно разрабатывал на ней.

Весь этот сахар Kotlin...

Поясните, пожалуйста, за структуру

suspendCoroutine { continuation ->
networkService.download(...)
}

Я не понимаю, как это читать (
# RE: Вопрос по синтаксисуСергей 14.11.2020 22:29
Михаил, в этом случае советую вам начать изучение Kotlin с какой-нибудь книги, например - Head First. Kotlin. Руководство для начинающих программистов.

Изначально Дмитрий сразу сказал, что не будет разбирать основы языка Kotlin, а начнет с более важных тем.
# RE: Вопрос по синтаксисуМихаил 16.11.2020 10:12
Спасибо.
# RE: Вопрос по синтаксисуDmitry Vinogradov 14.11.2020 23:20
Сергей все верно написал. Этот курс надо проходить, когда уже знаете Котлин. Поэтому сначала прочитайте основы, наберите немного практики и приходите за продвинутыми фишками)
# suspendCoroutineИван 29.01.2021 01:38
Дмитрий, ты сказал что в уроке про билдеры расскажешь как правильно писать suspend функцию что бы suspendCoroutine не блокировал поток. Но в уроке про билдеры я не нашел примера про suspendCoroutine, там только примеры билдеров
# ИванИван 29.01.2021 03:44
вроде в теме про диспатчеры есть пример
# RE: suspendCoroutineDmitry Vinogradov 03.02.2021 00:34
Подскажи, о каком именно предложении в уроке идет речь, чтобы мне точно понимать.

suspendCoroutine сам по себе не блокирует поток. В нем мы не должны писать блокирующий код. Можно только асинхронный с колбэками.

А чтобы синхронный код увести в другой поток, можно использовать withContext. Об этом есть немного в уроке 17 и далее уже в практических уроках.
# ИванИван 09.03.2021 03:04
Можем либо withcontext либо suspend функцию высшего порядка thread{}, я прав?

А вообще вопрос касался этого предложения:
"О том, как в корутине выполнять синхронный код так, чтобы не блокировать поток корутины, мы поговорим позже, когда будем рассматривать билдеры."
# RE: Урок 3. Корутины. Suspend функцииАлександр 29.01.2021 18:08
:D
# RE: Урок 3. Корутины. Suspend функцииГеоргий 03.03.2021 20:12
Честно говоря меня ваши объяснения только запутали. Что плохого, если я из viewModelScope запущу suspend запрос, обращение к базе или просто супер долгий метод? Например, если это будет копирование файлов? Как будет верно вызвать такую функцию?
# RE: Урок 3. Корутины. Suspend функцииDmitry Vinogradov 05.03.2021 08:42
Ок, попробую перечитать урок и внести правки.

По контракту suspend функция не должна блокировать поток корутины в которой она вызывается. Потому что корутина вполне может работать и в Main потоке (случай viewModelScope).

Если из viewModelScope запускать suspend функцию, которая блокирует поток, то это заблокирует Main поток.
# А как купить курс???Григорий 25.11.2021 18:10
А как купить курс???
# синдром самозванцаГеоргий 02.11.2022 15:38
Честно говоря мало что понял из сказанного. Может быть я просто всегда использую suspend как функцию возвращающую результат и вообще не использую в коде suspendCoroutine? У нас нет легаси и о его существовании, я кажется узнал только что)) Весь урок как вода на одном дыханье, но наверное кто-то это использует :sigh:
# Отмена синдрома самозванцаГеоргий 02.11.2022 15:47
мне кажется этот раздел полезен, если вы много работаете с легаси кодом. В retrofit и room suspend выведены наружу, и работать с suspendCoroutine там просто не зачем. Но, наверное, в качестве общего развития почитать можно было
# return suspendCoroutineDamir 20.11.2022 17:47
Добрый день!
Не совсем понял, зачем мы используем return, когда вызываем return suspendCoroutine, и как он нам возвращает результат, и главное, что он нам возвращает, когда мы ее только запустили - коллбэк еще не отработал, результата нет, что будет получено по return из suspendCoroutine ? Судя по сигнатуре, вызывающей suspendCoroutine функции, вернуться должен в любом случае File, но понять это пока не получается.

Давайте я сначала напишу, что я понял - может это сократит Вам время на объяснение того, что я не понял).

Итак у нас есть некая API, которая умеет ходить в сеть в другом потоке ( чтобы не тормозить основной поток из которого она вызывается) и в коллбэке возвращает файл - это в общем довольно частое поведение API в Java.

Мы хотим обернуть ее в suspend-функцию.
Для этого мы создаем по сути, функцию-обертку
suspend fun download(url: String): File {

}
из которой вызываем нашу функцию
networkService.download(url, object: NetworkService.Callback {
override fun onSuccess(result: File) {
}
})

и в которой в функции onSuccess, то есть когда мы получили File, вызываем continuation.resume от объекта continuation, который получили на входе блока, для того, чтобы наша корутина смогла перескочить на следующий блок switch - тут все ясно, благодаря Вашему прошлому уроку.)
# return suspendCoroutineТимофей 03.12.2022 12:50
В следующих уроках есть ответ на ваш вопрос.

Language

Автор сайта

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

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

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

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

 

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

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



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



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

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal