Kotlin сопрограмм - задержка, как это работает? - PullRequest
2 голосов
/ 28 мая 2020

Я довольно привык использовать RX для обработки параллелизма, но в моей текущей работе у нас есть сочетание AsyncTask, Executors + Handlers, Threads и некоторых LiveData. Теперь мы думаем о переходе к использованию Kotlin Сопрограммы (и фактически начали использовать их в определенных местах в кодовой базе).

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

Я пробовал следовать за ними в Google codelab, и, хотя он дает мне некоторое понимание, он также вызывает множество вопросов без ответов, поэтому я попытался испачкать руки, написав код, отладив и просмотрев журнал выводит.

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

Здесь у меня есть код ниже, который ведет себя как я и ожидал. Я установил контекст сопрограммы с помощью Dispatchers.Main. Итак, как и ожидалось, когда я запускаю сопрограмму getResources, она блокирует поток пользовательского интерфейса на 5 секунд из-за Thread.sleep(5000):

private const val TAG = "Coroutines"

class MainActivity : AppCompatActivity(), CoroutineScope {
    override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        log("onCreate", "launching coroutine")
        launch {
            val resource = getResource()
            log("onCreate", "resource fetched: $resource")
            findViewById<TextView>(R.id.textView).text = resource.toString()
        }
        log("onCreate", "coroutine launched")
    }

    private suspend fun getResource() : Int {
        log("getResource", "about to sleep for 5000ms")
        Thread.sleep(5000)
        log("getResource", "finished fetching resource")
        return 1
    }

    private fun log(methodName: String, toLog: String) {
        Log.d(TAG,"$methodName: $toLog: ${Thread.currentThread().name}")
    }
}

Когда я запускаю этот код, я вижу следующие журналы :

2020-05-28 11:42:44.364 9819-9819/? D/Coroutines: onCreate: launching coroutine: main
2020-05-28 11:42:44.376 9819-9819/? D/Coroutines: onCreate: coroutine launched: main
2020-05-28 11:42:44.469 9819-9819/? D/Coroutines: getResource: about to sleep for 5000ms: main
2020-05-28 11:42:49.471 9819-9819/com.example.coroutines D/Coroutines: getResource: finished fetching resource: main
2020-05-28 11:42:49.472 9819-9819/com.example.coroutines D/Coroutines: onCreate: resource fetched: 1: main

Как видите, все журналы исходят из основного потока, и между журналами до и после Thread.sleep(5000) есть 5-секундный промежуток. Во время этого 5-секундного перерыва поток пользовательского интерфейса блокируется, я могу подтвердить это, просто взглянув на эмулятор; он не отображает какой-либо пользовательский интерфейс, потому что onCreate заблокирован.

Теперь, если я обновлю функцию getResources, чтобы использовать функцию приостановки delay(5000) вместо использования Thread.sleep(5000) вот так:

private suspend fun getResource() : Int {
    log("getResource", "about to sleep for 5000ms")
    delay(5000)
    log("getResource", "finished fetching resource")
    return 1
}

Тогда то, что я вижу, меня смущает. Я понимаю, что delay - это не то же самое, что Thread.sleep, но, поскольку я запускаю его в контексте сопрограммы, который поддерживается Dispatchers.Main, я ожидал увидеть тот же результат, что и при использовании Thread.sleep.

Вместо этого я вижу, что поток пользовательского интерфейса не заблокирован, пока происходит 5-секундная задержка, а журналы выглядят так:

2020-05-28 11:54:19.099 10038-10038/com.example.coroutines D/Coroutines: onCreate: launching coroutine: main
2020-05-28 11:54:19.111 10038-10038/com.example.coroutines D/Coroutines: onCreate: coroutine launched: main
2020-05-28 11:54:19.152 10038-10038/com.example.coroutines D/Coroutines: getResource: about to sleep for 5000ms: main
2020-05-28 11:54:24.167 10038-10038/com.example.coroutines D/Coroutines: getResource: finished fetching resource: main
2020-05-28 11:54:24.168 10038-10038/com.example.coroutines D/Coroutines: onCreate: resource fetched: 1: main

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

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

Ответы [ 2 ]

1 голос
/ 28 мая 2020

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

Этот код:

lifecycleScope.launch {
    myTextView.text = "Starting"
    delay(1000L)
    myTextView.text = "Processing"
    delay(2000L)
    myTextView.text = "Done"
}

Это что-то вроде:

myTextView.text = "Starting"
handler.postDelayed(1000L) {
    myTextView.text = "Processing"
    handler.postDelayed(2000L) {
        myTextView.text = "Done"
    }
}

Функции приостановки не следует ожидать блокировать. Если да, то они составлены неправильно. Любой блокирующий код в функции приостановки должен быть заключен во что-то, что является его фоном, например withContext или suspendCancellableCoroutine (что является более низким уровнем, потому что он работает напрямую с продолжением сопрограммы).

Если вы попытаетесь написать функция приостановки, подобная этой:

suspend fun myDelay(length: Long) {
    Thread.sleep(length)
}

, вы получите предупреждение компилятора о «Несоответствующем вызове метода блокировки». Если вы отправите sh его фоновому диспетчеру, вы не получите предупреждения:

suspend fun myDelay(length: Long) = withContext(Dispatchers.IO) {
    Thread.sleep(length)
}

Если вы попытаетесь отправить его на Dispatchers.Main, вы снова получите предупреждение, потому что компилятор считает любой код блокировки в основном потоке неправильным.

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

1 голос
/ 28 мая 2020

Лучший способ связать существующую интуицию с миром сопрограмм - это сделать это ментальное отображение: тогда как в классическом мире ОС планирует потоки к ядрам ЦП (предварительно приостанавливая их по мере необходимости), диспетчер планирует сопрограммы для потоков . Сопрограммы не могут быть приостановлены с упреждением, здесь вступает в силу кооперативный характер параллелизма сопрограмм.

Имея это в виду:

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

delay(delayTime) просто приостанавливает сопрограмму и планирует ее возобновление delayTime позже. Следовательно, вы должны ожидать увидеть совсем другой результат, чем с Thread.sleep, который никогда не приостанавливает сопрограмму и продолжает занимать свой поток, ситуация сравнима с ситуацией, когда Thread.sleep() не позволяет ядру ЦП запускать другие вещи, но будет занято-подожди.

...