Как сопрограммы Kotlin знают, когда уступать при совершении сетевых вызовов? - PullRequest
0 голосов
/ 19 сентября 2018

Я новичок в сопрограммах Kotlin, и одна вещь, которую я не смог выяснить, - как сопрограммы знают, когда уступать другим при совершении сетевых вызовов.

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

Например, скажем, мы хотим нарисовать некоторый пользовательский интерфейс, которыйбудет отображать данные с удаленного сервера, и у нас есть только один поток, чтобы запланировать наши сопрограммы.Мы могли бы запустить одну сопрограмму, чтобы сделать вызовы REST API для получения данных, а другая сопрограмма нарисовать остальную часть пользовательского интерфейса, который не зависит от данных.Однако, поскольку у нас есть только один поток, одновременно может быть запущена только одна сопрограмма.И если сопрограмма, используемая для упреждающего извлечения данных, не дает, пока она ожидает поступления данных, две сопрограммы будут выполняться последовательно.

Насколько я знаю, реализация сопрограммы Kotlin не исправляет ни один из существующихРеализация JVM или сетевые библиотеки JDK.Поэтому, если сопрограмма вызывает REST API, она должна блокироваться так же, как это делается с использованием потока Java.Я говорю это потому, что мне кажется, что в python похожие понятия, которые называются зелеными нитями.И для того, чтобы он работал со встроенной сетевой библиотекой python, нужно сначала «обезьянить-исправить» сетевую библиотеку.И для меня это имеет смысл, потому что только сама сетевая библиотека знает, когда давать.

Так может ли кто-нибудь объяснить, как сопрограмма Kotlin знает, когда давать при вызове блокирующих сетевых API-интерфейсов Java?Или, если это не так, значит ли это, что задачи, упомянутые в приведенном выше примере, не могут быть выполнены одновременно для одного потока?

Спасибо!

Ответы [ 2 ]

0 голосов
/ 19 сентября 2018

сопрограмма работает превентивно

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

Изучение этого кода поможет вампосмотрите сущность сопрограмм Kotlin:

import kotlinx.coroutines.experimental.*
import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    var continuation: Continuation<Unit>? = null
    println("main(): launch")
    GlobalScope.launch(Dispatchers.Unconfined) {
        println("Coroutine: started")
        suspendCoroutine<Unit> {
            println("Coroutine: suspended")
            continuation = it
        }
        println("Coroutine: resumed")
    }
    println("main(): resume continuation")
    continuation!!.resume(Unit)
    println("main(): back after resume")
}

Здесь мы используем самый тривиальный диспетчер Unconfined, который не выполняет диспетчеризацию, он запускает сопрограмму прямо там, где вы вызываете launch { ... } и continuation.resume().Сопрограмма приостанавливает себя, вызывая suspendCoroutine.Эта функция запускает блок, который вы предоставляете, передавая объект, который вы можете использовать позже, чтобы возобновить сопрограмму.Наш код сохраняет его в var continuation.Элемент управления возвращается к коду после launch, где мы используем объект продолжения для возобновления сопрограммы.

Вся программа выполняется в главном потоке и печатает это:

main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume

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

Это фактически описывает то, что вы хотитеделать с простыми нитями.Преимущество сопрограмм заключается в том, что вы можете сделать «блокирующий» вызов в середине кода, привязанного к GUI, и он не замерзнет.В вашем примере вы бы написали одну сопрограмму, которая выполняет сетевой вызов, а затем обновляет графический интерфейс.Пока выполняется сетевой запрос, сопрограмма приостанавливается и запускаются другие обработчики событий, поддерживая графический интерфейс.Обработчики - это не сопрограммы, это просто обычные обратные вызовы графического интерфейса.

Проще говоря, вы можете написать этот код Android:

activity.launch(Dispatchers.Main) {
    textView.text = requestStringFromNetwork()
}

...

suspend fun requestStringFromNetwork() = suspendCancellableCoroutine<String> {
    ...
}

requestStringFromNetwork - это эквивалент "исправлений"слой IO ", но вы на самом деле ничего не исправляете, вы просто пишете обертки вокруг открытого API библиотеки IO.Практически все библиотеки Kotlin IO добавляют эти оболочки, а также есть библиотеки расширений для библиотек Java IO.Также очень просто написать свой собственный, если вы будете следовать этим инструкциям .

0 голосов
/ 19 сентября 2018

Ответ таков: Coroutine не знает о сетевых вызовах или операциях ввода-вывода.Вы должны написать код в соответствии с тем, что вы хотите, заключив тяжелую работу в различные сопрограммы, чтобы они могли выполняться одновременно, потому что поведение по умолчанию является последовательным.

Например:

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here (maybe I/O)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here (maybe I/O), too
    return 29
}

fun main(args: Array<String>) = runBlocking<Unit> {
        val time = measureTimeMillis {
            val one = doSomethingUsefulOne()
            val two = doSomethingUsefulTwo()
            println("The answer is ${one + two}")
        }
    println("Completed in $time ms")
}

выдаст что-то вроде этого:

The answer is 42
Completed in 2017 ms

, а doSomethingUsefulOne () и doSomethingUsefulTwo () будут выполняться последовательно.Если вы хотите одновременное выполнение, вы должны вместо этого написать:

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

, который будет выдавать:

The answer is 42
Completed in 1017 ms

, поскольку doSomethingUsefulOne () и doSomethingUsefulTwo () будут выполняться одновременно.

Источник: https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#composing-suspending-functions

ОБНОВЛЕНИЕ: О том, где исполняются сопрограммы, мы можем прочитать в руководстве по проекту github https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#thread-local-data:

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

Для ThreadLocal asContextElementФункция расширения здесь для спасения.Он создает дополнительный элемент контекста, который сохраняет значение заданного ThreadLocal и восстанавливает его каждый раз, когда сопрограмма переключает свой контекст.

Его легко продемонстрировать в действии:

val threadLocal = ThreadLocal<String?>() // declare thread-local variable
fun main(args: Array<String>) = runBlocking<Unit> {
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, threadlocal value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}

В этом примере мы запускаем новую сопрограмму в фоновом пуле потоков с помощью Dispatchers.Default, поэтому она работает в потоках, отличных от пула потоков, но все равно имеет значение локальной переменной потока, которое мыуказывается с помощью threadLocal.asContextElement (value = "launch"), независимо от того, в каком потоке выполняется сопрограмма.Таким образом, вывод (с отладкой):

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[CommonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[CommonPool-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
...