Irbisa · Шпаргалка

Корутины Kotlin

Suspend-функции, scope, dispatcher-ы, структурная конкурентность, отмена, обработка исключений

Middle-разработчик · 17 вопросов

  1. 01

    Что такое корутины, чем они отличаются от потоков и почему их называют легковесными?

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

    fun main() = runBlocking {  repeat(100_000) {           // 100k threads would run out of memory    launch {      delay(1_000)            // suspends: the thread serves other coroutines      print(".")    }…
  2. 02

    Что делают runBlocking и GlobalScope и почему им не место в коде Android-приложения?

    runBlocking запускает корутину и блокирует текущий поток, пока она не завершится; GlobalScope запускает корутины без родителя, которые живут столько же, сколько процесс.

  3. 03

    Чем launch отличается от async и как по-разному они обрабатывают исключения?

    launch запускает работу «выстрелил и забыл» и возвращает Job; async вычисляет значение и возвращает Deferred<T>, которое ждут через await().

    class ProfileVM(private val api: Api) : ViewModel() {  fun save(p: Profile) {    viewModelScope.launch { api.save(p) }          // Job, no result  }   fun load() = viewModelScope.launch {…
  4. 04

    Что такое structured concurrency и почему это важно на Android?

    Structured concurrency (структурированная конкурентность) означает, что каждая корутина стартует внутри скоупа и становится потомком его Job: родитель не завершается, пока не завершатся потомки, отмена родителя отменяет их, а ошибка потомка сообщается родителю.

  5. 05

    Чем CoroutineScope отличается от CoroutineContext и какие элементы хранит контекст?

    CoroutineContext устроен как индексированный набор элементов, которые настраивают корутину; CoroutineScope является интерфейсом с одним контекстом и ресивером для launch и async, поэтому новые корутины его наследуют.

  6. 06

    Какой диспетчер выбрать для CPU-работы, блокирующего I/O и обновления UI и чем Dispatchers.Default отличается от Dispatchers.IO?

    Dispatchers.Main нужен для UI, Dispatchers.Default для CPU-нагрузки (столько потоков, сколько ядер), Dispatchers.IO для блокирующих вызовов (по умолчанию до 64 потоков).

    class ReportRepo(  private val dao: ReportDao,  private val io: CoroutineDispatcher = Dispatchers.IO,   // injectable for tests) {  private val writer = io.limitedParallelism(1)           // one writer at a time…
  7. 07

    Что на самом деле делает ключевое слово suspend и во что компилятор превращает suspend-функцию?

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

    // What you writesuspend fun showProfile(id: Long) {  val user = api.user(id)          // suspension point 1  val posts = api.posts(user.id)   // suspension point 2  render(user, posts)}…
  8. 08

    Когда использовать withContext, а когда async/await, и как запустить три независимых сетевых вызова параллельно?

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

    suspend fun loadDashboard(): Dashboard = coroutineScope {  val user = async { api.user() }  val feed = async { api.feed() }  val badges = async { api.badges() }  Dashboard(user.await(), feed.await(), badges.await())   // all three overlap}…
  9. 09

    Поймает ли try/catch в этом коде исключение и как правильно обрабатывать исключения в корутинах?

    fun load() {  try {    viewModelScope.launch {      val feed = api.feed()       // throws IOException      _state.value = FeedState.Ready(feed)    }  } catch (e: IOException) {    _state.value = FeedState.Error(e)  }}

    Нет. launch только планирует корутину и сразу возвращается; исключение выбрасывается позже, внутри корутины, когда блок try уже завершён.

  10. 10

    Как работает отмена корутин, почему она кооперативная и почему нельзя глотать CancellationException?

    cancel() только помечает Job как отменяемый; код останавливается в ближайшей точке приостановки вроде delay или withContext, которая проверяет job и выбрасывает CancellationException.

    suspend fun hashAll(files: List<File>): List<String> = withContext(Dispatchers.Default) {  files.map { file ->    ensureActive()                     // CPU loop: stop here if cancelled    sha256(file)  }}…
  11. 11

    Чем отличаются viewModelScope, lifecycleScope и repeatOnLifecycle и почему launchWhenStarted объявили устаревшим?

    viewModelScope живёт до onCleared(), lifecycleScope живёт, пока не уничтожены Activity или Fragment, а repeatOnLifecycle(STARTED) запускает свой блок на STARTED и отменяет его на STOPPED.

    class ChatFragment : Fragment(R.layout.fragment_chat) {  private val vm: ChatViewModel by viewModels()   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {    viewLifecycleOwner.lifecycleScope.launch {      viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {…
  12. 12

    Как обернуть API на колбэках в suspend-функцию или Flow?

    Для одного результата берите suspendCancellableCoroutine: запустите вызов, возобновите continuation из колбэка и отмените исходный вызов в invokeOnCancellation.

    suspend fun Call.await(): Response = suspendCancellableCoroutine { cont ->  enqueue(object : Callback {    override fun onResponse(call: Call, response: Response) = cont.resume(response)    override fun onFailure(call: Call, e: IOException) = cont.resumeWithException(e)  })  cont.invokeOnCancellation { cancel() }       // cancel the HTTP call too…
  13. 13

    Тысяча корутин по тысяче раз увеличивают один и тот же счётчик, и итог получается меньше миллиона. Почему и как это исправить, не блокируя поток?

    counter++ означает «прочитать, изменить, записать», а Dispatchers.Default выполняет эти корутины на нескольких потоках одновременно, и обновления перезаписывают друг друга.

    suspend fun broken(): Int {  var counter = 0  withContext(Dispatchers.Default) {    repeat(1_000) { launch { repeat(1_000) { counter++ } } }   // races: usually less than 1_000_000  }  return counter…
  14. 14

    Корутины и Flow против RxJava: в чём компромиссы и чем отличаются их пулы потоков?

    Корутины дают код, выглядящий последовательным, отмену, привязанную к скоупам, и обычный try/catch.

  15. 15

    Что меняют SupervisorJob и supervisorScope и почему scope.launch(SupervisorJob()) { } ломает отмену?

    Под SupervisorJob или supervisorScope упавший потомок не отменяет ни родителя, ни соседей.

    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) // wrong: the passed Job becomes the parent, this is not a child of scopeval orphan = scope.launch(SupervisorJob()) { longRunningSync() }// scope.cancel() leaves it running; only orphan.cancel() stops it…
  16. 16

    api.promo() бросает исключение, а promo.await() нигде не вызывается. Упадёт ли этот код и что будет с лентой?

    fun refresh() {  viewModelScope.launch {    val promo = async { api.promo() }   // throws IOException    val feed = api.feed()    _state.value = FeedState.Ready(feed)    // promo.await() is never called  }}

    Да, упадёт. async стартует сразу и, как потомок launch, сообщает об ошибке родителю немедленно, не дожидаясь await().

  17. 17

    Найдите баг: первый же вызов API, которому нужен токен, зависает навсегда, а за ним и все остальные.

    class TokenProvider(private val auth: AuthApi) {  private val mutex = Mutex()  private var token: Token? = null   suspend fun validToken(): Token = mutex.withLock {    val current = token    if (current != null && !current.isExpired()) current else refresh()  }   suspend fun refresh(): Token = mutex.withLock {    auth.refresh().also { token = it }  }}

    validToken() держит Mutex и вызывает refresh(), который пытается захватить его повторно.