Является ли NetworkOnMainThreadException допустимым для сетевого вызова в сопрограмме? - PullRequest
0 голосов
/ 02 декабря 2018

Я собираю простое демо-приложение в Kotlin для Android, которое получает заголовок веб-страницы с помощью Jsoup.Я провожу сетевой вызов, используя Dispatchers.Main в качестве контекста.

Мое понимание сопрограмм заключается в том, что если я вызываю launch на Dispatchers.Main, то запускает в главном потоке, но приостанавливает выполнение, чтобы не блокировать поток.

Мое понимание android.os.NetworkOnMainThreadException состоит в том, что он существует, потому что сетевые операции тяжелы, и при запуске в главном потоке его блокируют.

Поэтому мой вопрос заключается в том, что сопрограмма не блокируетпоток, в котором он запущен, действительно ли NetworkOnMainThreadException действительно?Вот пример кода, который выдает данное Исключение в Jsoup.connect(url).get():

class MainActivity : AppCompatActivity() {
    val job = Job()

    val mainScope = CoroutineScope(Dispatchers.Main + job)

    // called from onCreate()
    private fun printTitle() {
        mainScope.launch {
            val url ="https://kotlinlang.org"
            val document = Jsoup.connect(url).get()
            Log.d("MainActivity", document.title())
            // ... update UI with title
        }
    }
}

Я знаю, что я могу просто запустить это с использованием контекста Dispatchers.IO и предоставить этот результат потоку main / UI, но этоКажется, уклоняется от некоторых утилит сопрограмм.

Для справки, я использую Kotlin 1.3.

Ответы [ 2 ]

0 голосов
/ 03 декабря 2018

Приостановка сопрограммы не является функцией, которая волшебным образом «разблокирует» существующий блокирующий сетевой вызов.Это строго кооперативная функция, которая требует, чтобы код явно вызывал suspendCancellableCoroutine.Поскольку вы используете какой-то ранее существующий блокирующий API ввода-вывода, сопрограмма блокирует вызывающий поток.

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

NonBlockingHttp.sendRequest("https://example.org/document",
        onSuccess = { println("Received document $it") },
        onFailure = { Log.e("Failed to fetch the document", it) }
)

С этим видом API ни один поток не будет заблокирован, независимо от того, используете вы сопрограммы или нет.Однако по сравнению с блокирующим API его использование довольно громоздко и грязно.В этом вам помогут сопрограммы: они позволяют вам продолжать писать код точно такой же формы, как если бы он блокировал, только это не так.Чтобы получить его, вы должны сначала написать suspend fun, который переводит ваш API в приостановку сопрограммы:

suspend fun fetchDocument(url: String): String = suspendCancellableCoroutine { cont ->
    NonBlockingHttp.sendRequest(url,
            onSuccess = { cont.resume(it) },
            onFailure = { cont.resumeWithException(it) }
    )
}

Теперь ваш код вызова возвращается к следующему:

try {
    val document = fetchDocument("https://example.org/document")
    println("Received document $document")
} catch (e: Exception) {
    Log.e("Failed to fetch the document", e)
}

Если вместо этого у вас все в порядке с сохранением блокирующего сетевого ввода-вывода, а это означает, что вам нужен выделенный поток для каждого одновременного сетевого вызова, то без сопрограмм вам придется использовать что-то вроде асинхронной задачи, bg от Anko и т. Д.Эти подходы также требуют предоставления обратных вызовов, поэтому сопрограммы могут снова помочь вам сохранить естественную модель программирования.Базовая библиотека сопрограмм уже поставляется со всеми необходимыми деталями:

  1. Специализированный пул эластичных потоков, который всегда запускает новый поток, если все в настоящий момент заблокированы (доступ через Dispatchers.IO)
  2. Примитив withContext, который позволяет вашей сопрограмме перепрыгивать из одного потока в другой и обратно

С помощью этих инструментов вы можете просто написать

try {
    val document = withContext(Dispatchers.IO) {
        JSoup.connect("https://example.org/document").get()
    }
    println("Received document $it")
} catch (e: Exception) {
    Log.e("Failed to fetch the document")
}

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

0 голосов
/ 02 декабря 2018

Мое понимание сопрограмм заключается в том, что если я вызываю запуск на Dispatchers. В основном он запускается в основном потоке, но приостанавливает выполнение, чтобы не блокировать поток.

Единственные точки, где выполнение приостанавливается, чтобы не блокировать поток, - это методы, помеченные как suspend, т. Е. Методы приостановки.

Поскольку Jsoup.connect(url).get() не является методом приостановки, он блокирует текущий поток.Поскольку вы используете Dispatchers.Main, текущий поток является основным потоком, и ваша сетевая операция выполняется непосредственно в основном потоке, в результате чего блокируется NetworkOnMainThreadException.

, как ваш get() метод.приостанавливается, оборачивая его в withContext(), что является методом приостановки и гарантирует, что Dispatchers.Main не блокируется во время работы метода.

mainScope.launch {
    val url ="https://kotlinlang.org"
    val document = withContext(Dispatchers.IO) {
        Jsoup.connect(url).get()
    }
    Log.d("MainActivity", document.title())
    // ... update UI with title
}
...