Как сопрограммам Kotlin удается планировать все сопрограммы в основном потоке, не блокируя его?

#android #kotlin #kotlin-coroutines

Вопрос:

Я экспериментировал с сопрограммами Kotlin в Android. Я использовал следующий код, пытаясь понять его поведение:

 fun onStartButtonPressed(view: View) {
    Log.d(TAG, "Outside Scope: ${Thread.currentThread().id}")

    lifecycle.coroutineScope.launch {
        Log.d(TAG, "Top Level Scope: ${Thread.currentThread().id}")

        val t1 = launch {
            Log.d(TAG, "Task 1 Scope: ${Thread.currentThread().id}")
            for (i in 1..2) {
                delay(1000L)
                withContext(Dispatchers.Main) {
                    viewBinding.task1ProgressBar.progress = i * 50
                }
            }
        }

        val t2 = launch {
            Log.d(TAG, "Task 2 Scope: ${Thread.currentThread().id}")
            for (i in 1..4) {
                delay(1000L)
                withContext(Dispatchers.Main) {
                    viewBinding.task2ProgressBar.progress = i * 25
                }
            }
        }

        t1.join()
        t2.join()

        withContext(Dispatchers.Main) {
            Log.d(TAG, "Completion Scope: ${Thread.currentThread().id}")
            viewBinding.startButton.isEnabled = true
            viewBinding.resetButton.isEnabled = true
            viewBinding.statusTextView.text = "All tasks have been finished."
        }
    }

    viewBinding.statusTextView.text = "All tasks have been started."
    viewBinding.startButton.isEnabled = false
    viewBinding.resetButton.isEnabled = false
}
 

Вывод кода выглядит следующим образом:

 2021-06-15 21:05:13.066 18079-18079/com.demo.coroutinedemo D/MainActivity: Outside Scope: 2
2021-06-15 21:05:13.132 18079-18079/com.demo.coroutinedemo D/MainActivity: Top Level Scope: 2
2021-06-15 21:05:13.140 18079-18079/com.demo.coroutinedemo D/MainActivity: Task 1 Scope: 2
2021-06-15 21:05:13.142 18079-18079/com.demo.coroutinedemo D/MainActivity: Task 2 Scope: 2
2021-06-15 21:05:17.189 18079-18079/com.demo.coroutinedemo D/MainActivity: Completion Scope: 2
 

Это очень важно для меня, так как join() метод блокирует основной поток, но пользовательский интерфейс не зависает, почему это так?

Ответ №1:

Именно по этой причине были изобретены сопрограммы и чем они отличаются от параллелизма потоков. Сопрограммы не блокируют, а приостанавливают (ну, они могут делать и то, и другое). И «приостановить» — это не просто другое название для «блокировать». Когда они приостанавливаются (например, путем вызова join() ), они эффективно освобождают поток, который их запускает, чтобы он мог делать что-то еще где-то в другом месте. И да, это звучит как нечто технически невозможное, потому что мы находимся в середине выполнения кода какой-то функции, и нам приходится там ждать, но хорошо… добро пожаловать в сопрограммы 🙂

Вы можете думать об этом так, как будто функция разделяется на две части: до join() и после нее. Первая часть планирует фоновую операцию и немедленно возвращается. Когда фоновая операция завершается, она планирует вторую часть в основном потоке. Это не то, как сопрограммы работают внутренне (функции на самом деле не вырезаются, они создают продолжения), но именно так вы можете легко представить, как они работают, если вы знакомы с исполнителями или циклами событий.

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

Комментарии:

1. Итак, метод задержки также приостановлен, и именно по этой причине основной поток может обрабатывать другие задачи? Что, если метод задержки является длительной задачей, такой как обработка изображений, будет ли он по-прежнему запланирован в главном потоке? Кстати, есть ли какие-либо рекомендуемые ресурсы о том, как внутренние сопрограммы работают?

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

3. Наиболее типичный способ обернуть код блокировки , который будет делегирован соответствующему диспетчеру, — это обернуть его в вызов withContext , в котором вы передаете либо Dispatchers.Default или Dispatchers.IO в качестве параметра контекста. Если у вас есть параллельные задачи для выполнения в функции приостановки, вы обычно используете coroutineScope вместо withContext нее и можете запускать параллельные async вызовы из нее.

4. Да, delay() приостанавливается, т. е. освобождает основной поток и планирует выполнение кода под ним за 1000 мс. Затем может включиться другая задача. Если вы замените delay(1000) на Thread.sleep(1000) , вы должны увидеть , что обе задачи работают последовательно, для их завершения требуется 6000 мс, и в течение этого времени блоки пользовательского интерфейса (возможно, вам придется удалить withContext() , я не уверен).