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 executes
job.cancelAndJoin()
It’s going to be waiting for the coroutinefinally{}
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 in
finally{}
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!