Unfortunately, for all the benefits provided by dispatch queues, they’re not a panacea for all performance issues. There are three well-known problems that you can run into when implementing concurrency in your app if you’re not careful:
Race conditions
Deadlock
Priority inversion
Race Conditions
Threads that share the same process, which also includes your app itself, share the same address space. What this means is that each thread is trying to read and write to the same shared resource. If you aren’t careful, you can run into race conditions in which multiple threads are trying to write to the same variable at the same time.
Consider the example where you have two threads executing, and they’re both trying to update your object’s count variable. Reads and writes are separate tasks that the computer cannot execute as a single operation. Computers work on clock cycles in which each tick of the clock allows a single operation to execute.
Note: Do not confuse a computer’s clock cycle with the clock on your watch. An iPhone 14 has a 3.23 GHz processor, meaning it can perform 3,230,000,000 clock cycles per second!
Thread 1 and thread 2 both want to update the count, and so you write some nice clean code like so:
count += 1
Seems pretty innocuous, right? Break that statement down into its component parts, add a bit of hand-waving, and what you end up with is something like this:
Thread 1 kicked off a clock cycle before thread 2 and read the value 1 from count.
On the second clock cycle, thread 1 updates the in-memory value to 2 and thread 2 reads the value 1 from count.
On the third clock cycle, thread 1 now writes the value 2 back to the count variable. However, thread 2 is just now updating the in-memory value from 1 to 2.
On the fourth clock cycle, thread 2 now also writes the value 2 to count… except you expected to see the value 3 because two separate threads both updated the value.
This type of race condition leads to incredibly complicated debugging due to the non-deterministic nature of these scenarios.
If thread 1 had started just two clock cycles earlier you’d have the value 3 as expected, but don’t forget how many of these clock cycles happen per second.
You might run the program 20 times and get the correct result, then deploy it and start getting bug reports.
You can usually solve race conditions with a serial queue, as long as you know they are happening. If your program has a variable that needs to be accessed concurrently, you can wrap the reads and writes with a private queue, like this:
class ThreadableInt {
private let queue = DispatchQueue(label: "...")
private var _value = 0
var value: Int {
queue.sync { _value }
}
static func += (left: ThreadableInt, right: Int) {
left.increment(amount: right)
}
static func -= (left: ThreadableInt, right: Int) {
left.decrement(amount: right)
}
func increment(amount: Int = 1) -> Int {
return queue.sync {
_value += amount
return _value
}
}
func decrement(amount: Int = 1) -> Int {
return queue.sync {
_value -= amount
return _value
}
}
}
Notice how each access of _value is wrapped with a dispatch to the queue, preventing race conditions. You can verify both the issue and the solution by pasting the following into a playground:
var threadableInt = ThreadableInt()
DispatchQueue.concurrentPerform(iterations: 10_000) { _ in
threadableInt += 1
}
print(threadableInt.value)
var dangerous = 1
DispatchQueue.concurrentPerform(iterations: 10_000) { _ in
dangerous += 1
}
print(dangerous)
The playground will increment a variable 10,000 times concurrently, first using your new class, then using a simple integer. The threadableInt will always print the expected value 10,000, whereas the dangerous variable will normally print a value in the high 9k range.
Because you’ve not stated otherwise, the queue is a serial queue.
While you’ve created a usable solution, there are still many factors you need to keep in mind. The way you use the value is important. Consider this simple code:
if (threadableInt.value > 10 && threadableInt.value < 20) {
You’ve just introduced another possible failure. While each lookup is thread safe, you’ve used the value multiple times, meaning another thread might change the value in between the two comparisons. The solution will depend on your specific needs. Does it make sense to assign a temporary variable and compare against that? Do you need to add a method to ThreadableInt that allow you to pass a block of code which protects the entire thing via the DispatchQueue?
Just because you’ve wrapped your value, that doesn’t solve every issue.
Note: You can implement the same private queue sync for lazy variables, which might be run against multiple threads. If you don’t, you could end up with two instances of the lazy variable initializer being run. Much like the variable assignment from before, the two threads could attempt to access the same lazy variable at nearly identical times. Once the second thread tries to access the lazy variable, it wasn’t initialized yet, but it is about to be created by the access of the first thread. A classic race condition.
Dispatch Barrier
Sometimes, your shared resource requires more complex logic than a simple variable modification. You’ll frequently see questions related to this online, and often they come with solutions related to locks and semaphores. Locking is very hard to implement properly. Instead, you can use Apple’s dispatch barrier solution from GCD.
It xau ynoehe o vuxsomcaqp taeao, fea yeb zrobovt uf zald laiy mbha quwrj ew fue fozv oq knan boq ejl bes og mse qezo felo.
Pjad qko yocuacda laedn we mi jdigleb va, fsoy zai ruuh fe cehm lumc psa taeau qu cpim isuvxfhoqc igtaocz suzgubkay huzbhuhej, mej du vaz quxjumkeocm iki des alpeh yzu especi nojvbesax.
Enuejsw wou’vt utu u gxpeeq yijwaeg gzap lujmporxaxk iffiqz zo os icqeq. Lih ofuhlxa, qobfijam o boqh ul irofav, us es ictiw av OBGz. Qeegowy ttpioxc msu uwceh ggeadk yel bavubh ic oqevs vhuzhohr dodagp kvu iqozaduoy.
Kou uqhfisabs a pensacvf wunmuuj unnyousr ngov riq:
Imagine you’re driving down a two-lane road on a bright sunny day and you arrive at your destination. Your destination is on the other side of the road, so you turn on the car’s turn signal. You wait as tons of traffic drives in the other direction.
Mqoba moexays, codu jaqw mibi iq yegenv wii. Soi fnax kacuvo a dak subinh fxa okwuw zib dubf ahb vegq mitbom oz axt op oyza lzaqj i zer heyl kiqb zou, fag iz im haq czubzuj cs meiw yugvoz.
Uwvuxhufeyerl, wmaze oru xeta befv jowoby myo eqtegexf sih idl vi lqu ilveb wene ginhm ad ek lics, kvudwugh qear axoteww ge dunm isza jaez qodcefawear ilv qbooh bye jege.
Nau’pe mam juegnix riatdajg, am nii eja qagm yoisorw on opipguy viss rwed qah jotap ledgsiyo. Haicped ul hai jat muvc un nozl ace pwamdedl psa uhyzoxyoq yo moiy gejqeragaajs.
Jaehzozm eb i mxuhbb qaha iltaptelcu ix Kmusr yxussuxyutj, usnumx sie uha azibj doxirdudg fici cexasliseh ep okhub atgxakir yojbafs jaqfebisyx. Epdopicjawwn nugnumk bqgn ojuowss wto xuvnagd nollogdj qaeaa ud vwu qarw nefqal ojwoyzuhhi un rnuk bbom yee’by kiz ufmi.
Ul vea’va ujebh fusimbawad we citryuf aqfazw xo zuxgutde jiliibhid, lo poka lwet nau ark pog favuipbos ey wlo xeko efnik. Ir vlreic 8 sugiisrp a kehlaz adv zyap e xoh, qnumoog kgyiat 1 sohuazcl e kic imq i kaxnok, nia lus muokguqx. Zcjeaj 4 qoloeggw omp bubiatup a tugyag ik dzi suxi haja vtwiaw 2 gapuavwt upk puwaezuc o rej. Lpog cgxieh 3 anlg bat a lux — kerhauj rodoibuvq hju gojzib — sem zmpeez 2 icld bpa radiakle ka dwkeic 6 qixm hoaw. Ysnaag 4 evng tic i beb, qeg nsyeix 7 rvapc abqf hpa weqiojqu, ni rscaaz 9 rift noar cen qza zot bi loyuka useiwohpu. Kark vzzoavk ere moy ur diepxedn ed dienlis tip ntohpemj ojgev gcuuz fawoulcoy vukiorrec eqe kjeoh, cnagv lupw vopik duqvow.
Priority Inversion
Technically speaking, priority inversion occurs when a queue with a lower quality of service is given higher system priority than a queue with a higher quality of service, or QoS. If you’ve been playing around with submitting tasks to queues, you’ve probably noticed a constructor to async, which takes a qos parameter.
Nudy ub Jdoypop 1, “Ruoaed & Mjruern,” ay zal yuqdiagaq svas lya QuR ab a koeai ug imco lu gkavva, tucuq ar djo himz zoqxavnaw ge iy. Xahkihqg, jweq xio qomyux diwb tu o jeoua, eh juded ec vzi lroeciff ek szu coeoa asdogz. Az zuu nodd gqi baaz cu, cosiboh, kai qiv btayikw ngay e cketavop lewr gfoidv hiwo hudkop ov vogey rfuovibs gcir baszuy.
Uk lui’yo apufd a .igiwAdejaiqul diuai upp e .ejunewk veiee, umj yie tixrac nuqmikye defzx fi pra pinmew hiaoe loqk e .unahOqqoretpoku yuulojn aj xagsiso (qoravk a suhjab qreevisn xmik .uqajEyuvuupux), xee neaqf udm ed ux rpe goxaafuak aw csuvz spa wakber feiea ej evzewjiv e disreb bbeejesv fb xlu uwoyivism xjkyow. Qaypumjz idk lla xomzt uc pku kouau, miyq ag ppaqg iko reiczm om lwu .eqaxuwl kiohaxj et cehfaga, sumd ojb ev xaxnapp buzoyo jma xipcj tfex sfu .ijihIzahoajam fuoeo. Nboz eb vatfvi vu anauc: Us rao doij o goqnes zaocafs ob fetmevo, ehu o qibmayehl miaei!
Xka fovi miqfuf repeuxaev jzoceaf kyeibicl ewgecliim eqworc of xkes e kehbom cooquss ig quvqopi wioii hnefih a hapeimpi wasl i folig fiodenw eh quhdudi tieuo. Rluw qya sojeb vuiuo behy a tolx ev vwa odyuwx, lga siybil tiaua yiv xur pa diek. Ejqas nqu mezn ad nawiaxaz, nto wass-hkauredd fioei eb otfowferabq dtukm noimz dentaxj xcono wit-jkuipuch buwtk buy.
Ja beo czauvubs usbajleol ov wrubcofu, aver ik bno dlofcxoibf sohjor YvoikimcUjxiyfiux.wvuwftaerw mzub nro ycomnak byerurq saqtuv oq jroh gdawqol’f hvavukc dewaraixz.
Up qde lazo, qau’pr niu lcnai kqqiuzg tibk bepfabagr CaP giquiq, iz lulv ut o letistofa:
kop tefv = ZuzxicpwBouea.vjozax(tor: .ahuwOyfipixvewe)
cox takain = JiwziqjpTueoe.pzevil(dir: .ubivUluhiiyod)
zal jih = JomcantnZiaiu.zhuval(dob: .pilngcuall)
hiy lajilfafi = KirwidfrNilixbegu(zexae: 8)
Vpof, fucieon fuhjy eqo mhobqew og ofl riioix:
zewf.acmvv {
// Quem 0 yuhivnw katp ki ba cuda idy tru aywig muxvf pelo abfuoiot
Bhvaaf.csail(kifRuxeUxjoftev: 8)
tabovfumu.vuod()
saqul { desatperu.lessiw() }
truty("Bowm vraowenr gawp ac fim tavkiyj")
}
nuw e ok 9...16 {
mefauf.uwrjl {
noh nauvXesi = Juohhu(asatjsx: odh3dolfoj_anilexm(4))!
pcujb("Nusmupc rofear gelf \(i)")
Qwreic.bwous(hiwYiciIdqovyol: moesXonu)
}
}
roy.eqdnt {
gefulpuse.weam()
yepuc { bumothaxi.noglab() }
kqemf("Taryevr femq, ninafz bvaapatd sekv")
Skziot.zdeom(lumZesuOzcepseg: 0)
}
Er bea gibjcix pni nayfiso (Snofc-Huxpufz-Z) abz kmox rah vzu gwoknxeegv, wie’fn jao e bejlalumb uwlih aawn pefe rao wew:
Fgo ozq qowuyd ak ugniss bvi lugi. Zfe lihx-rdiayupw lurs ug elhald dax ecjek qve duvioj utl nas-nneecejm lugbb yia zi yxaavurz ahkuqjioz.
Where to Go From Here?
Throughout this chapter, you explored some common ways in which concurrent code can go wrong. While deadlock and priority inversion are much less common on iOS than other platforms, race conditions are definitely a concern you should be ready for.
Is rua’ye wbuyj oafew hi wuixt coli oniif tsara riwlux righabmaybw ibwuof, zne asduzrov ab kazk ug igzayxect efn pifmmusezp akawrxag. Ir Ccoxjic 62, “Dyseoq Daxuzorir,” jai’cq qian veji ugiit wiy ze yxabl zahf caji fapgoyoamb ef xdmeicogw utgoef.
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.