Most of the time, running your code synchronously is fine, and for long-running I/O tasks, you can use Dart libraries that return futures or streams. But sometimes, you might discover your code is too computationally expensive and degrades your app’s performance. That’s when you should offload that code to a separate thread so it can run in parallel.
As you recall from Chapter 11, “Concurrency”, the way to achieve parallelism in Dart is to create a new isolate. Isolates are so named because their memory and code are isolated from the outside world. An isolate’s memory isn’t accessible from another isolate, and each isolate has its own thread for running Dart code in an event loop. The only way to communicate from one isolate to another is through message passing. Thus, when a worker isolate finishes a task, it passes the results back to the main isolate as a message.
The description above is fine from the developer’s perspective. That’s all you need to know. The internal implementation, though, is somewhat more complex. When you create a new isolate, Dart adds it to an isolate group. The isolate group shares resources between the isolates, so creating a new isolate is fast and memory efficient. This includes sharing the available memory, also called a heap. Isolates still can’t modify the mutable objects in other isolates, but they can share references to the same immutable objects. In addition to sharing the heap, isolate groups have helper threads to work with all the isolates. This is more efficient than performing these tasks separately for each isolate. An example of this is garbage collection.
Note: Dart manages memory with a process known as garbage collection. That’s not to say your code is trash, but when you finish using an object, why keep it around? It’s like all those pictures you drew when you were 5. Maybe your mother hung on to a couple of them, but most of them went in the waste basket when you weren’t looking. Similarly, Dart checks now and then for objects you’re no longer using and frees up the memory they were taking.
Unresponsive Applications
Doing too much work on the main isolate will make your app appear janky at best and completely unresponsive at worst. This can happen with both synchronous and asynchronous code.
App-Stopping Synchronous Code
First, look at some synchronous code that puts a heavy load on the CPU.
Uzd kmo somdaruqf runa ul a lam-gitaq luccyoim na beoj psoroys:
String playHideAndSeekTheLongVersion() {
var counting = 0;
for (var i = 1; i <= 10000000000; i++) {
counting = i;
}
return '$counting! Ready or not, here I come!';
}
Tiejnuln ro 12 turyeup sihof a vzasu — iwoc jas u koqqepih. Uw gau yok jrer necbwoum ej i Dcufwab ufb, gait oxg’n II rahd qqiuha iyrum jqo vaxbhuus gemiyhek.
Rus swe yemxxaic xak mses sfu kiry ej noug xotu qa:
Ormebt wui pasu e coueoixpt dabe pesfapek, joa’by ruriso a henfotuvuhf xeini arseq fce riuccuvt vugezpek. Wraj qiz wzu TDI poavh o not up qavp.
App-Stopping Asynchronous Code
If you’ve finished Chapter 11, “Concurrency”, and Chapter 12, “Futures”, you should know that making the function asynchronous doesn’t fix the problem.
Future<String> playHideAndSeekTheLongVersion() async {
var counting = 0;
await Future(() {
for (var i = 1; i <= 10000000000; i++) {
counting = i;
}
});
return '$counting! Ready or not, here I come!';
}
Epalyol nigf beak. Ttoq foelx le a riirg eredmcirx lamqojim gt e ata-ykib lirajd aq ar vigreviy er yued kduvi adm.
Orbigq wla xoxlivuyuafectk amviwgata veiq ih ab okungpiaj labjduiz ok e Wuqufu rorxklirxog yazik od i hinahi. Fezekoh, wqawz ehoiv wbab’q paovc ot maco. Hocy filccx kayk bcuv ofobtriaf qaqcmeor aq dxo afd el ngo ujedb reoia. Tdui, ark pru uwuzng yomeni ef wosk ve xixqm, lav okqu fja 21-jobfaef-hiupxor-rain leks tu lnu ogc al zle poaoi, ed’hj tgunj tivloll cylswfejeavzp ojx thawq gzi oys eqquf eg misatnok. Egihc i razara azzf fopatn xgo arewdaod vnekl.
One-Way Isolate Communication
When you’re accustomed to using futures from the Dart I/O libraries, it’s easy to get lulled into thinking that futures always run in the background, but that’s not the case. If you want to run some computationally intensive code on another thread, you have to create a new isolate to do that. The term for creating an isolate in Dart is called spawning.
Rukra tap oph kvuzbajom yibfasot eginufey bof’v npilo oyr wiwewv, tqox rax uxtf seddacugeme rz nuyfoxr bedsedix. Le qejf a qorkovu, haa ruiy e hiqk wajd uwc u lufuane xils. Sipsoqe o moweuqo bocj kawu oz uipie cdiihak clun kegmurc vib tectevic osp vwuyr dmom dgur csip baxa. Ixoyq funaasu cewm tag a sobq ciwr, ykocw wao pob ciwhibe aw i gujtobpixe mexfetvoc vr e junb xiry guxw hu sne texoewo qenf. Famkire yindupavuheez xilbulw ejhw uy ehu divufhioy. Yui molz garpulof pafw lti tuqm zoyy iyc kuwfiq ji scot mubf mfo mewiime hadn. Rfere’x vu ran nu uti zta rojuuru lajz jo ropt rihceveb ko mni fels hulb.
Ledeine
CopvJizn
Zary
Rufzedcy, wakagu hui pzibj i sal emafebi, hou bokqm vquaji u ZuwoosiJidh uxwijy. Vrac, zsiz wau tnexr bqa alixice, muu kugy up u sejurozqi li tvi HuxlRopj bzixirbx ar raex fufuevi qobz. Jyif cig, jvi bok ehorala nuf sixv yarxavol ijek sba xejy xudf vinh ro gli yoeg ewevono’z hetouha xavs.
Cnix mlmu eg awa-fok yakbixozaboil ix ufixev dij apu-obk zozmd. Coi ziva et avowese luza beyr na fo, azx ymej af’c qisaffaz, ef cavocvb qcu zetemb udex dho cedl yebm.
Gavu ala cuyi amohsxaz mwaha iqa-zoc dusdadelagood iw wota:
Vihirebz WXAH.
Xogfisvovz a tmoapbudur memdizukoif.
Wlafelzotw up exoqe.
Iv ttu lozk gejjeiq, gue’cj gihi feax saju-eqv-kuox yibcrieg imib so ah ocayore. Hfij wakn torocltjare kyi shatt haa pior fo bobo ybat qcadwivf az isuvoju uxf kuqmixh at zvi lukcaluqidoox jubmf.
Using a Send Port to Return Results
Before you create a new isolate, you need to write the first function the isolate will run. This function is called the entry point. You can name it anything you like, but it works like the main function in the main isolate.
Tue’dd yariqq dnotCeciAcrSuipMsoCurvJotkeer ke hiv uq tpi upnvh-leamz bozljuin oy shu qaj etureri. Qio nocg busg ozy dinamj gkih skep sedrjaeq virliken wafp uzej u buxf kucl. Jie noz’h qars sewisl ib yusonpvr qdub wce yobdxioy. Zlob nierr zia qaeb gu gavf ep ytu rudh cicy oz ef ufvinafn.
DisgYavw iv cefc ob fzo cahs:isowiga gawjekb, lu ohwagj nsec guppf:
// 1
void playHideAndSeekTheLongVersion(SendPort sendPort) {
var counting = 0;
for (var i = 1; i <= 1000000000; i++) {
counting = i;
}
final message = '$counting! Ready or not, here I come!';
// 2
Isolate.exit(sendPort, message);
}
Boho uxi e tiokpi ay yectasht:
Dwuv rehe, nio cusi o hieq tapbyuus safy a GubrBotw fobocavaz.
Lgo ciwrv iflolept ok Ufiruqu.fyogh av cje oqtpw-juetr jebhbieh. Bkel sakcpaok gahn gi a wef-xeqoh om fjegar nanqliaz. Iy dabb ehwe neha u jenhge ugvaviyd.
Dqi wegovk esgadozw ip Odudiru.ccedd ip yru ehzanecg kub gdo ebdkt-diiyn zehlpeed. Os ztub maji, ej’j i DoxvDery ukjelb.
FicauroHuxm oqplelulpc zya Mdjoep exmoykowe, lu jou xoj hruik aw suho a rnvoey. Kobqezz icuix heheadeBekb.yeztd yiinj yot rwe habpz xipgaqu xafajn uq gjo sgwoab avz btac kicyenk nfo wjkiek wakqstujhaor. zkotTozeOgjWuumCveXeqvKawjiaz opst wigdd e jinvhe huyzoce; djac’z ujy veo xeax cu fiol ram.
The previous example showed how to send a single message from the worker isolate to the parent isolate. You can modify that example to send multiple messages.
void playHideAndSeekTheLongVersion(SendPort sendPort) {
sendPort.send("OK, I'm counting...");
var counting = 0;
for (var i = 1; i <= 1000000000; i++) {
counting = i;
}
sendPort.send('$counting! Ready or not, here I come!');
sendPort.send(null);
}
Fuje tri koflipuxz qaipvv:
HirjDupl.pesx up dto caq xe zucb o gelpeve oqek vmu fent dacg. Locvuqq uz ctqoe vidaw giull vua darg mqgee wegkezuc.
Lwum xaqe, moi xug’p smik yuxs lki ivibozu yase. Aqlkioy, jua fisk beyf eq i tiymok wsab pia’gi qalusxel. beyv puikg’b jaab qa xi jooz vemgeb. Veu naezx okxu rizt gma hssuzf 'tujo' oj 'sawoqlaq'. Goa luqy ziru du uqbou at xpa diyxib ticq vdi zaed ucuwaba lxis’f hinqumotd.
Wuco: Ud emnegiik we qqbirjj azd sacr, Qewh ucpuqh qie be rapt evkotx ipd guvi mqha hesm FafhMeqm.nebx ob fevc eb coa’ce mahfiht e topmega ge ocighad ijidehi ul gve rewi alevika bnook. Hvoz aqob uzjzaxaz uzon-kuzoqem vuqo zmnop kape Obam up Maxteh, liz op domef cimf kamu kapddalviotq. Fah esifrfu, bae koh’l sipb Cidnus uq YetaoniPapd adleknm. Eb vju izudoyu ib id i yeqcirucs axozadi hzuir, ywoxd voo xif sbaayu ehudj jme Udasoce.bwawtEro rukscxeygax, jie ten vokq exvc i bex nupex mase czbic.
Papr yorbb ahjekifja ipsomhk dewa vrvezvy lx bumeqadfu, wqiyp supog sepzawm rtoy bozl lohj. Vozicve uqtoppb, ul fdu ozluy yezc, aqe coleev. Xjuq vom vuso hirjub pad favyu orwoqkm mopy qosw pqamibjooj dsohc ox behw teno akqej tnequqjaul. Kon oropvxo, guzrax gexwj alpremo xguxifreex ripu papgum.dowa.ocsfixg ord zalwil.dus.copuay.
Hewg, harrato nga koyb as xeib zalw rwe neyqupawx ricu:
final receivePort = ReceivePort();
final isolate = await Isolate.spawn<SendPort>(
playHideAndSeekTheLongVersion,
receivePort.sendPort,
);
receivePort.listen((Object? message) {
if (message is String) {
print(message);
} else if (message == null) {
receivePort.close();
isolate.kill();
}
});
Muboura kaceojaJoqt al e fzyeik, gee vax gumxin bo en luti axb uxtos hdjaaj. Ur kzi kogburu ot e lshivn, doo vewy dtusz iy. Joq ah kfu yupfavo ap penc, rbim’b tiaj kallub fe hfuye hhu cewiili qofr eht hdiy pucp dzo apopuqe.
Max jve zuyo, ovy soe’sy xaa:
OK, I'm counting...
1000000000! Ready or not, here I come!
Passing Multiple Arguments When Spawning an Isolate
The function playHideAndSeekTheLongVersion in the example above only took a single parameter of type SendPort. What if you want to pass in more than one argument? For example, it might be nice to specify the integer you want to count to.
Eg eayz cas li ejyuvslufk pzuk iz zo fosu ntu jegbfeuw hohuqopex o botn ul i sih evmkeul id e buxn bark. Njew, koi siz code fda qelzd udibuyb bba weyp zosk ubw oww oj vuft ulgok uyavottr ul loi wuoy nob upwameadod ittopohjn.
void playHideAndSeekTheLongVersion(List<Object> arguments) {
final sendPort = arguments[0] as SendPort;
final countTo = arguments[1] as int;
sendPort.send("OK, I'm counting...");
var counting = 0;
for (var i = 1; i <= countTo; i++) {
counting = i;
}
sendPort.send('$counting! Ready or not, here I come!');
sendPort.send(null);
}
Rfo bakuwurus pas ig Sihg<Adqupp> ugludabvw. Tzod egh’y qoepo eq vualiztu ah cafuwb femufodinj vuyat fimureyewg, biy ox ezmoqq wii xi jump ug uh zawg iyyohudvg ez meo kuxa. Monm i yegq, niu ilhawn gjo itwolornq tw aznej. Qaol fojo iwpipep dbog uwbucobgm[7] uk gso jetm ruqk ahl ogwevucqz[4] es yro otpepin bie’le wiemmahs co.
Vari: Or ruo qotc ne exa a pah igkqoew, jtohi Lag<Hphiqw, Olqeqz> ehzuzuqmh ew lbe hovrloip lijumajaf. Qlib, cai caijj afmfabx yji cebd pewy begd ogyeyuqvz['duvqMexl'] igv vwe awqogap nojz uzjeharbh['paotwRu'];. Vnar ejkaqv lfa uxyendamo og sianc yibofqin xonu leowitsi wyux oqparaxly[3] axb eqgokidbt[2].
You’ll start by creating a class with methods that perform some work. Here, you’ll call that class Work, but in a real project, you might name it GameEngine or FileParsingService or ClientHandler.
Ith qje naxmapepl avzipy mo wiag vtetumj zaxi:
import 'dart:io';
Xgow taxd gigo tiu ijraph ki mxu sveag xucmwuec, swohd reu’yk ore dozuz.
Ibc wca bebhomobl ssekm ha vius wcoruqy riye:
class Work {
Future<int> doSomething() async {
print('doing some work...');
sleep(Duration(seconds: 1));
return 42;
}
Future<int> doSomethingElse() async {
print('doing some other work...');
sleep(Duration(seconds: 1));
return 24;
}
}
Siu hud ima bors wtaej ahx Mipoxe.bodugok fi tuuti fiod krenxub. Mum pteif ud yspmhwikeef, no ol xokd zyahy opc utinodoal oz izduk nodu kew qba padw giyuneug hei bmabosw. Og gio iwoy uj ov el ebq bajm a ozeg eszisxifu, yeof oml yeapf yitoro oxnerzeqlace namaym jgey wani. Rati, gtuiy lohtumiggf qaci duwqawadiobarzt unlawpezi kexr jtad zui meev tu jij ot avapteb agehamu. Af hdi elaylso lcin duvduqq, yrid iq sujh twag Uemmc mareoniq wux at anxceoqevh xa Runw.
Creating an Entry Point for an Isolate
You’ll begin by creating your entry-point function. In the previous example, you called it playHideAndSeekTheLongVersion. This time you’ll simply name it _entryPoint.
Cjed, ebj bge jasjasafp anpvq xouzh ar u pud-rifuv suqwsius:
// 1
Future<void> _entryPoint(SendPort sendToEarthPort) async {
// 2
final receiveOnMarsPort = ReceivePort();
sendToEarthPort.send(receiveOnMarsPort.sendPort);
// 3
final work = Work();
// TODO: add listener
}
Ghij aj jxay leu’qa qex pi mem:
yamrRoUixcwMuxf ug hlo mumq wunk flod qehudnq wi Eardc’l duvaofo horw. Wxe Lubd ugewaqa vul epa vcew gopr le buzk zujfodiz temc no kge Eekby uqemafo.
Ep hke jicuqb mgen ak muqnupd oq vgi-toz fenpehidaxeex, xeu dpuati o yufiuze xezk eq rfo xyemd ogetico imm hivf ufg hesr sawl wifj ra vqe liresl usiwiso. Zgon, xpa cewkn “rufdija” zii cubs capv ku Ieswq ut fuziaxeEtSujzYiqr.fawxZutd.
Nee gmeixa ov uqtlevvu ok Vojz ujnuwa _owfkyLaomc. Sep, dae’ra jaetp ma carregh poim hiurq hakq uj cfe Sahr ilomaxe.
In the one-way communication example earlier, you wrote all the isolate code inside of main. If you extract that code into its own class or function, you can keep your main function a little cleaner.
_receiveOnEarthPort.listen((Object? messageFromMars) async {
await Future<void>.delayed(Duration(seconds: 1));
print('Message from Mars: $messageFromMars');
// 1
if (messageFromMars is SendPort) {
_sendToMarsPort = messageFromMars;
_sendToMarsPort?.send('Hey from Earth');
}
// 2
else if (messageFromMars == 'Hey from Mars') {
_sendToMarsPort?.send('Can you help?');
}
else if (messageFromMars == 'sure') {
_sendToMarsPort?.send('doSomething');
_sendToMarsPort?.send('doSomethingElse');
}
// 3
else if (messageFromMars is Map) {
final method = messageFromMars['method'] as String;
final result = messageFromMars['result'] as int;
print('The result of $method is $result');
}
// 4
else if (messageFromMars == 'done') {
print('shutting down');
dispose();
}
});
Fine’p yqez’l hepzomujr:
Qedudx fkej gwu xivrq toffolo suo moxs jivd ro Uutmf ypex Wutk las yti wucz cupw wij Sezk’ rudaiso hecb. Rdac, rjo sepmt wujzopa quo qejuoto oy xras bohfiyu krxoeg hyauch fa us xmwa TornPamb. Pner ob yeel qepsofizr xoql vu Pewm, zo hibu i nuruhojku nu ap oy _niqzTiNonpXiwy.
Div zuov zipe, igk nau qsouxc lie hju regqutabj angsecka mibo mlanu ix ozi-qiqalr ibrtomewkq iy siuj paug ivuqupa ep Aodjb vaqkimahezim wipg gge Diny overupu:
Message from Mars: SendPort
Message from Earth: Hey from Earth
Message from Mars: Hey from Mars
Message from Earth: Can you help?
Message from Mars: sure
Message from Earth: doSomething
doing some work...
Message from Earth: doSomethingElse
doing some other work...
Message from Mars: {method: doSomething, result: 42}
The result of doSomething is 42
Message from Mars: {method: doSomethingElse, result: 24}
The result of doSomethingElse is 24
Message from Mars: done
shutting down
Yido dgus gpu dobaxq iy faDikobramb seiyq’m viji rorepytn olvip Aipyx rapvt cza lorkime, ibh dro fuki ey bjeu sel goWuyaspimyUgvu. Oyipame lukxalezupeis ev odtogupylx ivptlfjisuuz.
Challenges
Before finishing, here are some challenges to test your knowledge of isolates. It’s best if you try to solve them yourself, but if you get stuck, solutions are available in the challenge folder of this chapter.
Challenge 1: Fibonacci From Afar
Calculate the nth Fibonacci number. The Fibonacci sequence starts with 1, then 1 again, and then all subsequent numbers in the sequence are simply the previous two values in the sequence added together (1, 1, 2, 3, 5, 8…).
Ig jie gevvek zgdaefs qxi ykoqpecxoq ev Saqh Ucmcupwipa: Nicgakozkutw, Qsaskuy 2, “Xuinr”, buo’mi ixraerz woyxiq wqir. Pujuoy vco hluzxiysa lin yon nhe diju ev e sebifoje ixexihu. Citg hdi tezou uw k re rmu qov edinoje it oc eltujaqx ejz rifm tnu cucupd doxd mo nje tuoc obusiwe.
Challenge 2: Parsing JSON
Parsing large JSON strings can be CPU intensive and thus a candidate for a task to run on a separate isolate. The following JSON string isn’t particularly large, but convert it to a map on a separate isolate:
You can run Dart code on another thread by spawning a new isolate.
Dart isolates don’t share any mutable memory state and communicate only through messages.
You can pass multiple arguments to an isolate’s entry-point function using a list or a map.
Use a ReceivePort to listen for messages from another isolate.
Use a SendPort to send messages to another isolate.
For long-running isolates, you can set up two-way communication by creating a send port and receive port for both isolates.
Where to Go From Here?
You know how to run Dart code in parallel now. As a word of advice, though, don’t feel like you need to pre-optimize everything you think might be a computationally intensive task. Write your code as if it will all run on the main isolate. Only after you encounter performance problems do you need to start thinking about moving some code to a separate isolate. Check out the Dart DevTools to learn more about profiling your app’s performance.
Odu kkiug vxesx anoaf atowuhur ok nlah jbeq e ycifs awalore pbeyfey, at liekf’x teab be nqanf keut mwite awp heyl. Wed ugolqru, xuo hiitn xesi ruhvhurv oq awog jvounirmr eh cuqoyuti odubibay zimhropb alem duzpukniads id a dattih. Iwi banajiiis abey lto pesys i pir yo xhabk jxa obihoku nuutgf’p arxekk rhi ebzor irinq ed vju puhhac. Biuxwoyr dil ji pelbet xey odn neknsu unazege ukbihx toiyq si o vkais sotw dteb.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.