Почему «withContext» не переключает сопрограммы в «runBlocking»? - PullRequest
2 голосов
/ 11 ноября 2019

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

Код работает на детской площадке kotlin: https://pl.kotl.in/V0lbCU25K

Случай, который не работает

suspend fun main() = runBlocking {
    println("Hello, world!!!")
    println(Thread.currentThread().name)
    withContext(Dispatchers.IO) {
        println("Before heavy load: ${Thread.currentThread().name}")
        Thread.sleep(5000)
        println("After heavy load: ${Thread.currentThread().name}")
    }
    println("waiting")
    println(Thread.currentThread().name)
}

Выходы

Hello, world!!!
main @coroutine#1
Before heavy load: DefaultDispatcher-worker-1 @coroutine#1
After heavy load: DefaultDispatcher-worker-1 @coroutine#1
waiting
main @coroutine#1

Функция sleep в вышеприведенных блоках кода выполняется в том же потоке, что и основной поток, и блокирует его.


Следующие случаи соответствуют моим ожиданиям (задача, отнимающая много времени, не блокирует основной поток)

Корпус 1

suspend fun main() = runBlocking {
    println("Hello, world!!!")
    println(Thread.currentThread().name)
    launch {
        println("Before heavy load: ${Thread.currentThread().name}")
        Thread.sleep(5000)
        println("After heavy load: ${Thread.currentThread().name}")
    }
    println("waiting")
    println(Thread.currentThread().name)
}

Выходы

Hello, world!!!
main @coroutine#1
waiting
main @coroutine#1
Before heavy load: main @coroutine#2
After heavy load: main @coroutine#2

Корпус 2

suspend fun main() = runBlocking {
    println("Hello, world!!!")
    println(Thread.currentThread().name)
    launch {
        withContext(Dispatchers.IO) {
            println("Before heavy load: ${Thread.currentThread().name}")
            Thread.sleep(5000)
            println("After heavy load: ${Thread.currentThread().name}")
        }
    }
    println("waiting")
    println(Thread.currentThread().name)
}

Выходы

Hello, world!!!
main @coroutine#1
waiting
main @coroutine#1
Before heavy load: DefaultDispatcher-worker-1 @coroutine#2
After heavy load: DefaultDispatcher-worker-1 @coroutine#2

Ответы [ 2 ]

1 голос
/ 11 ноября 2019

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

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

Вот демонстрациячто означает, что поток не заблокирован:

fun main() {
    measureTimeMillis {
        runBlocking {
            launchManyCoroutines()
            println("Top-level coroutine sleeping on thread ${currentThread().name}")
            delay(2_000)
            println("Top-level coroutine done")
        }
    }.also { println("Program done in $it milliseconds") }

}

private fun CoroutineScope.launchManyCoroutines() {
    val cpuCount = getRuntime().availableProcessors()
    (1 until cpuCount).forEach { coroId ->
        launch { // on the main thread
            val sum = withContext(Dispatchers.Default) {
                println("Coroutine #$coroId computing on thread ${currentThread().name}")
                computeResult()
            }
            println("Coroutine #$coroId done on thread ${currentThread().name}:" +
                    " sum = $sum")
        }
    }
    (cpuCount + 1..100).forEach { coroId ->
        launch { // on the main thread
            println("Coroutine $coroId sleeping 1 s on thread ${currentThread().name}")
            delay(1_000)
            println("Coroutine #$coroId done on thread ${currentThread().name}")
        }
    }
}

private fun computeResult(): Int {
    val rnd = ThreadLocalRandom.current()
    return (1..1_000_000).map { rnd.nextInt() }.sum()
}

Эта программа запускает (100 + доступных процессоров) одновременных сопрограмм, все в основном потоке. Некоторые из них используют withContext(Dispatchers.Default) для выполнения ресурсоемкой задачи (суммирование миллиона случайных целых чисел) в пуле потоков, в то время как другие выполняют работу с приостановкой (задержка на одну секунду) непосредственно в главном потоке. Наконец, сопрограмма верхнего уровня спит в течение 2 секунд, прежде чем завершить.

Вся программа завершается чуть более 2 секунд и печатает что-то вроде следующего:

Top-level coroutine sleeping on thread main

Coroutine #2 computing on thread DefaultDispatcher-worker-2
Coroutine #3 computing on thread DefaultDispatcher-worker-3
Coroutine #4 computing on thread DefaultDispatcher-worker-5
Coroutine #1 computing on thread DefaultDispatcher-worker-1
Coroutine #5 computing on thread DefaultDispatcher-worker-6
Coroutine #6 computing on thread DefaultDispatcher-worker-4
Coroutine #7 computing on thread DefaultDispatcher-worker-8

Coroutine 9 sleeping 1 s on thread main
Coroutine 10 sleeping 1 s on thread main
...
Coroutine 99 sleeping 1 s on thread main
Coroutine 100 sleeping 1 s on thread main

Coroutine #3 done on thread main: sum = -1248358970
Coroutine #4 done on thread main: sum = -228252033
Coroutine #6 done on thread main: sum = -147126590
Coroutine #2 done on thread main: sum = -1065374439
Coroutine #1 done on thread main: sum = -2029316381
Coroutine #7 done on thread main: sum = -865387844
Coroutine #5 done on thread main: sum = -1695642504

Coroutine #9 done on thread main
Coroutine #10 done on thread main
...
Coroutine #99 done on thread main
Coroutine #100 done on thread main

Top-level coroutine done
Program done in 2066 milliseconds

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

1 голос
/ 11 ноября 2019

withContext на самом деле не работает асинхронно, он объединяет контексты.

В соответствии с основными документами Kotlin :

withContext:

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

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

fun main() = runBlocking {
        println("Before block ${Thread.currentThread().name}")
        withContext(Dispatchers.IO) {
            println("Long op ${Thread.currentThread().name}")
            delay(1000)
        }
        println("After block")
    }

- это:

Before block DefaultDispatcher-worker-1
IO? DefaultDispatcher-worker-1
After block

То, что вы хотите, может быть достигнуто с помощью функции launch внутри вашей сопрограммы:

Результат приведенного ниже кода:

fun main() = runBlocking {
        println("Before block ${Thread.currentThread().name}")
        launch(Dispatchers.IO) {
            println("Long op ${Thread.currentThread().name}")
            delay(1000)
        }
        println("After block")
    }

будет:

Before block DefaultDispatcher-worker-1
After block
IO? DefaultDispatcher-worker-3

TL; DR

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

withContext не выполняет асинхронность. Это объединяет контексты. Для выполнения асинхронного задания используйте функцию launch с указанным контекстом сопрограммы.

...