This is the 19th day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021

preface

In the previous article, explained about the Kotlin coroutine corresponding to the release of resources, timeout, composite suspend functions related knowledge. In this article, we will explain the synchronization corresponding to Kotlin coroutines and explore the coroutine context and scheduler.

No more words, just get started!

Let’s take a look at an example

suspend fun doSomethingUsefulOne(a): Int {
    println("doSomethingUsefulOne")
    // All pending functions in kotlinx.coroutines are cancelable.
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(a): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 29
}

// Use async concurrency
fun main(a) = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")}// This is twice as fast because two coroutines are executed concurrently. Note that concurrency with coroutines is always explicit.
    println("Completed in $time ms")}Copy the code

We can see that using async {} in coroutines makes methods in closures behave synchronously!

Can async {} be used outside of coroutines to synchronize methods inside closures?

B: of course!

1. Async style functions

1.1 No crash

suspend fun doSomethingUsefulOne(a): Int {
    println("doSomethingUsefulOne")
    // All pending functions in kotlinx.coroutines are cancelable.
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(a): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 29
}

// This is not a suspend function
fun doSomethingUsefulOneAsync(a) = GlobalScope.async { 
    doSomethingUsefulOne()// This is a suspend function
}

// This is not a suspend function
fun doSomethingUsefulTwoAsync(a) = GlobalScope.async { 
    doSomethingUsefulTwo()// This is a suspend function
}

// Async style functions
fun main(a){  // Notice that there is no runBlocking {} so it's not inside the coroutine closure, it's inside the main entry!
    val time = measureTimeMillis {
        val one = doSomethingUsefulOneAsync()// Non-suspended functions can be called directly from a non-coroutine
        val two = doSomethingUsefulTwoAsync()
        // Waiting for results must call suspend or block
        // We use 'runBlocking {... } 'to block the main thread
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")}Copy the code

Here we see the use of GlobalScope.async {}, which calls the suspension function in the corresponding closure and returns a non-suspension function. So you can call the corresponding non-suspended function directly from the main thread, but since the calculation is inside the suspended function and this is not the coroutine area, you need to use runBlocking {… } to block the main thread.

Let’s see how it works:

doSomethingUsefulTwo
doSomethingUsefulOne
The answer is 42
Completed in 1124 ms
Copy the code

We can see that using this method can still achieve the corresponding effect!

But !!!!!!! This use is strongly not recommended !!!!!

Why not? Let’s do another example

1.2 There is a crash

suspend fun doSomethingUsefulOne(a): Int {
    println("doSomethingUsefulOne")
    delay(3000L) // Change 1 to wait 3 seconds
    println("doSomethingUsefulOne over")
    return 13
}

suspend fun doSomethingUsefulTwo(a): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 10 / 0 // Change 2 is changed to crash code
}

// This is not a suspend function
fun doSomethingUsefulOneAsync(a) = GlobalScope.async { 
    doSomethingUsefulOne()// This is a suspend function
}

// This is not a suspend function
fun doSomethingUsefulTwoAsync(a) = GlobalScope.async { 
    doSomethingUsefulTwo()// This is a suspend function
}

// Async style functions
fun main(a){  // Notice that there is no runBlocking {} so it's not inside the coroutine closure, it's inside the main entry!
    val time = measureTimeMillis {
        val one = doSomethingUsefulOneAsync()// Non-suspended functions can be called directly from a non-coroutine
        val two = doSomethingUsefulTwoAsync()
        // Waiting for results must call suspend or block
        // We use 'runBlocking {... } 'to block the main thread
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")}Copy the code

Here we see that in doSomethingUsefulOne, the hang time has been changed to 3 seconds; Second added a crash code to doSomethingUsefulTwo.

Let’s see how it works:

DoSomethingUsefulOne doSomethingUsefulTwo // Wait here for 3 seconds before the following log appears! DoSomethingUsefulOne over // Wait 3 seconds before this log and the subsequent error log are printed! Exceptionin thread "main" java.lang.ArithmeticException: / by zero
Copy the code

It can be seen from this running effect:

  • doSomethingUsefulOneThis method hangs for 3 seconds with no crash code;
  • doSomethingUsefulTwoThis method hangs for 1 second and has crash code;
  • doSomethingUsefulOnewithdoSomethingUsefulTwoThese two methods are executed synchronously;
  • whendoSomethingUsefulTwoWhen this method crashes, it waits for 3 seconds instead of the first error message;
  • In other words,doSomethingUsefulOneThe longer this method hangs, the longer the error message will wait!

That way is obviously a big hole ah! So say: ten million! Never! Never! Don’t use this method!!

Of course, as an interviewer, it is ok to torture the candidate appropriately.

So what should be used?

2. Structured concurrency with Async

2.1 No crash

suspend fun doSomethingUsefulOne(a): Int {
    println("doSomethingUsefulOne")
    // All pending functions in kotlinx.coroutines are cancelable.
    delay(1000L) // Change this to the original data above without the crash condition
    return 13
}

suspend fun doSomethingUsefulTwo(a): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 29 // Change this to the original data above without the crash condition
}

// Use async structured concurrency
suspend fun concurrentSum(a): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

fun main(a) = runBlocking {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")}Copy the code

Code analysis:

  • First of all, main is still the main coroutine;
  • Secondly, the use ofcoroutineScope {}The corresponding closure is filled with the code from yesterday’s example;
  • The function becomes a suspended function, and the return value is the sum of synchronized methods

Let’s see how it works

doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 1075 ms
Copy the code

The total run time is similar to the above, but what about the corresponding crash?

2.2 Crash

suspend fun doSomethingUsefulOne(a): Int {
    println("doSomethingUsefulOne")
    // All pending functions in kotlinx.coroutines are cancelable.
    delay(3000L)
    println("doSomethingUsefulOne over")
    return 13
}

suspend fun doSomethingUsefulTwo(a): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 10 / 0
}

// Use async structured concurrency
suspend fun concurrentSum(a): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

fun main(a) = runBlocking {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")}Copy the code

DoSomethingUsefulOne hangs for 3 seconds, doSomethingUsefulTwo crashes!

Let’s see how it works

DoSomethingUsefulOne doSomethingUsefulTwo // An error is reported when delay(1000L) suspends Exception for a secondin thread "main" java.lang.ArithmeticException: / by zero
Copy the code

From this point of view, when the second suspend method fails, the first suspend method terminates. This is what normal wants!

3. Explore the coroutine context and scheduler

Coroutines always run in some context represented by the CoroutineContext type. A coroutine context is a collection of various elements, where the main element is the Job in the coroutine.

All coroutine builders such as launch and async receive an optional CoroutineContext parameter, which can be used to explicitly specify a scheduler for a new coroutine or other context element.

Take a look at the following example

3.1 All schedulers

fun main(a) = runBlocking<Unit> {

    // When launch {... }, which inherits the context (and the scheduler) from the CoroutineScope that started it.
    In this case, it inherits the context from the runBlocking main coroutine in the main thread.
    launch {
        delay(1000)
        println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")}// unrestricted -- will work in the main thread
    // Unconstrained schedulers are great for performing tasks that do not consume CPU time, and for coroutines that do not update any shared data (such as the UI) that is limited to a particular thread.
    launch(Dispatchers.Unconfined) {
        println("Unconfined : I'm working in thread ${Thread.currentThread().name}")}// The default scheduler will be retrieved
    The default scheduler uses a shared background thread pool.
    launch(Dispatchers.Default) {
        println("Default : I'm working in thread ${Thread.currentThread().name}")}// Will make it get a new thread
    // A dedicated thread is a very expensive resource.
    launch(newSingleThreadContext("MyOwnThread")) {
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")}}Copy the code

Let’s see how it works:

Unconfined : I'm working in thread main
Default : I'm working inThread DefaultDispatcher-worker-1 // Time consuming operations can use newSingleThreadContext: II am working in thread MyOwnThread // I am working in thread MyOwnThread //m working in thread main
Copy the code

From this operation, we can know:

  • When launch {… }, it inherits the context from the runBlocking main coroutine in the main thread. It remains the main thread even if it suspends for a second or calls a suspend function;
  • When the parameter is:Dispatchers.UnconfinedUnconstrained — still in the main thread;

What about limited? What happens??

3.2 Unrestricted scheduler vs restricted scheduler

fun main(a) = runBlocking<Unit> {

    // Coroutines can be suspended on one thread and resumed on another.
    launch(Dispatchers.Unconfined){
        println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
        delay(500L)
        println("Unconfined : After delay in thread ${Thread.currentThread().name}")
    }

    launch {
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")}}Copy the code

Running effect

Unconfined : I'm working in thread main
main runBlocking: I'm working in thread main 
Unconfined : After delay inThread kotlinx. Coroutines. DefaultExecutor / / here to become a child thread main runBlocking: After a delayinThread main // The thread is still the primary thread even after suspensionCopy the code

From this running effect, we know:

  • A scheduler with no arguments is still the main thread even after suspension! Suitable for UI update operations;
  • Parameters as follows:Dispatchers.UnconfinedThe scheduler for the main thread before suspension, after suspension becomes child thread! Inappropriate UI update operation
  • So coroutines can be suspended on one thread and resumed on another.

Since coroutines can be suspended on one thread and resumed on another, how do you debug them?

3.2 Debugging coroutines and threads

3.2.1 Tool Debugging

As is shown in

Every time you debug, you need to manually click the left mouse button to pop up the following prompt

Their states are described here respectively

  • The first coroutine has a SUSPENDED state – it is waiting for values so that they can be multiplied.
  • The second coroutine is evaluating a — it has a RUNNING state.
  • The third coroutine has an CREATED state and does not evaluate the value b.

However, when the author debugs, the following state will not change with the debugging change. The state can only be changed when the following content is closed, the left mouse button above is re-executed, and the following content is opened again.

I also do not know the operation is wrong or what! Let’s just say it exists.

3.2.2 Log Debugging

fun log(msg:String) = println("[${Thread.currentThread().name}] $msg")

// Debug with logs
fun main(a) = runBlocking<Unit> {
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")}Copy the code

This is very simple, just define a method to print the current thread before printing

Running effect

[main] I'm computing a piece of the answer
[main] I'm computing another piece of the answer
[main] The answer is 42
Copy the code

conclusion

Well, that’s the end of this piece! I believe you have further mastered the coroutine related knowledge points! The next article will cover coroutine context and scheduler in depth!