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

preface

In the last article, the Kotlin coroutine corresponding to cancel combinatorial suspend function was given a preliminary understanding. In this article, we will explain the related knowledge of Kotlin coroutine related to the release of resources, timeout, composite suspend function!

No more words, just get started!

Take a look at this example:

fun main(a) = runBlocking<Unit> {
    val job = launch {
        repeat(1000){ i ->
            println("job:I'm sleeping $i")
            delay(10L)
            // Analysis point 1, if the release of resources here? What will be the consequence?
        }
    }
    delay(130L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit!")}Copy the code

Running effect

job:I'm sleeping 0
job:I'm sleeping 1 ... A job: I'm sleeping 10
main: I'm tired of waiting!
main:Now I can quit!
Copy the code

First we want the code inside the closure to loop 1000 times via repeat(1000), but the main thread forces the inner loop to be terminated by calling job.cancelAndJoin().

Imagine that when we request a network connection, and when we read a file stream, we’re doing time-consuming operations in a thread.

CancelAndJoin () like this will cause a massive memory leak if the execution is not completed or the corresponding stream is not freed and closed properly. (Analysis point 1)

Therefore, Kotlin gives us the corresponding solution:

1. Release resources in finally

The code is therefore modified to:

fun main(a) = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job:I'm sleeping $i ...")
                delay(10L)
                // Do not release resources here}}finally {
            // Release resources here
            // Any attempt to call a suspended function ina finally block throws a CancellationException
            // Because the code that runs continuously here can be cancelled
            println("job:I'm running finally")
        }
    }
    delay(130L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")}Copy the code

Finally {}, finally{}

job:I'm sleeping 0 ...
job:I'm sleeping 1 ...
...略
job:I'm sleeping 10 ...
main:I'm tired of waiting!
job:I'm running finally
main:Now I can quit.
Copy the code

Notice how this works:

  • The first run is:main:I'm tired of waiting!And thenjob:I'm running finally;
  • That is, when the main thread executesjob.cancelAndJoin()It’s going to be waiting for the coroutine finally{}After the run will continue to execute!
  • Therefore, regardless of whether the corresponding coroutine completes properly or not, the code block is executed first, and the actual completion is followed, so the free resource part can be placed infinally{}here

However, to actually release a resource, you often need a cancelled coroutine. How about calling a suspended function in finally{}?

fun main(a) = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job:I'm sleeping $i ...")
                delay(10L)}}finally {
            delay(20L)
            println("job:I'm running finally")
        }
    }
    delay(130L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")}Copy the code

The code does not report any errors! Run to see the effect:

...略
job:I'm sleeping 9 ...
job:I'm sleeping 10 ...
main:I'm tired of waiting!
main:Now I can quit.
Copy the code

Look at the last few lines! Job :I’m running finally release resource not executed!

That’s when something new is needed! withContext(NonCancellable){}

1.1 withContext (NonCancellable) {}

Let’s see what this code block actually means!

As is shown in

To summarize, when a cancellation is performed, the code block in the corresponding withContext is not canceled with the cancellation, but the contents of the corresponding code block are executed. More importantly, it does not affect the logic of the original coroutine!

So change the code again:

fun main(a) = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job:I'm sleeping $i ...")
                delay(10)}}finally {
            withContext(NonCancellable){
                println("job:I'm running finally")
                delay(20L)
                println("You can release the corresponding resource here.")
            }
        }
    }
    delay(130L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")}Copy the code

Running effect

job:I'm sleeping 0 ...
job:I'm sleeping 1 ...
...略
job:I'm sleeping 10 ...
job:I'm sleeping 11 ...
main:I'm tired of waiting!
job:I'M Running finally can release the corresponding resource here main:Now I can quitCopy the code

When we cancel or request network time, we may face the situation of timeout, so how to handle the timeout?

2. Timeout processing

2.1 withTimeout

fun main(a) = runBlocking {
    withTimeout(1330L){
        repeat(1000){ i ->
            println("I'm sleeping $i")
            delay(500L)}}}Copy the code

From this code block, it loops 1000 times in 1330L milliseconds, hanging for 500 milliseconds each time!

Running effect

I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1330 ms
Copy the code

We found that after 1330L milliseconds, it crashed! In practice, the program is not allowed to crash, so there is withTimeoutOrNull instead of timeout!

2.2 withTimeoutOrNull

fun main(a) = runBlocking {

    //withTimeoutOrNull Executes a timeout operation by returning NULL instead of throwing an exception:
    val result = withTimeoutOrNull(1330L){
        repeat(1000){ i ->
            println("I'm sleeping $i")
            delay(500L)}"OK" // When OK is returned, the above loop is complete, otherwise NULL} println(result ? :"Done")}Copy the code

Here we see the use of withTimeoutOrNull instead of withTimeout.

Let’s see how it works:

I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
Done
Copy the code

There’s not much to say, it’s simple! Next!

Now, freeing resources, and timeouts. Simulate a real combat to see!

3. Simulate actual combat

We will simulate a waterfall stream to load local image files

3.1 Not adding finally{}

import kotlinx.coroutines.*

var acquired = 0
class Resource {
    init {
        acquired++
    }
    fun close(a) {
        acquired--
    }
}
fun main(a) {
    runBlocking {
        repeat(1000){
            launch {
                val resource = withTimeout(60) {// Set the loading time to 60 milliseconds
                    delay(30)  // You can change it as much as you want, but not more than 60
                    Resource() 
                }
                // Close the resource
                resource.close()
            }
        }
    }
    // Non-0, memory leak
    //0 means all resources are freed, no memory leaks
    println(acquired) // The expected value is 0
}
Copy the code

Here we see that we created the mock resource file at the beginning, using global increment and global decrement each time we closed it.

If the last print is 0, it means that all resources are freed, otherwise there will be a memory leak!

Note: I purposely didn’t use finally here. Let’s see what it looks like without finally!

Running effect

51 // Every run is different! If the value is 0 each time, you can change the suspension time of the above note!Copy the code

Then try finally!

3.2 and finally {}

import kotlinx.coroutines.*

var acquired = 0

/ / pseudo code
class Resource {
    init {
// println("init $acquired")
        acquired++
    }

    fun close(a) {
// println("close $acquired")
        acquired--
    }
}

fun main(a) {
    runBlocking {
        repeat(1000) {
            launch {
                var resource: Resource? = null
                try {
                    withTimeout(60) {
                        delay(30)
                        resource = Resource()
                    }
                } finally{ resource? .close() } } } }//0 means all resources are freed, no memory leaks
    println(acquired) // The expected value is 0
}
Copy the code

Here we see nullable variables defined on the try, assigned each time the resource is instantiated, and closed if the resource is not empty in finally.

Running effect

0
Copy the code

All resources are released perfectly. Of course, readers can open the comments above and run again to see if the corresponding method has been executed.

4. Combine suspend functions

In the last article, I explained what a hang function is! Now let’s see how the composite suspend function works!

4.1 Default

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
}


fun main(a) = runBlocking {
    val time = measureTimeMillis { // This method simply counts the total elapsed time of the block in the closure!
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")}Copy the code

This measureTimeMillis {} simply indicates the total elapsed time in the closure. Nothing to say about the rest!

Look directly at the operation effect

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

You can see that both methods are executed in order! Execute the next one after the top one!

So what if you want to execute these two together?

4.2 Synchronization Execution

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 now see that each pending function is called with an additional async {} closure, which is used with more. Await () methods.

Let’s see how it works:

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

From this performance, you can see that the two methods are executed synchronously!

So let’s say I don’t want it to execute so fast, I want to initialize it and execute it when I want to execute it!

Koltin set us up too!

4.3 Lazy Execution

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
}

fun main(a) = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        
        Feel free to add any logical judgments before executing two

        two.start()
        
        Feel free to add any logical judgments before executing one
        // two-.join () wait for two to complete before calling the following code
        one.start()
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")}Copy the code

LAZY (start = coroutinestart.lazy) is added to async, which means that the corresponding suspended function is LAZY, and the corresponding.start() is manually called to execute the corresponding suspended function.

I deliberately put two before one here!

Let’s see how it works

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

Because this does not add any logical code between the start of the two pending functions, the running time is similar to synchronization. But as you can see, you can already control the order in which the corresponding suspended functions are executed by code! Lazy execution is achieved!

conclusion

Well, that’s the end of this piece! I believe you have a deeper understanding of Kotlin! In the next article, we’ll start explaining Kotlin coroutine context and scheduler!