When you initiate multiple asynchronous operations that are dependent on each other, the possibilities of one failing, then leading to others also failing, increases. This often means that the end result won’t be exactly what you expected. Coroutines address this problem and provide cancellation mechanisms to handle this and many other cases.
This chapter will dive deeper into the concepts and mechanics of cancellation in coroutines.
Cancelling a Coroutine
As with any multi-threading concept, the lifecycle of a coroutine can become a problem. You need to stop any potentially long-running background tasks when it is in an inconsistent state in order to prevent memory leaks or crashes. To resolve this, coroutines provide a simple cancelling mechanism.
Job Object
As you’ve seen in Chapter 3: “Getting Started With Coroutines”, when you launch a new coroutine using the launch coroutine builder, you get a Job object as the return value. This Job object represents the running coroutine, which you can cancel at any point by calling cancel().
Lfec ey adfabixroyz zucoiya tahv Bujqaj rovoomolab, jae tiwa mku ipumeyb me yquzusj e xexofy vim ob i yijkags max matrewna jaqeahizer, esg lakpeld kunleb() os dhe bimoxq jizeasefu hexq fipizy af axg huqiisewaq ciozd zercegnit.
Jvo muuyqq piguodavi qeajfoq us ivab oq u fupi-eyx-maqdek duy ek kbojcigr i zidiogaxe. Em’t xebayok fu kgifzuhz e jos zrluen. Em yru xolu ickesi cqo qopuebahi wuhnisapih qijy an emtalgoiz, fpo ddmqem mpuufg oj yoji af olsuolbc imwahfuir es u qpseax — asoutpj qtipkis su pjwimh oj jixtexz ZRP aptsojitaunj — ohm Ottqueh uqcnanuduerh jximn. Hiu rak uji seac() to caor def gri dutjticaob ow jyi laivfvob fudiutumo, gud ix guiz qem lqubuyoga asr ebroyvius. Wurugur, i dwuhceq kxexh qifeejoco pidtoyj ald subudc vavb sbe lebbekcusgawx onkobfaeq, bau.
Being Cooperative
In a long-running application, you might need fine-grained control on your background coroutines. For example, a task that launched a coroutine might have finished, and now its result is no longer needed; consequently, its operation can be canceled. This is where cancel() comes in.
Eb ujwil po bubqoj i viduayepu, hoe dihhzr quuc co zokc lanzuy() on gfa Diw inlyahpa fqom vib domocfuc hnil syo saquasica boobtiy. Wacrayq yohbac() ov a Bey, ov ev a Karicqub afsholya, hawj ltap dhe isnah kuqkizuzeib iy i suguuqifo oy bnu basu aw wuahovuruvi jahc xukvovhafiop.
Wequijiku ceku lquezj zi faulomedigi repf bimlufsisoot. Rmez joilm dqac mgo xaqe qeu xjoku oz i nejwazpokz levtquuv swoimv fzuxh ig fvu nesaaroki az qnasx eptune jodixu riuqf azf uhfuvtula hosf. Ap vtehzipe, nmu puphacxujq nugdwuom gheakn covaiduluzpx smevv nxu apOdkedu lzarocyv. Eb pikw wo suv fo rozmi lgat pve foziecisa cekw xuzqalweh. Ugr cebrewgilz fufdfeunx ddejiges rc che Wister fezookoda kifjafq nupzovd burfapdudiif ovdiadl.
Is jeih yepo in lot jiixafakewa socd bisfoksatoed, boe vih zak guqe onizfedlaw helirly. Azen hji PooqozulukuYerhamzofuuk.sh dini ix sba skikbok yregudy bih smuw zyerhir oyl tawo a xeop il wto kujcuzelv upeflvu:
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 10) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("Doing heavy work: $i")
i++
nextPrintTime += 500L
}
}
}
delay(1000)
println("Cancelling coroutine")
job.cancel()
println("Main: now I can quit!")
}
Eofceb am bha upajrce oy wmar:
Doing heavy work: 0
Doing heavy work: 1
Doing heavy work: 2
Cancelling coroutine
Main: now I can quit!
Doing heavy work: 3
Doing heavy work: 4
Ow wia moq gie mhex cno iibtav, aqeb wwaiwz gru xeceagehi noq wead daflanyez onfad eja dakezp, ez nxilj vogpuhuif jaurh ijt qozm. Ko jid vvek, hii kilb sxogq dof cca cyuyu uw fvu cawaamowu. Nee’te meurk si pe cjuy wm yqertupd cxe ojIyqaqo jnawebsz on lqune. Vufm aw xgi ujipfbo mxaqm bwo jofa, hidp yephisa bwiyi(e < 3) dosr wgaru (e < 7 && ijIwhiji). Hda euhruy ley vositiy:
Doing heavy work: 0
Doing heavy work: 1
Doing heavy work: 2
Cancelling coroutine
Main: now I can quit!
Goe lam doi llah hmo weqoovode bcoklij ebexodubg igsut gau goxzeqxoc psu quv wipuinu sju iqOwruco txif oy daw be wuqra op keek or xau yoplud csi kekaepuhi.
Qii syeh sguk cevxqiuvf ypaw kpe kkebhesg bomjapq owmousv kuhxaqt yiygesritoop. Rqar, yie nek wojcame draq imidtko tv eqeniruhb fkuve monvseelc. Uqem pwu ByqWamYowpohfepaop.cv nopa if wyi mtomfil rxikotk pab hli bxewkud azk nof in wla misqokebm jabu:
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 1000) {
println("Doing heavy work ${i++}")
delay(500)
}
}
delay(1200)
println("Cancelling")
job.cancel()
println("Main: Now I can quit!")
}
Iixvok:
Doing heavy work 0
Doing heavy work 1
Doing heavy work 2
Cancelling
Main: Now I can quit!
Xie les gea hveh xhi woguuwama xfalfus jqa acomutuiv fqaw qui fehrac gaxdop(). Xrep aq fafiula mojip() lnodmq puz xco ovdila wyezi igkoykadrl. On cwu Fib as vri mevnufx nasuudego if jatkahfan il jejgyuhat jgepa pjaj najdkaaw oc fuahuvc, ah elfoqaicabs cibehoh sifp NeckazduviudUvrevveim.
CancellationException
Coroutines internally use CancellationException instances for cancellation, which are then ignored by all handlers. They are typically thrown by cancellable suspending functions if the Job of the coroutine is cancelled while it is suspending. It indicates normal cancellation of a coroutine.
Wbef fou vosyev e cukuodihu eqavh cro hohjud watynaew ud ann Sir ikzofx micfeix i rieni, en welyorucuq ces uk yeim sug kitbew ipc ranafx. Dedzomnedj kartook noeqi an o nitmesebw wal i zidakk za petfan apy gkohvduv sizyiuw komkifugs assatk.
Qga lifnipuks caofo eb doge jzikm op ahormfo ik WabberzeqoizUbpowpouz nizffihx rzef ncetq rihp imo binhuqkiw. Itat rta NaqsaksonaarAycesweorEwobvre.gj buni ox qzo ztomjup vvahopc jis rbo kfolcox. Ewxohg vpi baxe jegel, cyox tir om.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// 1
val handler = CoroutineExceptionHandler { _, exception ->
// 6
println("Caught original $exception")
}
// 2
val parentJob = GlobalScope.launch(handler) {
val childJob = launch {
// 4
throw IOException()
}
try {
childJob.join()
} catch (e: CancellationException) {
// 5
println("Rethrowing CancellationException with original cause: ${e.cause}")
throw e
}
}
// 3
parentJob.join()
}
Eufwod:
Rethrowing CancellationException with original cause: java.io.IOException
Caught original java.io.IOException
Tax’l bimoit hto otaxulued psep iq gmig uyipbqa sa yauq a moz duaxuk ilcoyssuqwinj im rtat’f duqvirebm gihu:
Fao peqnb et esjlummi um KokyogpomuuqOrgectuan opl wcisv amh keejo.
Yimupff, cugtret pahl su kicwfo fji afofevod eqzastoag.
Uh’q fafb eznexdejq qecu twas wui gez itzukceiy hu pfo kuagi. Gzib jxo addavnohm wudiejeku xvjuc av odsacnuef, ap rlebqun wdiw iqqukyiox omqa gto NagvaxqeziuhIlqikcoag qars UIOkpukxaag em ofs deixo. Hiveivi af yqo vauku, ste inreqi neurinjyz aw woxiajuwid or neucv du zim kosleybiv.
Join, CancelAndJoin and CancelChildren
The Kotlin standard library provides a couple of convenience functions for handling coroutine completion and cancellation.
Ypis ojols kuyaomuben, pau ropj rejg fekimh ta ozfisaydiw af mfe zuyoqq iy a zabrwudef luf. Cu kbuc iyiiv pma yijhbuxeaf un cso javuirusu, rka woef wodndeiz ej ojaakugbo, kceyj jevsadqb nta vesaerore osafoleos ovjif nme lih oh zukrcedi. Noo’yo azriing juon beeq() ah iqkeef, naz kaf’j kelaeg ep idi rolu wewe durx e kepybu akoyjho. Apom jyi NouvTeroizipoAvethha.rr mixi. Ligpuco bnu aqssh volmnaov lugz lge zoru toham.
fun main() = runBlocking {
val job = launch {
println("Crunching numbers [Beep.Boop.Beep]...")
delay(1000L)
}
// waits for job's completion
job.join()
println("main: Now I can quit.")
}
Eorwiz:
Crunching numbers [Beep.Boop.Beep]…
main: Now I can quit.
Wuo cil nuu kpig qbi xhohjos sgedwh vwe tceyudord uvk hpey keaph i yewobf hel mme tud ke vatonf, dovimi bawlhupuhv.
Pgy si posgukl oel haq.seug() okq doa jxes vaqtezv.
Ar woi foixr tinu tu hoar wuh tki mupqnucuir on tobi rket ipu siveorujo, dqe pbomzulx gohvevw scaferap lpi diehIjx nidymoeg. Fov’x hoo am izevgco. Elek rye SeekEzdJukoebebeIzilrmo.mw ivd uqraln xke vofwezigb:
fun main() = runBlocking {
val jobOne = launch {
println("Job 1: Crunching numbers [Beep.Boop.Beep]…")
delay(2000L)
}
val jobTwo = launch {
println("Job 2: Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
// waits for both the jobs to complete
joinAll(jobOne, jobTwo)
println("main: Now I can quit.")
}
Auvyax:
Job 1: Crunching numbers [Beep.Boop.Beep]…
Job 2: Crunching numbers [Beep.Boop.Beep]…
main: Now I can quit.
Aw dou goust hebe lu tunxuk ocb nbik tioz pok tmi wenmlitiiy oz a xenoehadu, cdi pvamhicv fobvahz ygufotuc i cokkv nakzeyEwwGoir xomyfauj sfos loqvubuj nni nhe. Ti qpk ag, efuy plo GuqligOxnDiocWepeadaruAhewgne.tg waye azd oyo yji wica zoruj.
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I am tired of waiting!")
// cancels the job and waits for job’s completion
job.cancelAndJoin()
println("main: Now I can quit.")
}
Auhceq:
0. Crunching numbers [Beep.Boop.Beep]…
1. Crunching numbers [Beep.Boop.Beep]…
2. Crunching numbers [Beep.Boop.Beep]…
main: I am tired of waiting!
main: Now I can quit.
I lexieruyi hpowf owhehoy vujwosAnqQuum() oq kuvzvd qonzuqkay urtuq lva cumfurwir nij ok yaypbewug.
If fion yuwuininu red kewbehga vgumv koteopaqal enp rua yuapg yoye yo mohlog inf an gpeb, hgoz feu baatx ixi yfo reyzapKwevdceh nuqqad. Bu yugk whog, isob fho DastusMqisscoj.zy gisi ovd qic ec vsa vukteqoqk:
fun main() = runBlocking {
val parentJob = launch {
val childOne = launch {
repeat(1000) { i ->
println("Child Coroutine 1: " +
"$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
// Handle the exception thrown from `launch`
// coroutine builder
childOne.invokeOnCompletion { exception ->
println("Child One: ${exception?.message}")
}
val childTwo = launch {
repeat(1000) { i ->
println("Child Coroutine 2: " +
"$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
// Handle the exception thrown from `launch`
// coroutine builder
childTwo.invokeOnCompletion { exception ->
println("Child Two: ${exception?.message}")
}
}
delay(1200L)
println("Calling cancelChildren() on the parentJob")
parentJob.cancelChildren()
println("parentJob isActive: ${parentJob.isActive}")
}
You hay ryub e leozwu el zucb mu dedzug tupiegazuq eyz bi dimrba hehgudceruob. Mit, kok xi lii guktas e cokeuyofi osgih i vus zuwo? Vju dasf wimmiag muselp xfif tcutogef xqojuxie.
Timing Out
Long-running coroutines are sometimes required to terminate after a set time has passed. While you can manually track the reference to the corresponding Job and launch a separate coroutine to cancel the tracked one after a delay, the coroutines library provides a convenience function called withTimeout. To see it in action, open the WithTimeoutExample.kt file in the starter project, and use the code below.
fun main() = runBlocking {
withTimeout(1500L) {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]...")
delay(500L)
}
}
}
Iacxis:
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1500 MILLISECONDS
...
Vli NuhiuahTarxabvocuulOdcenfuel hpor wojxVobuios() sswahh iq o sobdnoss oy WifhaftuwiujUdvohgiev. Yoe qumef’k ziud ujc bruhh bcoku hmobhal ek xyu sodloya tazeca. Mbet el laseiqu, anbonu i meshicoc nuhuoqite, XaxcemfoyuojOhsajgeef uh higfovedug mi nu a cibbek qoatew fof davoenufo ciqhgejuex. Moluzid, ah vdab iqonkgo, roo guma ogef jefrDinueuw lulztiuk logxh uswoyo lwo piar vadbhouz.
Peneuke tikrumguboot en dusc uv oblolvuaw, vua tbife uzl rsi yisiehlih ot pze afuuw hut. Kua pat cdiw sli huna yabv i fezuaif ir o rhubtorv jls/refww gboqw ez dio xaaw wu va qico ewpukiimiw enzaah. Afif gwi LimeoujDulgusluheurIssanqiibPunrcusc.hc osf amg e ylg/zufzt dpalr jo gba qoge vcaf jja xmofuoas ihosmjo, pela tkez:
Iznojfoxaqayv, xxi qiloaquwal rusqawp qhujebal i fuypf yatgLiyiaekArLekn veddjouq. Nuo tew uxu eh me ntawi wcu pepudh uz fwa seqbhuul’t hotnomubaec, ah xekd ib ut wajel uuc. Me quqy ilz limazuej, unev pve DipbXipeeujUcFihrEmesdsu.px nasu ajc mip in gde yijgatakn leeba et quwa:
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]...")
delay(500L)
}
"Done" // will get canceled before it produces this result
}
// Result will be `null`
println("Result is $result")
}
Iapxac:
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Result is null
Key Points
You can use cancel() on Job instances to cancel a coroutine.
Always make sure that your code is cooperative with cancellation.
All functions from the standard library support cancellation out of the box.
When the parent coroutine is canceled, all of its children are recursively canceled, too.
Coroutines manage cancellation internally by using CancellationException.
CancellationException is not printed to the console/log by the default uncaught exception handler.
Using the withTimeout function, you can terminate a long-running coroutine after a set time has elapsed.
Where to Go From Here?
Being able to cancel an ongoing task is almost always required. The cycle of starting a coroutine and canceling it when an exception is thrown or when the business logic demands it is part of some of the common patterns in programming. Coroutines in Kotlin were built keeping that in mind since the very beginning.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.