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:
doSomethingUsefulOne
This method hangs for 3 seconds with no crash code;doSomethingUsefulTwo
This method hangs for 1 second and has crash code;doSomethingUsefulOne
withdoSomethingUsefulTwo
These two methods are executed synchronously;- when
doSomethingUsefulTwo
When this method crashes, it waits for 3 seconds instead of the first error message; - In other words,
doSomethingUsefulOne
The 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 of
coroutineScope {}
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.Unconfined
Unconstrained — 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.Unconfined
The 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!