Kotlin coroutine (on)
Reference:
Start School on Code | Bilibili
Kotlin: Using Coroutines | Pluralsight
A quick introduction to kotlin coroutines |
Kotlin Coroutine
Eliminate the difficulty of collaboration between concurrent tasks, break the callback hell, and realize multi-thread switching in the same code block.
launch
launch——Create a new coroutine and run it on the specified thread, non-blocking.
launch(Dispatchers.IO){
val image = getImage(imageId)
}
join
launch returns a Job object, and the Job.join() method can synchronously wait for the completion of the coroutine.
fun main(args: Array) = runBlocking {
val job = scope. launch { // launch newcoroutine and keep a reference to its job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
}
Cancelling coroutine execution
Use the Job.cancel() method to cancel a coroutine.
val job1 = scope.launch {...}
val job2 = scope. launch {...}
// Canceling a coroutine scope will cancel all sub-coroutines under this scope at the same time
scope. cancel()
val job1 = scope. launch { … }
val job2 = scope. launch { … }
// The first coroutine is canceled, the second is unaffected
job1. cancel()
job.cancelAndJoin() instead
job. cancel()
job. join()
Making computation code cancelable
yield() – suspend function, called periodically to check for cancellation.
fun main(args: Array) = runBlocking {
val job = scope. launch {
repeat(1000){
print(".")
yield()
Thread. sleep(1)
}
}
delay(100)
job. cancelAndJoin()
println("done")
}
isActive – an extended property that can be used inside a coroutine through the CoroutineScope object to detect whether the coroutine is canceled.
fun main(args: Array) = runBlocking {
val job = scope. launch {
repeat(1000){
if(!isActive) throw CancellationException()//Report an exception
print(".")
Thread. sleep(1)
}
}
delay(100)
job. cancelAndJoin()
println("done")
}
fun main(args: Array) = runBlocking {
val job = scope. launch {
repeat(1000){
if(!isActive) return @launchprint(".")
Thread. sleep(1)
}
}
delay(100)
job. cancelAndJoin()
println("done")
}
CancellationException
A cancelable suspending function throws a CancellationException when cancelled, which can be handled in the usual way. For example, try {...} catch{...} finally {...}
.
fun main(args: Array) = runBlocking {
val job = scope. launch {
try {
repeat(1000){
yield()
print(".")
Thread. sleep(1)
}
} catch (ex: CancellationException) {
println("cancelled: ${ex. message}")
} finally {
run(NonCancellable) {//Run non-cancellable code block
delay(1000)
println("In finally")
}
//println("In finally")
}
}
delay(100)
//job. cancelAndJoin()
job. cancel(CancellationException("Reason"))
job. join()
println("done")
}
join() and cancelAndJoin() Both functions will wait for all recycling operations to complete before continuing to execute the following code
Timeout
withTimeout()——The function returns an exception when it times out
fun main(args: Array) = runBlocking {
try{
val job = withTimeout(100) {
repeat(1000){
yield()
print(".")
Thread. sleep(1)
}
}
} catch (ex: TimeoutCancellationException) {
println("cancelled: ${ex. message}")
}
delay(100)
}
withTimeoutOrNull()——The function returns Null instead of an exception on timeout
fun main(args: Array) = runBlocking {
val job = withTimeoutOrNull(100) {
repeat(1000){yield()
print(".")
Thread. sleep(1)
}
}
if(job == null) {
println("timeout")
}
}
run Blocking
runBlocking—Creates a new coroutine, but blocks the main thread.
runBlocking{
delay(500)
}
CoroutineContext
/**
* Persistent context for the coroutine. It is an indexed set of [Element] instances.
* An indexed set is a mix between a set and a map.
* Every element in this set has a unique [Key].
*/
@SinceKotlin("1.3")
public interface CoroutineContext
We can access
CoroutineContext
in each coroutine block, which is the index set ofElement
, and we can use the indexKey
to access the context elements in
Job
public interface Job : CoroutineContext. Element
val job =launch {
println("isActive? ${coroutineContext[Job]!!.isActive})
//println("isActive? ${coroutineContext[Job.Key]!!.isActive})
}
job. join()
Operation result: isActive? true
CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element
The error handling of the coroutine can be customized:
//custom CoroutineContext
val errorHandle = CoroutineExceptionHandler { context, error ->
Log.e("Mike","coroutine error $error")
}
//Specify a custom CoroutineContext
GlobalScope. launch(context = errorHandle) {
delay(2000)
throw Exception("test")
}
Print result: coroutine error java.lang.Exception: test
Parent-child relationship
If you want to have a parent-child relationship between two coroutines, you need to flow the coroutineContext from the outer coroutine.
val outer = launch {
launch(coroutineContext) {//No parent-child relationship will be formed without coroutineContext. After the outer coroutine is canceled, it will not wait for the inner coroutine to complete, but cancel it directly
repeat(1000) {
print(".")
delay(1)
}
}
}
//delay(200)//Delay for a period of time to let the sub-coroutine start
//outer.cancelChildren()//Cancel child coroutine
outer. cancelAndJoin()
delay(1000)
println()
println("Outer isCancelled? ${outer.isCancelled}")
- If the parent coroutine is canceled or ended, all child coroutines under it will be canceled or ended.
- The parent coroutine needs to wait for the child coroutine to finish executing before finally entering the completion state, regardless of whether the code block of the parent coroutine itself has been executed.
- When the child coroutine is canceled, the parent coroutine will not be canceled, but the parent coroutine will be notified when the child coroutine is cancelled.
- The child coroutine will inherit the elements in the context of the parent coroutine. If it has members with the same Key, it will overwrite the corresponding Key. The overwriting effect is only valid within its own scope.
Combining context elements
To define multiple elements for coroutineContext, you can use the +
operator.
fun main() = runBlocking {
launch(Dispatchers. Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
}
Print result: I’m working in thread DefaultDispatcher-worker-1 @test#2
newSingleThreadContext
newSingleThreadContext is used to create a new thread for the coroutine to run. Dedicated threads are very expensive resources. In a real application, it must be freed using the close function when no longer needed, or stored in a top-level variable so that it can be reused throughout the application.
newSingleThreadContext("STC").use { ctx ->
val job = launch(ctx) {
println("STC is working in thread ${Thread.currentThread().name}")
}
}
The use
function in the kotlin standard library is used here to release the thread created by newSingleThreadContext when it is no longer needed.
withContext
withContext——Create a new coroutine, run it on the specified thread, and automatically switch back after execution.
Launch(Dispatchers.Main){
val image = withContext(Dispatchers.IO){
getImage(imageId)
}
avatarIv. setImageBitmap(image)
}
Multiple withContext are executed serially, eliminating the nesting of concurrent codes during collaboration.
Launch(Dispatchers.Main){
withContext(Dispatchers.IO){
...
}
withContext(Dispatchers.IO){
...
}
...
}
suspend Suspend function, non-blocking suspend.
When a coroutine executes to the suspend function, it will be suspended, break away from the current thread, and then switch back (resume) after executing the suspend function.
The suspend keyword has no direct effect on the coroutine, it is just a reminder that the function is a time-consuming operation, and the real implementation of the suspension needs to directly or indirectly call a coroutine’s own or our custom suspend inside the function function.
Time-consuming functions are automatically executed in the background, so that the main thread does not get stuck.
suspend fun suspendingGetImage(imageId:String){
withContext(Dispatchers.IO){//The suspension function that comes with the coroutine
getImage(imageId)
}
}
launch(Dispatchers.Main){
...
val image = suspendingGetImage(imageId)
avatarIv. setImageBitmap(image)
}
async
If bothWhen processing multiple time-consuming tasks, and these tasks have no interdependence, you can use async …await() to handle them.
btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val time1 = System. currentTimeMillis()
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.e("TAG", "1. Execute task1.... [The current thread is: ${Thread.currentThread().name}]")
"one" //The return result is assigned to task1
}
val task2 = async(Dispatchers.IO) {
delay(1000)
Log.e("TAG", "2. Execute task2.... [The current thread is: ${Thread.currentThread().name}]")
"two" //The return result is assigned to task2
}
Log.e("TAG", "task1 = ${task1.await()} , task2 = ${task2.await()} , time-consuming ${System.currentTimeMillis() - time1} ms [The current thread is: ${Thread.currentThread().name}]")}
}
Operation result:
2020-03-30 16:00:20.709 : 2. Execute task2… [The current thread is: DefaultDispatcher-worker-3]
2020-03-30 16:00:21.709 : 1. Execute task1… [The current thread is: DefaultDispatcher-worker-3]
2020-03-30 16:00:21.711 : task1 = one , task2 = two , taking 2037 ms [The current thread is: main]
await() will only suspend the coroutine when async is not executed and returns a result. If async already has a result, await() will directly get the result and assign it to a variable without suspending the coroutine.
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.e("TAG", "1. Execute task1.... [The current thread is: ${Thread.currentThread().name}]")
"one" //The return result is assigned to task1
}.await()//The effect of using this is the same as withContext
Deferred
async
The return value of the function is a Deferred object. Deferred is an interface type, inherited from the Job interface, so Job contains both attributes and methods Deferred, which mainly extends the await()
method on the basis of Job.
val result: Deferred = doWorkAsync("Hello")
runBlocking {
println(result. await())
}
fun doWorkAsync(msg: String): Deferred = async {
log("$msg - Working")
delay(500)
log("$msg - Work done")
return @async 42
}
fun log(msg: String) {
println("$msg in ${Thread.currentThread().name}")
}
Print result:
Hello – Working in ForJoinPool.commonPool-worker-1
Hello – Work done in ForJoinPool.commonPool-worker-1
42
Start lazy
CoroutineStart.LAZY
will not actively start the coroutine, but until after calling async.await()
or async.satrt()
will start (that is, lazy loading mode)
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async(start =CoroutineStart.LAZY) {
delay(3000)
1
}
val asyncB = async(start = CoroutineStart.LAZY) {
delay(4000)
2
}
log(asyncA. await() + asyncB. await())
}
}
log(time)
}
[main] 3
[main] 7077
val outer = launch {
val inner = async(star = CoroutineStart.LAZY) {
delay(100)
}
}
outer. join()
After using lazy loading, a parent-child coroutine relationship will be formed.