内容简介:Wanna keep in touch and be notified of similar posts?Another year has come to an end, so it's time to reflect again. I'll attempt to sum up what I've done this year, how that compares to what I was planning to do at this same time last year, and what I exp
This article assumes that you already know the basics of coroutines, and what a suspending function is.
Starting a coroutine
The rule of thumb with coroutines is that suspending functions can only be called from suspending functions. But how do we make our first call to a suspending function, if we need to already be in a suspending function to do so? This is the purpose that coroutine builders serve. They let us bridge the gap between the regular, synchronous world and the suspenseful world of coroutines.
launch
is the usually the first coroutine builder that we at when learning coroutines. The “trick” with launch
is that it’s a non-suspending function, but it takes a lambda parameter, which is a suspending function:
fun launch( context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit ): Job
It creates a new coroutine, which will execute the suspending block
of code passed to it. launch
is a fire-and-forget style coroutine builder, as it’s not supposed to return a result. So it returns immediately after starting the coroutine, while the started coroutine fires of asynchronously.
It does return a
Job
instance, which, according to the documentation…
is a cancellable thing with a life-cycle that culminates in its completion.
Basically, this Job
represents a piece of work being performed for us by a coroutine, and can be used to keep track of that coroutine. We can check if it’s still running, or cancel
it:
val job = GlobalScope.launch { println("Job is running...") delay(500) println("Job is done!") } Thread.sleep(200L) if (job.isActive) { job.cancel() }
delay
is a handy suspending function that we can use inside coroutines to wait for a given amount of the time in a non-blocking way.
Since the coroutine above is cancelled before the delay
is over, only its first print statement will be executed.
Job is running...
This happens because we call cancel
while the suspending delay
call is happening in the coroutine.
Blocking execution
What if there were no suspension points in the coroutine, and its entire body was just blocking code? For example, if we replace the delay
call with
Thread.sleep
:
val job = GlobalScope.launch { println("Job is running...") Thread.sleep(500L) println("Job is done!") } Thread.sleep(200L) if (job.isActive) { job.cancel() }
If we run the code again, we’ll see this output:
Job is running... Job is done!
We’re in trouble, cancellation is now broken! It turns out that coroutines can only be cancelled cooperatively . While a coroutine is running continuously blocking code, it won’t be notified of being cancelled.
Cooperation
Why doesn’t the Thread
that the coroutine is running on get interrupted forcibly? Because doing something like this would be dangerous. Whenever you write blocking code, you expect all those lines of code to be executed together, one after another. (Kind of like in a transaction!) If this gets cuts off in the middle, completely unexpected things can happen in the application. Hence the cooperative approach instead.
So how do we cooperate? For one, we can call functions from kotlinx.coroutines
that support cancellation already - delay
was an example of this. If our coroutine is cancelled while we are waiting for delay
, it will throw a JobCancellationException
instead of returning normally. If our coroutine was cancelled some time before
a call delay
, and this cancellation wasn’t handled, delay
will also throw this exception as soon as it’s called.
For example, let’s say that we have a list of entities to save to two different places which we perform by calling these two blocking functions:
fun saveToServer(entity: String) fun saveToDisk(entity: String)
We don’t want to end up in a situation where we’ve saved an entity to one of these places, but not the other. We either want both of these calls to run for an entity, or neither of them.
A first approach to this problem would be to use
withContext
, to suspend the caller, and move this operation to another thread. The code below will block a thread on the IO dispatcher for the entire length of our operation, which ensures that this coroutine is practically never cancelled:
suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) { entities.forEach { entity -> saveToServer(entity) saveToDisk(entity) } }
However, we can also add cancellation support, by checking if our current coroutine has been cancelled, manually. For example, we can do this after processing each entity:
suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) { entities.forEach { entity -> saveToDisk(entity) saveToServer(entity) if (!isActive) { return@withContext } } }
If our coroutine is cancelled while we run the blocking part of our code, that entire blocking part will still be executed together, but then we’ll eventually notice the cancellation at the end of the loop, and stop performing further work, in a safe way.
Yielding
Another handy function we have is
yield
, which has the original purpose of performing a manual suspension of the current coroutine, just to give other coroutines waiting for the same dispatcher a chance to execute. It essentially reschedules the rest of our coroutine to be executed on the same dispatcher that it’s currently on. If there’s nothing else waiting to use this dispatcher, this is essentially just a 0-length delay.
However, yield
also handles cancellation (meaning that it also throws a JobCancellationException
when it’s invoked in a cancelled coroutine), so we can call it every once in a while when performing lots of blocking work, to provide opportunity for the coroutine to be cancelled. This is done completely manually though, explicitly, which means we are aware of the possibility of cancellation.
yield
can easily replace manual cancellation checks, if terminating with an exception upon cancellation is good enough for us:
suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) { entities.forEach { entity -> saveToDisk(entity) saveToServer(entity) yield() } }
Just like with delay
, even if the coroutine happens to have been cancelled some time before yield
, it will notice this, and throw an exception. The cancellation doesn’t have to happen at the exact time that yield
is called.
Note that if there’s some cleanup of the coroutine to do (freeing up resources, etc.) when cancelled, manual cancellation checks can still be very handy, and should be used instead of yield
.
Conclusion
We’ve seen that coroutines always rely on cooperative
cancellation. We can either check if the coroutine we’re executing has been cancelled ourselves, or if we invoke any kotlinx.coroutines
functions in our code, these will perform the check for us, and attempt to stop the execution of our coroutine with an exception.
If you want to learn even more about coroutine cancellation, the following talk is for you: KotlinConf 2019: Coroutines! Gotta catch ‘em all! by Florina Muntenescu & Manuel Vivo .
Feedback on this article is very welcome. One of the best places to leave it would be on reddit .
Wanna keep in touch and be notified of similar posts? Follow me @zsmb13 on Twitter !
Continue reading...
Another year has come to an end, so it's time to reflect again. I'll attempt to sum up what I've done this year, how that compares to what I was planning to do at this same time last year, and what I expect to be next.
In what may be the start of a new series, I code review a project that was posted on reddit recently and got very popular very quickly. Let's see what we can learn from it?
Primaries Matter (a discussion of constructors)
Primary constructors play a fundamental role in Kotlin classes. Let's take a close look at them, and really understand what exactly is part of a primary constructor, and what makes this constructor so special.
Retrofit's coroutine support has been a long time coming, and it's finally coming to completion. Take a look at how you can use it to neatly integrate networking into an application built with coroutines.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。