Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is NetworkOnMainThreadException valid for a network call in a coroutine?

I'm putting together a simple demo app in Kotlin for Android that retrieves the title of a webpage with Jsoup. I'm conducting the network call using Dispatchers.Main as context.

My understanding of coroutines is that if I call launch on the Dispatchers.Main it does run on the main thread, but suspends the execution so as to not block the thread.

My understanding of android.os.NetworkOnMainThreadException is that it exists because network operations are heavy and when run on the main thread will block it.

So my question is, given that a coroutine does not block the thread it is run in, is a NetworkOnMainThreadException really valid? Here is some sample code that throws the given Exception at 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
        }
    }
}

I know I can simply run this using the Dispatchers.IO context and providing this result to the main/UI thread, but that seems to dodge some of the utility of coroutines.

For reference, I'm using Kotlin 1.3.

like image 852
Dan 0 Avatar asked Dec 02 '18 17:12

Dan 0


People also ask

Do coroutines run main thread?

Once the withContext block finishes, the coroutine in login() resumes execution on the main thread with the result of the network request.

What is runBlocking in coroutines?

Definition of runBlocking() functionRuns a new coroutine and blocks the current thread interruptible until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.

How do you call coroutines on Android?

The simplest way to create a coroutine is by calling the launch builder on a specified scope. It Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled. The launch doesn't return any result.

How coroutine is implemented?

A coroutine internally uses a Continuation class to capture the contexts for its execution. Then the dynamic aspect is modeled as a Job class. The use of async usually creates a Deferred job, which is a subclass of the Job class. The CoroutineContext type is required for a coroutine to execute.


2 Answers

My understanding of coroutines is that if I call launch on the Dispatchers.Main it does run on the main thread, but suspends the execution so as to not block the thread.

The only points where execution is suspended so as to not block the thread is on methods marked as suspend - i.e., suspending methods.

As Jsoup.connect(url).get() is not a suspending method, it blocks the current thread. As you're using Dispatchers.Main, the current thread is the main thread and your network operation runs directly on the main thread, causing the NetworkOnMainThreadException.

Blocking work like your get() method can be made suspending by wrapping it in withContext(), which is a suspending method and ensures that the Dispatchers.Main is not blocked while the method runs.

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
}
like image 144
ianhanniballake Avatar answered Oct 13 '22 16:10

ianhanniballake


Coroutine suspension is not a feature that magically "unblocks" an existing blocking network call. It is strictly a cooperative feature and requires the code to explicitly call suspendCancellableCoroutine. Because you're using some pre-existing blocking IO API, the coroutine blocks its calling thread.

To truly leverage the power of suspendable code you must use a non-blocking IO API, one which lets you make a request and supply a callback the API will call when the result is ready. For example:

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

With this kind of API no thread will be blocked, whether or not you use coroutines. However, compared to blocking API, its usage is quite unwieldy and messy. This is what coroutines help you with: they allow you to continue writing code in exactly the same shape as if it was blocking, only it's not. To get it, you must first write a suspend fun that translates the API you have into coroutine suspension:

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

Now your calling code goes back to this:

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

If, instead, you're fine with keeping your blocking network IO, which means you need a dedicated thread for each concurrent network call, then without coroutines you'd have to use something like an async task, Anko's bg etc. These approaches also require you to supply callbacks, so coroutines can once again help you to keep the natural programming model. The core coroutines library already comes with all the parts you need:

  1. A specialized elastic thread pool that always starts a new thread if all are currently blocked (accessible via Dispatchers.IO)
  2. The withContext primitive, which allows your coroutine to jump from one thread to another and then back

With these tools you can simply write

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")
}

When your coroutine arrives at the JSoup call, it will set the UI thread free and execute this line on a thread in the IO thread pool. When it unblocks and gets the result, the coroutine will jump back to the UI thread.

like image 38
Marko Topolnik Avatar answered Oct 13 '22 17:10

Marko Topolnik