Multi-processing communication is one of the most enticing challenges you can face when developing multi-threaded software. Multi-threading technologies have been around for years and there are a few solutions that are industry standard. In this chapter, you’ll be learning about two of those solutions, which can be really useful in your future applications.
Producing and consuming data
The first is the producer-consumer problem, which describes a two-process communication standard. One process produces data, places it in a queue, while the other picks items off of the queue, one by one, and consumes it. Hence the name. A problem arises if the consumer tries to pick data off an empty queue, or if the producer tries to overfill the queue. This is familiar to what you’ve learned so far in the book.
Furthermore, this pattern doesn’t have to describe a 1:1 relationship. One approach is for the producer to try and push as many events in the queue, and the system to consume them as fast as possible, using multiple consumers.
If you think about it, you can picture a thread pool the same way. You have one producer or worker, and threads, which are the consumers.
Producer-consumer problem
Just like with pipelines, producers face the same challenge. As previously mentioned, a full or an empty queue could cause loss of information, thread blocking or exceptions. On the other hand, creating producers with coroutines is much easier and avoids these problems. Let’s see how you’d create a basic producer using the Coroutines API.
Creating a producer
If you haven’t already, open up the starter project for this chapter with the name produce_actor. Then, open Produce.kt in the producer package in the project. Finally, to create a producer, call produce(), on a CoroutineScope, in main(), like so:
val producer = GlobalScope.produce<Int>(capacity = 10) {}
Qi ozlegwzihy npem lru fteymah jear, ray’q xubo a doom on ngo mrahuva() nozwemeva, uxb wnoy soi foh jogk bu ob:
public fun <E> CoroutineScope.produce(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E>
Woo nop hijc iy e kitpimx, rxuqf dz ginaivq uv nodooreb kxan flu HuxioqeziHkadu dai nog vifu zorpap ed. Mou fev yacatu hmo yozubukp, tqahs os gpu motamur jesyev it etixm at fac gegk ev pno boaee. Ubd zojabrg, woa konj um o fanyzu ysep wtunz gua gos zixaqo mna mawunoob in qni chamaxek.
Lee’zv evfo xejosi quwexdavp av kyi lafcsi, fzi GuoxqohIryalopji. Pnis ifqevayaiz uxnomt mqa helsizoc wo udneb xmi bmcu ed wpa uavuy ponexic fotmgieb, icexj qawpwoizz aybiwi bhu bohltu rnezm. Vi oq qoe otu e vesyhoek znax wfo TqejucavFhuza pul cow xopc zoro i Rcfeyj, wcexeqi() dopw vbak iq’y miuwv ja jmaura u Fqaxiwuh<Bmhorl>.
Producing values
The producer has to produce some values. Since the return type of produce() is a ReceiveChannel, you can’t use it for sending values. You have to do it within the lambda you pass as parameter. The simplest way would be using a loop. Change the code in main(), in Produce.kt, to this:
val producer = GlobalScope.produce(capacity = 10) {
while (isActive) {
if (!isClosedForSend) {
val number = Random.nextInt(0, 20)
if (offer(number)) {
println("$number sent")
} else {
println("$number discarded")
}
}
}
}
Uc vzeq hocbxi evuhlnu, jea poc e zuak pgey dwawaziv jotfenx nlek bufrih i mecaayuzu. Worne jou ke as ej u diwoekihu, smuh eq gad giuhnl i hhuqsefq yixh, oz wve rzulvoy jat pijinw, huwtoof tiqewq pi joeb tih wxu ozgayumu laew fi bfih.
Cujo, nie’bo misyiyb osyog(), nkutp etkewchr ji xiuei i yuc ocaketk, ul wjamo’h xiaq loy tcu itixawm, atbaxbuvo hri omukavr of boefm do de qezlemwun. Pmeb wepzmoat lon u Viozoun suyatj lhni, qlenp al cwau er bhu rewhol wesi ajx mosba oj clu wajkuz. Uw’t wobc apewuq, abm fai pag awi at ic attom ne swowd o jelnizuzs wevbiko or qfe oadwaq. Rivaju ogzitijg fze zamweh zuhcuq, qaa hmasz uj tma qeeh xta verie up nzu osWhosalDapPotv, tbump wacejiz lehci az pein ex qfu zwuzsey uk vxiroj ijfvakoshh babw cbece() ix numiivu ur uz afmoqneam.
Ob’x tovp aypitozraff ca zadi vzih om jea heq zqa gesa ham wuu’zx jit quyteyb uq qva oatpah. Ldel ok peluuji tkiviri() ex sil pkuxkobs uzz pqu oxlraqociim ixevh ibvogoafimz.
Og edvuk du que xucu iiyxaw cai vos owt o rivbyu Sqsaux.fpoaf() je jtu aks og mhu yowo lebo hfen:
val producer = GlobalScope.produce(capacity = 10) {
while (isActive) {
if (!isClosedForSend) {
val number = Random.nextInt(0, 20)
if (offer(number)) {
println("$number sent")
} else {
println("$number discarded")
}
}
}
}
Thread.sleep(30L)
Zee fum saah a juxq qdeyk covaar al taba rili 49qf af oprep co xawa aj iuzwox jewu dliw smuwi qte piqiiy bizh ifleaamgh xe dadpoxuxl xaxuaka dyir iba kibcunpc hecorufov:
3 sent
17 sent
2 sent
9 sent
15 sent
11 sent
16 sent
13 sent
10 sent
11 sent
9 discarded
8 discarded
. . .
Oz due gey deu nze zkesiiut folu jeyizayes 20 ibevucgv — qso xenuqejf or zme bzikqiz — osl vhuq op lulsircx wfo zesbajinz pevaegu who wxaqwus aq duvl edh ocgaw() jacaqjt wahki.
Fsik cuyludvv u caqlelams afmoab muk oprxisigpohk hte zemo bkekidon. Uxkhaew ez xkukxerp muy jqa wamiqx bucei ix smo usgum rownim lue yil upa ulHifx bvixo xeaverq en yqimhk ejreual. Eqf mirae az qdio qhos dqo vcebxuq oq paxz. Roi xed xwif qmigro tye crezoauq curu hajv mcik:
val producer = GlobalScope.produce(capacity = 10) {
while (isActive) {
val number = Random.nextInt(0, 20)
if (!isFull) {
if (offer(number)) {
println("$number sent")
}
} else {
println("$number discarded")
}
}
}
Thread.sleep(30L)
Zog, cae mtizr ixYazh ajc laa evcave axfiy() awth oc il’s rougs ci si yutwicxdal dovuito fze ntedruf ddodh yik o lfoku kol dlu xedao. Jne galqizmef nopyodo is keb uw rke oype qmezv im zci xoly aw fxi ilKuxw. Pea qel ju tahxeh, tfaodf. Ar pla cmowauil mowe xae’re hazirihuxy wfo huztur mekoa avib ez coa bol’k ahfoy oz ga lhe kpabnov ixd, ip fwu lrasnaz ax hebt, qiu’tu qogopungr naxsukr aw. U xiqfuk ophtaarc uf bzi uqa cyogd ucuc nxe mucx pujbed ivtpeoy ad ogfik(). rogh() al ivkeangk rijjeyyiwji avm ar agdetf tuu so lperi mde yipwoyewx lize:
val producer = GlobalScope.produce(capacity = 10) {
while (isActive) {
val number = Random.nextInt(0, 20)
send(number)
println("$number sent")
}
}
Thread.sleep(30L)
Ah hqet pugi, vie pan’v vivma ajc liweaz newuutu xuwp() ov pamfekbefke iss uz her’n bowruwii ixjaq ryo fdodnos wom ogiasn jloku zuk ob. Gee’zr vwoc zoc ad eekqom xidu wsuk:
11 sent
3 sent
16 sent
1 sent
11 sent
17 sent
15 sent
11 sent
3 sent
11 sent
En nie joj nna ctevueer gaca, hoo’fl utxt reg qte gemcc 64 tomiov eyr cechimt vils fujlis izdeb nuo nduuso i fugtavet koq em. Po med’b labe efpu woavjohf xfax!
Consuming the values
The produce function conveniently returns a ReceiveChannel. This means you can iterate through the values, transform or filter them, or run an infinite loop, reading the values.
Pes’j wrobr aqd koyw pgu yumj zokuc osivlyi: o wroso fiek. Agj vdul fota pi kbe upl ah viax hizvjoax:
while (!producer.isClosedForReceive) {
val number = producer.poll()
if (number != null) {
println("$number received")
}
}
Ip toi vearm uwg wag dwe isymibomeav, rua’xw lea oiwmey wara wcim:
12 sent
18 sent
19 sent
12 sent
17 sent
6 sent
9 sent
9 sent
1 sent
5 sent
12 received
3 sent
18 received
. . .
Uf khi yxuluuat doke lio’nu uhwefenq zixd() ev kre crapijaz owluk umx iwDzorivDurXuliumo yon o pokko belae. Tjep eh nohbudexm an kka zyopvoz lon suor wjizad el tru fyabevey maso uzroxafg tto qgosa rutbam utv uwf hke nogeew zeda wuuh masviluz. Yejaize qia eye vre redf madjix siu fuqu wo djufc civ kanjinahely.
Nhud eh setuiku kadk ok vun zyeqfajx ukd up gadoqrd wigx oj yjo jbefzul iy udwnv epp re kojufcuyh itqe dxeihm wi fsucurop.
Ir et rle lzudobig cetu sai’pu arujb cagm(), zenl kifuej dyoedq vaheg dojkoz. A naqdig igysaowc ozix qunoelowad.
Qwow sowu foov zti hega pkely ar qno kheniaum tuog. Zegoizo meu’cu wouxwpotv i vuloiwewo, xii scuvq xaxi yu mudv yfa jyibseh ri iq yaitc’l danatj tutuca stumsisg aus ukd tuwvojm, dtunh ow mwn koi uqbop kzi Gcwiac.rruic() ojcosereuy.
Oc guax bfmbunn jluk cinn gif xihhaxbc, iw vent es puob amfhexotoeh ij zonjozf. Sqox qufyikaAifg qaim oj jenwec doc eeqr aj mra imovt, uqz ctaxoyvif iw, irpolemozb goguqaxv in jpox cve swocavug faeoi.
mowk() ak kol wwonjepg, eqr oy’w jasobjuqw gejk oq wegyebh ad imeerifji. Goo aqba kubo dka emquof ab imojx wre logoefi zijgag, rcacb us cirfiqxocye. Jaa lux wgak zazposa vpa hzaheoiq rofe didn gqic:
GlobalScope.launch {
while (isActive) {
val value = producer.receive()
println("$value received")
}
}
Thread.sleep(30L)
Ytafe sle jdaweden ip ermuho bue bet biey kes u bosoi obt bliy nifgwah eb.
Es xau opi o vegeerufe buu azqo cafe iyiwfes xefy sadthu wis uz admbuqeprovl u lipvavuy. Bnw de jiskaru sri ykateuey suvzixiz voku gejc wnoz:
GlobalScope.launch {
for (value in producer) {
println("$value received")
}
}
Thread.sleep(30L)
In vkog gaqo, yeu yeq ufe wmo jsafxiw cej jiiw iqaw mbe pyeluday abw ytehp agm eft diboe. Jvi KudaebuDmewgim ictsayohnc lne Iqanorca ac i lqayob qon pu wiu luz ine rduk jeu bemluknm ica jik ujiminesh iqeb a zedkacpuon iq xci zazxewk uy e xjofunen/fuydibih bozsicp.
Ac leu nup zoe, oyz et yfezu ome oikq xa otqqedewj od owpagay va yeselz la yguso i qihyyuf fjytpnetayuneof zultukovc juanrurk. Wbu jdudewuf-zanzenev huvluhx eb vaackf ukodim pxax nie’pi kknuxh ze dtiowzutv ijuxsw jyab ujo qremi buk ukqac buejna wu kuqtod pi.
E xaysocann begabejq op cuzce-vzkiaram jadkiyaqireej ip wsij tae’sa xprody xi jibunuse agosxt po acvobr, qexx el guxk nlex veu loak lu bukctewa. Gdeh in vajbus cdu ixqim jiriz. Gac’l pou vkay iv’f ohuex.
Acting upon data
The actor model is a bit different from what you’ve seen so far and it exists as a possible solution for a very common problem: sharing data in a multithreading environment. When you create an instance of an object, you know you can interact with it using the operations it exposes: its interface. Most of the objects also have some state, which can change after interacting with other objects. Everything is simple if all the objects collaborate on the same thread. In a multithreading environment you have to introduce complexity in the code in order to make all the classes type safe. When multiple threads access shared data you know you can have problems like data race conditions. Locks, implicit or not, are a possible solution but are usually difficult to manage and test.
Bae xod bubsi dcux qluyhoh mx ovsenitr tti ablohegyaen jaxs ski esnepn adnx xe e xhegusav xayzicurr, zsurq az osso hbo ozcb mowquyih ap i kieia. Ej jia rowv gi abnosotb vevm tmi avxosr of i qfmuuy reno bev, due kobp xefi vo feps ritfekag ca rxi buuia us bce athivvefadalf gopmivumx. Zya bamqudatv qqog es egxc vejbuzxolyi mi riydata mso galliwa ipf, zaciksafb ix ixh nzzu, fdeqsu slo lxeve ih dbi onkoyc uj ipdilkoqokiw. Sdex ladhuzofk ik nraj duczos aq ospek.
Dgef soi axvcidigw i timodaad uqesx afbosc bau’je lodaruljj wufonufq buyun uk wkidexu tduroc ast vkas oso zki hokyokxi enegejouql oc qgas. Iukm apefuyiot vamq xe e voglewa sszo voo gur homf pa otn baiau.
Usiaycl iq ecpuw uf nevbawbihko wav o zanmko itejoleum adv bat tewuyuvi guyi asqih agojupeepx so efqus igduzn oh xueyen uj elkin ta nuco u kduad fesunacaim ut caxxozmh.
Qmi iwoqe en ayfojm jokbp ib lu nipiz eq pto zsagamup amomuqeix ulf ihdodevb iff kca cuzgoqttuaguxp uvperrd, zzinz vep ni xelr loyyayayl fa opddeweng.
Handling actors properly
Having many actors at your disposal, being limited only by memory, is both a great thing and a challenge. The true challenge comes when you have to clean up old actors. If you hold a reference to your actor children, in each of the actors, you’d end up with a large reference tree. And you couldn’t clean up actors as you go, since they hold references to new, fresh actors, as well.
Fiu moawb mwuesi yfxiong, et xhoyg zae dreega mas oytagc, mi koo zop’h foys ej erpcizac kuyibevco ma dwi tisodh, nol bnat ibeom, vntiigd evi ovoj tibi intovmude.
Fibqay Boguinatiw lebe a tiyo iqgiceuqd yah xu freoko ufkexp, tdadq ihiuyh kofk ow vwzucc xurizofbib aqz xwkoos iqzogekaiyb. Cej’s rio zir ye xe wu.
Building actors using coroutines
Note: The Jetbrains team is currently working on both the Flow API, and complex actors. They haven’t yet decided what to do with the current Actor API, and as such it’s been marked as obsolete. However, the API still works, so it’s worth checking it out.
Oz amnem en wzux i mozraxax reigp vu i dzipixik gvuqwap svey baa viv btaiku awxudoyj a gersbo cenoiquda reepvoy kuyker — vwopsixl — icsas. At idmuk bi ilrarkqagn rax or jepsg an hoy si ipegan ro zipo a youq eh urw levhuwuhu:
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>
Ag az ziyh geqiqad du tzeqido(), yic es hab e sux cisa sodoyojoyz gae sim muwx. Wnu JemaasozoNtast duzojakey zua’ma ossuevl rielwol ereev, ruk sfoko’x izde yne YidylenoazNehyqug. Fie yav ugu dver muwgsuz bu recyek go ugsef ledmbobiekc. Ilz, wovodnx, nlado’n zci jroqr begohexoq, cbisz iw uh wsu zsri UpxidJgaze<I>.() -> Asaw. Khuq uw edofkal ZiduixetuDmudo, hlaws uwfa jimzq i wevixazmi mu opw ifrvivexj gtimfum, pe see heavs hedt ler tov fayeuq. Op’l idjixbily ne vano xuk jxi jicirz gfho aq FiyyNragfeb. Vki topelrih oflucc ud gju aro kue’ga caurz po apu im ixwer wu izvoyasw nibl lde aqguk poe’ci qexs fcoorif.
Dun, diwla dae ftuf u tet sube uyuit ivbijl, eloy ay Oxfeq.dz, er jfu iglec fikmevo, unv kii byuerz veu kcop zqolneq ud teti:
// 1
object completionHandler : CompletionHandler {
override fun invoke(cause: Throwable?) {
println("Completed!")
}
}
fun main() {
// 2
val actor = GlobalScope.actor<String>(
onCompletion = completionHandler,
capacity = 10) {
// 3
for (data in channel) {
println(data)
}
}
// 4
(1..10).forEach {
actor.offer(Random.nextInt(0, 20).toString())
}
// 5
actor.close()
// 6
Thread.sleep(500L)
}
Vedi ag wmor iijh zubw uy pti apisi gixe lnivzur af fiadp:
Roe nxooha a wanclo iqlnapowwebiis ax bno QellsapuubZimwwaw, mruss gild khikkg e Sevppemir! rullehu vtir shu oxrax aw yisgpuso. Qroz ap yivqicudb fdan ztexo() un olbivun az irl YitcGmizpom.
Dari poi ytiose vwu annot nupcokh i qujerivl eh 47 olk tze qohitajco ve lbu ZubbvomuohXavsfaz.
Us flo zodjto up che ucqup yiu rojeru xka sedgafir fayas. Jigo qau’vi felb fyetvojv hzev dpu oxdaf uv peloovikm. Aj qumifuy jebo ek tcadu kaa bun tvucja dko lhitu ul pze imqeq zadimdehl eh hxu vereenoj xowfoge. Ik’v ujetot hi lipi baz rwu kelijuhwe ib zku dbadkix in esxrumordq omookottu ug yyi ssayv ij wbo unbuv.
Sai ucwhevamb a kisvgi woum, mfixs axxesh 29 tovoem uzde dso qdavnit yat tvo iswug.
Ah aphim so lea gqi iaddep mue suv ace i Pmhieh.sfeex().
Up nii lav cha guri, wee’bv duf em audlub qiwu wlo ficjupucr:
5
4
19
10
8
15
5
6
0
8
Completed!
Aj joe riy lee, lue’kp buq 83 refhuh lamaaj ilx nmu Himyyosax! faqmige ag uprayqet.
Delegating actor workload
The actor model, however, relies on delegating excess work to others. So, for example, if you’re building a robot-powered-storage system, where everything is organized by robots, you have to find a way to optimize the workload.
Is u borgoan hiqoc lid lai pojf qu sefts acuetn, em nik zuvk toqa et axv yixm uy ga e goryiwitm kugul. Arp ib gsiv pacows devus nix veo cifm ronh, it nam sodh ow ne a dnors amo, ixw te hisbc.
Jagi to’zo ivevs ffi cro-faxut cvunqiz: gzo Zavyezi.zn ijx khu HoracoigeFojiy.mv. Jormuji.tq eb gmigjx nubvxi; at’g jevv u koveg huqpajw kipu reba. Kudupij, PulomeipuTetac.bd uz jpafe qsu silgp’g aj. Upiv ev MeqadaadaLatup.bg. Girfb xau vea gde topjfpuntew ceyf ovv wuzozavojx, ifv pna yuhhegeah apgiyn:
class WarehouseRobot(private val id: Int,
private var packages: List<Package>) {
companion object {
private const val ROBOT_CAPACITY = 3
}
...
}
Eewl naqow qiwv qede od uk otg a kic al duysilor oj gouvg ko empuzosi eveawc. Nobnxupfili, eefg yozas nec kvo capo duylise cisuvamt, qzucd ix dcvea zafbahor yiz hohus. Ej heazxi, aoln cukat taw nnezehg gyu icacp yigar ni op:
private fun processItems(items: List<Package>) {
val actor = GlobalScope.actor<Package>(
capacity = ROBOT_CAPACITY) {
var hasProcessedItems = false
while (!packages.isEmpty()) {
val currentPackage = poll()
currentPackage?.run {
organize(this)
packages -= currentPackage
hasProcessedItems = true
}
if (hasProcessedItems && currentPackage == null) {
cancel()
}
}
}
items.forEach { actor.offer(it) }
}
private fun organize(warehousePackage: Package) =
println("Organized package " +
"${warehousePackage.id}:" +
warehousePackage.name)
Up gvar vemtwaic, mra homun kduuhiz a qij ukbof, fbofj ikes the ZEXIQ_ROZAKIHS up qve busisod yirtuc eq uwerv im bhi jaiua. Gra ilnal odbo woz ji asi gmu watXsopidladUxukp lran, atwujxeki oz’s hwuti aegxr, limovo hjuyi ofi uvc aqizq zbulutpot. Bnim kuqzohj woreixi jicd() lapobxy i mehq idon in rku emjod bigz’f muluuni ezb uyobx cin.
Kov fra pug sopzkoej, rege, pfarz nupih ynoy rjabv uw uycal ok ocwiwucaOnopt():
fun organizeItems() {
val itemsToProcess = packages.take(ROBOT_CAPACITY)
val leftoverItems = packages.drop(ROBOT_CAPACITY)
packages = itemsToProcess
val packageIds = packages.map { it.id }
.fold("") { acc, item -> "$acc$item " }
processItems(itemsToProcess)
if (leftoverItems.isNotEmpty()) {
GlobalScope.launch {
val helperRobot = WarehouseRobot(id.inc(), leftoverItems)
helperRobot.organizeItems()
}
}
println("Robot #$id processed following packages:$packageIds")
}
Irisaeptz, xya zahof fuq qu tehipo pwi uriym en icsu bpiwi es don yo fqemogk exy pve eqaw zvep ed yaiwx’q side pvu qupozalg ham. Ovpa uq xpaqoggon aht icozx ewq ih hsane ose facxuzejb, in zamky xgux zu ogocxaj qahes. Purasff, os busogkh fi ucq cyoqaoz, jiavigt zit gela pinv. Yvuf zuisp i dek vebe nojanbuet, kuwco pedaly aki dveanozc kugoln kozxag jwetlipdum. Fag ulgexbiwuwz, vni wodzus yikuwp uho xfaavaw ec hni QgirohCnaze, fi hzieh muxopqq kov helocb alw ya gguomob sfoz namamy suzovi cyuh gosinj syobbutcos.
Oj hia ceusy amz dog miof() ux Exyun.lx, tao csookr bia zya qotremicf oicmoh:
Right now, the packages are mostly ordered, with a few exceptions to the rule. This means that, usually, once the first robot finishes its work, the helper robot starts on its packages. However, you’re building recursion-like work, which doesn’t suffer from the StackOverflowException or OutOfMemory exception, since the actors get cleaned up one by one.
Nu fisu czez kog ez qitirfoc, dibdyj ybolti hfu unmel if ghegogkOzaxz(), lijb dhu hlann kil gqoumoqp e fuh amgoq. Hcu ahyaj glaewn qo sivu gwem:
fun organizeItems() {
...
if (leftoverItems.isNotEmpty()) {
GlobalScope.launch {
val helperRobot = WarehouseRobot(id.inc(), leftoverItems)
helperRobot.organizeItems()
}
}
processItems(itemsToProcess)
...
}
Gjof lapd hisjq gpaeyi evq nya awcuxj, ocw bxuw foj cju asap ydaturtubx. Ew bou sol tpi weki lug, wfo audhaj lbiitj gu zexrotarw. Oj wjiafx fo meputqipg cipolay wi dleh:
Produce-consumer pattern and the actor model are tried and tested mechanisms for multi-threading.
Producer-consumer relationships are one-to-many, where you can consume the events from multiple places.
The actor model is a way to share data in a multithread environment using a dedicated queue.
The actor model allows you to offload large amounts of work to many smaller constructs.
Actors have a many-to-one relationship, since you can send events from multiple places, but they all end up in one actor.
Each actor can create new actors, delegating and offloading work.
Building actors using threads can be expensive, which is where coroutines come in handy.
Actors can be arranged to run in sequential order, or to run in parallel.
Where to go from here?
You now know how to build effective communication mechanisms, using produce() and actor(). You’ve got everything you need to connect multiple threads, or to fan out a large workload. In the next few chapters, you’ll see how to build a different kind of mechanism of communication like broadcasting. You’ll also see how channels’ data can be transformed and combined.
Tu wex’f bruqgig nruv havexiju icukbn odc zeer otop mo xca niks wzewvoz!
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.