Concurrency ใน iOS เป็นหัวข้อกว้างมาก ในบทความนี้ผมอยาก Zoom In บน Sub-topic เกี่ยวกับ Queues และ Grand Central Dispatch (GCD) Framework
โดยเฉพาะ ผมอยากสำรวจความแตกต่างระหว่าง Serial และ Concurrent Queues รวมถึงความแตกต่างระหว่าง Synchronous และ Asynchronous Execution
ถ้าคุณไม่เคยใช้ GCD มาก่อน บทความนี้เป็นจุดเริ่มต้นที่ดี ถ้าคุณมีประสบการณ์กับ GCD บ้าง แต่ยังสงสัยเรื่องข้างต้น ผมคิดว่าคุณยังได้ประโยชน์ และหวังว่าคุณจะได้เรียนรู้อะไรใหม่ 1-2 อย่าง
ผมสร้าง SwiftUI Companion App สาธิต Concepts ในบทความนี้แบบ Visual App ยังมี Quiz สนุกสั้นๆ ที่ผมแนะนำให้ลองก่อนและหลังอ่านบทความนี้ Download Source Code ที่นี่ หรือ Public Beta ที่นี่
ผมจะเริ่มด้วย Intro GCD ตามด้วย Explanation เต็มรูปแบบเกี่ยวกับ Sync, Async, Serial และ Concurrent หลังจากนั้นจะครอบคลุม Pitfalls บางอย่างเมื่อทำงานกับ Concurrency สุดท้ายจะปิดด้วย Summary และ General Advice
Introduction
เริ่มกับ Brief Intro GCD และ Dispatch Queues ข้ามไปที่ Section Sync vs Async ได้ถ้าคุณคุ้นเคยแล้ว
Concurrency และ Grand Central Dispatch
Concurrency ให้คุณใช้ประโยชน์จาก CPU Cores หลายตัวของอุปกรณ์ เพื่อใช้ Cores เหล่านี้ คุณต้องใช้ Multiple Threads แต่ Threads เป็น Low-level Tool และจัดการ Threads แบบ Manual ให้ Efficient เป็นเรื่องยากมาก
Grand Central Dispatch ถูกสร้างโดย Apple 10 ปีก่อน เป็น Abstraction ช่วย Developers เขียน Multi-threaded Code โดยไม่ต้องสร้างและจัดการ Threads เอง
ด้วย GCD Apple ใช้ Asynchronous Design Approach แทนที่จะสร้าง Threads โดยตรง คุณใช้ GCD Schedule Work Tasks และ System จะ Perform Tasks ให้คุณด้วยการใช้ Resources อย่างดีที่สุด GCD จัดการสร้าง Threads ที่จำเป็น และ Schedule Tasks บน Threads เหล่านั้น ย้ายภาระ Thread Management จาก Developer ไปที่ System
ข้อได้เปรียบใหญ่ของ GCD คือคุณไม่ต้องกังวล Hardware Resources ขณะเขียน Concurrent Code GCD จัดการ Thread Pool ให้ และ Scale จาก Single-core Apple Watch จนถึง Many-core MacBook Pro
Dispatch Queues
นี่คือ Building Blocks หลักของ GCD ให้คุณ Execute Arbitrary Blocks ของ Code ด้วย Set of Parameters ที่คุณ Define Tasks ใน Dispatch Queues เริ่มต้นเสมอแบบ First-In, First-Out (FIFO) สังเกตผมพูด เริ่มต้น เพราะ Completion Time ของ Tasks ขึ้นอยู่กับหลาย Factors และไม่รับประกัน FIFO (รายละเอียดภายหลัง)
กว้างๆ ไป มี Queues 3 ประเภทให้คุณ:
- Main Dispatch Queue (Serial, Pre-defined)
- Global Queues (Concurrent, Pre-defined)
- Private Queues (Serial หรือ Concurrent, คุณสร้าง)
ทุก App มาพร้อม Main Queue ซึ่งเป็น Serial Queue Execute Tasks บน Main Thread Queue นี้รับผิดชอบวาด UI ของแอปและตอบสนอง User Interactions (Touch, Scroll, Pan, etc.) ถ้าคุณ Block Queue นี้นานเกินไป iOS App จะค้าง และ macOS จะแสดง Beach Ball/Spinning Wheel
เมื่อทำ Long-running Task (Network Call, Computationally Intensive Work ฯลฯ) เราหลีกเลี่ยง Freeze UI โดยทำงานบน Background Queue แล้ว Update UI ด้วยผลลัพธ์บน Main Queue:
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
DispatchQueue.main.async {
// UI work
self.label.text = String(data: data, encoding: .utf8)
}
}
}
Rule of Thumb: UI Work ทั้งหมดต้อง Execute บน Main Queue คุณสามารถเปิด Main Thread Checker Option ใน Xcode เพื่อรับ Warning ทุกครั้งที่ UI Work Execute บน Background Thread
นอกจาก Main Queue ทุก App มาพร้อม Pre-defined Concurrent Queues หลายตัว ที่มี Quality of Service (QoS) ต่างกัน (Abstract Notion ของ Priority ใน GCD)
ตัวอย่าง โค้ด Submit Work Asynchronously ไปที่ Queue User Interactive (Highest Priority QoS):
DispatchQueue.global(qos: .userInteractive).async {
print("We're on a global concurrent queue!")
}
ทางเลือก เรียก Global Queue Default Priority โดยไม่ระบุ QoS:
DispatchQueue.global().async {
print("Generic global queue")
}
เพิ่มเติม คุณสร้าง Private Queues เองได้ด้วย Syntax:
let serial = DispatchQueue(label: "com.besher.serial-queue")
serial.async { print("Private serial queue") }
ตอนสร้าง Private Queues ช่วยถ้าใช้ Descriptive Label (อย่าง Reverse DNS Notation) เพราะช่วย Debug ใน Xcode Navigator, lldb, และ Instruments:
โดย Default Private Queues เป็น Serial (จะอธิบายเร็วๆ นี้ สัญญา!) ถ้าอยากสร้าง Private Concurrent Queue ทำได้ผ่าน Optional Attributes Parameter:
let concurrent = DispatchQueue(label: "com.besher.serial-queue", attributes: .concurrent)
concurrent.sync { print("Private concurrent queue") }
มี Optional QoS Parameter ด้วย Private Queues ที่คุณสร้างจะ Land ใน Global Concurrent Queues หนึ่งในตัวตาม Parameters ที่ให้
Task ประกอบด้วยอะไร?
ผมกล่าวถึง Dispatching Tasks ไป Queues Tasks หมายถึง Code Block ใดๆ ที่คุณ Submit ไป Queue ด้วย Functions sync หรือ async สามารถ Submit เป็น Anonymous Closure:
DispatchQueue.global().async { print("Anonymous closure") }
หรือใน Dispatch Work Item ที่ Perform ภายหลัง:
let item = DispatchWorkItem(qos: .utility) { print("Work item to be executed later") }
ไม่ว่าคุณ Dispatch Synchronously หรือ Asynchronously และเลือก Serial หรือ Concurrent Queue โค้ดทั้งหมดภายใน Single Task จะ Execute บรรทัดต่อบรรทัด Concurrency เกี่ยวข้องเฉพาะตอน Evaluate Multiple Tasks
ตัวอย่าง ถ้าคุณมี 3 Loops ภายใน Same Task Loops เหล่านี้จะ Always Execute In Order:
DispatchQueue.global().async {
for i in 0..<10 { print(i) }
for _ in 0..<10 { print("?") }
for _ in 0..<10 { print("?") }
}
โค้ดนี้ Print 10 ตัวเลข 0-9 ตามด้วย 10 วงกลมสีฟ้า ตามด้วย 10 หัวใจแตก ไม่ว่าคุณจะ Dispatch Closure นั้นอย่างไร
Individual Tasks ก็สามารถมี QoS Level ของตัวเองได้ (โดย Default ใช้ Queue Priority) ความแตกต่างระหว่าง Queue QoS และ Task QoS นำไปสู่ Behavior น่าสนใจที่เราจะ Discuss ใน Priority Inversion Section
ตอนนี้คุณน่าจะสงสัย Serial และ Concurrent คืออะไร และความแตกต่างระหว่าง sync และ async เมื่อ Submit Tasks นี่คือ Heart ของบทความ มาดูต่อ!
Sync vs Async
เมื่อ Dispatch Task ไป Queue คุณเลือกทำ Synchronously หรือ Asynchronously ด้วย Dispatch Functions sync และ async Sync และ Async ส่งผลหลักต่อ Source ของ Submitted Task คือ Queue ที่ Submit From
เมื่อ Code ถึง Statement sync จะ Block Current Queue จนกว่า Task นั้นจะ Complete เมื่อ Task Return/Complete Control จะคืนให้ Caller และ Code หลัง sync Task จะดำเนินต่อ
คิดว่า sync เทียบเท่า 'Blocking'
Statement async ด้านอื่น Execute Asynchronously เทียบกับ Current Queue และ Return Control ให้ Caller ทันที โดยไม่รอให้ Code ภายใน Async Closure Execute ไม่มี Guarantee ว่าจะ Execute เมื่อไหร่
Current Queue?
อาจไม่ชัดเจนว่า Source หรือ Current Queue คืออะไร เพราะไม่ได้ Explicitly Define ใน Code เสมอไป
ตัวอย่าง ถ้าคุณเรียก sync Statement ใน viewDidLoad Current Queue จะเป็น Main Dispatch Queue ถ้าเรียกฟังก์ชันเดียวกันใน URLSession Completion Handler Current Queue จะเป็น Background Queue
กลับไป Sync vs Async ลอง Example นี้:
DispatchQueue.global().sync { print("Inside") }
print("Outside")
// Console Output:
// Inside
// Outside
โค้ดด้านบน Block Current Queue เข้า Closure Execute Code บน Global Queue พิมพ์ "Inside" ก่อน แล้วจึง Print "Outside" Order นี้ Guaranteed
ลอง async แทน:
DispatchQueue.global().async { print("Inside") }
print("Outside")
// Potential Console Output (based on QoS):
// Outside
// Inside
โค้ดตอนนี้ Submit Closure ไป Global Queue แล้วดำเนิน Next Line ทันที จะ น่าจะ Print "Outside" ก่อน "Inside" แต่ Order ไม่ Guaranteed ขึ้นอยู่กับ QoS ของ Source และ Destination Queues และ Factors อื่นๆ ที่ System Control
Threads เป็น Implementation Detail ใน GCD — เราไม่มี Direct Control และจัดการได้แค่ Queue Abstractions แต่ผมคิดว่า 'Peek Under the Covers' ดู Thread Behavior ช่วยเข้าใจ Challenges บางอย่างกับ GCD
ตัวอย่าง เมื่อ Submit Task ด้วย sync GCD Optimize Performance โดย Execute Task บน Current Thread (Caller)
มี Exception หนึ่ง คือ Submit Sync Task ไป Main Queue — จะ Run Task บน Main Thread เสมอ ไม่ใช่ Caller Behavior นี้อาจมี Ramifications ที่เราจะ Explore ใน Priority Inversion Section

ใช้ไหนดี?
ตอน Submit Work ไป Queue Apple แนะนำ Asynchronous Execution มากกว่า Synchronous แต่มี Situations ที่ sync อาจดีกว่า เช่น Race Conditions หรือ Very Small Task จะครอบคลุมเร็วๆ นี้
Consequence ใหญ่ของ Async Work ภายใน Function คือ Function ไม่สามารถ Return Values โดยตรงได้ (ถ้า Depends บน Async Work) ต้องใช้ Closure/Completion Handler Parameter ส่งผลลัพธ์
สาธิต Concept นี้ ดู Function เล็กๆ รับ Image Data ทำ Expensive Computation Process Image แล้ว Return Result:
func processImage(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
// calling an expensive function
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
ใน Example นี้ Function upscaleAndFilter(image:) อาจใช้หลายวินาที ดังนั้น Offload ไป Queue แยกเพื่อหลีกเลี่ยง Freeze UI สร้าง Dedicated Queue สำหรับ Image Processing และ Dispatch Expensive Function Asynchronously:
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
imageProcessingQueue.async {
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
}
มี Issues สองประการ First Return Statement อยู่ใน Async Closure จึงไม่ Return Value ให้ Function processImageAsync(data:) และไม่มี Purpose ปัจจุบัน
แต่ Issue ใหญ่คือ Function processImageAsync(data:) ไม่ Return Value ใดๆ เพราะ Function จบ Body ก่อนเข้ Async Closure
แก้ Error นี้ ปรับ Function ให้ไม่ Return โดยตรง แทนที่จะมี Completion Handler Parameter ใหม่ เรียกเมื่อ Async Function Complete:
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data, completion: @escaping (UIImage?) -> Void) {
guard let image = UIImage(data: data) else {
completion(nil)
return
}
imageProcessingQueue.async {
let processedImage = self.upscaleAndFilter(image: image)
completion(processedImage)
}
}
เห็นได้ว่า การเปลี่ยนเป็น Asynchronous Propagate ไปยัง Caller ซึ่งต้อง Pass Closure และ Handle Results แบบ Asynchronous ด้วย With Introduction ของ Async Task คุณอาจ Modify Chain ของ Several Functions
Concurrency และ Asynchronous Execution เพิ่ม Complexity ใน Project ตามที่สังเกต Indirection นี้ทำให้ Debugging ยากขึ้น ดังนั้น Worth Off ที่จะ Think About Concurrency Early ใน Design Cycle — ไม่ใช่สิ่งที่ Tack On ท้าย Design Cycle
Synchronous Execution ตรงข้าม ไม่เพิ่ม Complexity มันให้คุณใช้ Return Statements เหมือนเดิม Function ที่มี sync Task จะไม่ Return จนกว่า Code ใน Task จะ Complete ดังนั้นไม่ต้อง Completion Handler
ถ้า Submit Tiny Task (เช่น Update Value) พิจารณาทำ Synchronously ไม่เพียงแค่ช่วย Code เรียบง่าย จะ Perform ดีกว่า — Async เชื่อว่ามี Overhead ที่ Outweigh Benefit สำหรับ Tiny Tasks ที่ใช้ Under 1ms
ถ้า Submit Large Task อย่าง Image Processing ข้างต้น พิจารณา Asynchronously เพื่อหลีกเลี่ยง Block Caller นาน
Dispatching บน Queue เดียวกัน
Safe ที่จะ Dispatch Task Asynchronously จาก Queue เข้า Queue ตัวมันเอง (เช่น ใช้ .asyncAfter บน Current Queue) แต่คุณ ไม่สามารถ Dispatch Task Synchronously จาก Queue เข้า Queue เดียวกัน ได้ ทำจะเกิด Deadlock Crash App ทันที!
Issue นี้ Manifest ตอน Chain ของ Synchronous Calls นำกลับไป Queue ต้นทาง นั่นคือ sync Task ไป Queue อื่น และเมื่อ Task Complete มัน Sync Results กลับ Original Queue นำไป Deadlock ใช้ async หลีกเลี่ยง Crash
Blocking Main Queue
Dispatching Tasks Synchronously From Main Queue จะ Block Queue นั้น Freeze UI จนกว่า Task Complete ดังนั้นหลีกเลี่ยง Dispatch Work Synchronously จาก Main Queue เว้นแต่ Light Work จริงๆ

Serial vs Concurrent
Serial และ Concurrent ส่งผลต่อ Destination — Queue ที่ Work ถูก Submit ไป Run นี่ Contrast กับ Sync และ Async ที่ส่งผลต่อ Source
Serial Queue จะไม่ Execute Work บนมากกว่า One Thread At a Time ไม่ว่า Dispatch Tasks กี่ Tasks บน Queue นั้น Tasks รับประกันว่าไม่แค่ Start แต่ Terminate แบบ FIFO
เพิ่มเติม เมื่อ Block Serial Queue (ด้วย sync Call, Semaphore, หรือ Tool อื่น) Work ทั้งหมดบน Queue จะ Halt จนกว่า Block จบ

Concurrent Queue สามารถ Spawn Multiple Threads System Decide จำนวน Threads Tasks เริ่มต้น FIFO Order แต่ Queue ไม่ Wait Tasks Finish ก่อน Start Next Task ดังนั้น Tasks บน Concurrent Queues สามารถ Finish In Any Order
เมื่อ Perform Blocking Command บน Concurrent Queue จะไม่ Block Other Threads บน Queue นั้น เพิ่มเติม Concurrent Queue ที่ Blocked มี Risk ของ Thread Explosion จะครอบคลุมรายละเอียดภายหลัง

Main Queue ใน App เป็น Serial Global Pre-defined Queues ทั้งหมดเป็น Concurrent Private Dispatch Queue ที่คุณสร้าง เป็น Serial By Default แต่ Set เป็น Concurrent ได้ผ่าน Optional Attribute ตามที่อธิบาย
สำคัญต้อง Note ว่า Concept Serial vs Concurrent Relevant เฉพาะตอน Discuss Specific Queue All Queues เป็น Concurrent Relative To Each Other
นั่นคือ ถ้า Dispatch Work Asynchronously จาก Main Queue ไป Private Serial Queue Work นั้น Complete Concurrently เทียบกับ Main Queue และถ้าสร้าง Two Different Serial Queues แล้ว Perform Blocking Work บนหนึ่ง Queue อีก Queue ไม่ Affected
สาธิต Concurrency ของ Multiple Serial Queues ดู Example:
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.async { for _ in 0..<5 { print("?") } }
serial2.async { for _ in 0..<5 { print("?") } }
ทั้งสอง Queues เป็น Serial แต่ Results จumbled เพราะ Execute Concurrently เทียบกัน Fact ว่าแต่ละอัน Serial (หรือ Concurrent) ไม่ Effect Result QoS Level Decides ใครจะ Generally Finish First (Order Not Guaranteed)
ถ้าอยาก Ensure First Loop Finish ก่อน Start Second Loop Submit First Task Synchronously จาก Caller:
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.sync { // <---- changed to 'sync'
for _ in 0..<5 { print("?") }
}
// don't get here until first loop terminates
serial2.async { for _ in 0..<5 { print("?") } }
นี่ไม่ Necessarily Desirable เพราะเรา Block Caller ขณะ First Loop Execute
เพื่อหลีกเลี่ยง Block Caller เรา Submit Both Tasks Asynchronously แต่ไป Same Serial Queue:
let serial = DispatchQueue(label: "com.besher.serial")
serial.async { for _ in 0..<5 { print("?") } }
serial.async { for _ in 0..<5 { print("?") } }
ตอนนี้ Tasks Execute Concurrently เทียบกับ Caller พร้อมกันรักษา Order Intact
Note ถ้าเราทำ Single Queue Concurrent ผ่าน Optional Parameter กลับไป Jumbled Results ตามคาด:
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.async { for _ in 0..<5 { print("?") } }
concurrent.async { for _ in 0..<5 { print("?") } }
บางครั้งคุณอาจสับสน Synchronous Execution กับ Serial Execution (อย่างน้อยผมเคย) แต่พวกมันต่างกันมาก Example ลองเปลี่ยน First Dispatch บรรทัด 3 จาก Example ก่อนหน้าเป็น sync Call:
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.sync { for _ in 0..<5 { print("?") } }
concurrent.async { for _ in 0..<5 { print("?") } }
ทันใดนั้น Results กลับมาอยู่ใน Order สมบูรณ์ แต่ Queue นี้ Concurrent จะเกิดได้ยังไง? sync Statement โหลมันเป็น Serial Queue?
คำตอบ ไม่!
นี่ Sneaky บิท สิ่งเกิดคือเราไม่ถึง async Call จนกว่า First Task Complete Execute Queue ยัง Concurrent มาก แต่ภายใน Zoomed-in Section ของ Code มัน Appear ถ้าเป็น Serial เพราะเรา Block Caller และไม่ Proceed Next Task จน First Task Finished
ถ้า Queue อื่นที่อื่นใน App Submit Work ไป Queue เดียวกันขณะมันยัง Execute sync Statement Work นั้น Will Run Concurrently กับสิ่งที่เรา Run ที่นี่ เพราะมันยัง Concurrent Queue
ใช้ไหนดี?
Serial Queues ใช้ประโยชน์จาก CPU Optimizations และ Caching ช่วย Reduce Context Switching
Apple แนะนำ Start With One Serial Queue Per Subsystem ใน App — เช่น One สำหรับ Networking, One สำหรับ File Compression ฯลฯ ถ้าจำเป็น ขยายเป็น Hierarchy ของ Queues Per Subsystem ด้วย SetTarget Method หรือ Optional Target Parameter ตอน Build Queues
ถ้าเจอ Performance Bottleneck Measure App Performance แล้วดู Concurrent Queue ช่วยไหม ถ้าไม่見 Measurable Benefit ดีกว่าที่จะ Stick กับ Serial Queues
Pitfalls
Priority Inversion และ Quality of Service
Priority Inversion คือ High Priority Task ถูก Prevent จาก Running โดย Lower Priority Task กลับค่า Relative Priorities
Situation นี้เกิดบ่อยเมื่อ High QoS Queue Share Resource กับ Low QoS Queue และ Low QoS Queue ได้ Lock บน Resource นั้น
แต่ผมอยาก Cover Scenario อื่นที่ Relevant กับ Discussion — คือ Submit Tasks ไป Low QoS Serial Queue แล้ว Submit High QoS Task ไป Queue เดียวกัน Scenario นี้ก็ Priority Inversion เพราะ High QoS Task ต้อง Wait Low QoS Tasks Finish
GCD Resolve Priority Inversion โดย Temporarily Raise QoS ของ Queue ที่มี Low Priority Tasks 'Ahead Of' หรือ Blocking High Priority Task
Clue เหมือนมีรถติด หน้า รถ Ambulance ทันใดนั้น Allowed Cross Red Light เพื่อ Ambulance Move (ในความเป็นจริงรถขยับไปข้าง แต่จินตนาการ Narrow (Serial) Street บ้าง ได้ Point :-P)
Illustrate Inversion Problem เริ่มจาก Code นี้:
enum Color: String {
case blue = "?"
case white = "⚪️"
}
func output(color: Color, times: Int) {
for _ in 1...times { print(color.rawValue) }
}
let starterQueue = DispatchQueue(label: "com.besher.starter", qos: .userInteractive)
let utilityQueue = DispatchQueue(label: "com.besher.utility", qos: .utility)
let backgroundQueue = DispatchQueue(label: "com.besher.background", qos: .background)
let count = 10
starterQueue.async {
backgroundQueue.async { output(color: .white, times: count) }
backgroundQueue.async { output(color: .white, times: count) }
utilityQueue.async { output(color: .blue, times: count) }
utilityQueue.async { output(color: .blue, times: count) }
// next statement goes here
}
เราสร้าง Starter Queue (Submit Tasks From) และ Two Queues QoS ต่างกัน จากนั้น Dispatch Tasks ไปแต่ละ Queue แต่ละ Task Print Circles จำนวนเท่ากัน สีเฉพาะ (Utility Queue เป็น Blue, Background เป็น White)
เพราะ Tasks Submit Asynchronously ทุกครั้ง Run App คุณจะเห็น Results แตกต่างเล็กน้อย แต่ตามคาด Queue ที่ QoS ต่ำกว่า (Background) จบท้ายเกือบเสมอ จริงๆ 10-15 Circles ท้ายมักเป็น White ทั้งหมด

แต่วันที่ Submit Sync Task ไป Background Queue หลัง Async Statement สุดท้าย คุณไม่ต้อง Print อะไรใน sync Statement เพียงเพิ่มบรรทัดนี้พอ:
// add this after the last async statement,
// still inside starterQueue.async
backgroundQueue.sync {}

Results ใน Console กลับกลับ! ตอนนี้ Higher Priority Queue (Utility) จบท้ายเสมอ และ 10-15 Circles ท้ายเป็น Blue
เข้าใจทำไม ต้อง Revisit Fact ว่า Synchronous Work Executed บน Caller Thread (เว้นแต่ Submit ไป Main Queue)
ใน Example ข้างต้น Caller (StarterQueue) มี Top QoS (UserInteractive) ดังนั้น sync Task ที่ดูไร้ความเสียหาย ไม่แค่ Block Starter Queue แต่ Run บน High QoS Thread ของ Starter Task นั้น Run ด้วย High QoS แต่มี Two Other Tasks หน้ามันบน Background Queue เดียวกันที่มี Background QoS Priority Inversion Detected!
ตามคาด GCD Resolve Inversion โดย Raise QoS ทั้ง Queue ให้ Match High QoS Task ชั่วคราว ผลคือ Tasks ทั้งหมดบน Background Queue Run ที่ User Interactive QoS ซึ่งสูงกว่า Utility QoS นั่นคือเหตุผล Utility Tasks จบท้าย!
Side-note: ถ้าลบ Starter Queue ออกจาก Example และ Submit จาก Main Queue แทน คุณจะได้ Results คล้ายกัน เพราะ Main Queue ก็มี User Interactive QoS
เพื่อหลีกเลี่ยง Priority Inversion ใน Example นี้ ต้องหลีกเลี่ยง Block Starter Queue ด้วย sync Statement ใช้ async แก้ปัญหา
แม้ไม่ Ideal เสมอไป คุณ Minimize Priority Inversions ได้โดย Stick to Default QoS ตอนสร้าง Private Queues หรือ Dispatch ไป Global Concurrent Queue
Thread Explosion
ใช้ Concurrent Queue คุณ Risk Thread Explosion ถ้าไม่ระวัง เกิดขึ้นตอน Submit Tasks ไป Concurrent Queue ที่ Blocked อยู่ (เช่น Semaphore, Sync, หรือทางอื่น) Tasks Will Run แต่ System Likely Spin Up New Threads Accommodate New Tasks และ Threads ไม่ Cheap
นี่ Likely ทำไม Apple Suggest Start With Serial Queue Per Subsystem ใน App เพราะแต่ละ Serial Queue ใช้ได้ One Thread จำได้ว่า Serial Queues Concurrent ใน Relation To Other Queues ดังนั้นยังได้ Performance Benefit Offload Work ไป Queue แม้ไม่ Concurrent
Race Conditions
Swift Arrays, Dictionaries, Structs และ Value Types อื่นๆ ไม่ Thread-Safe By Default ตัวอย่าง Multiple Threads พยายาม Access และ Modify Same Array คุณจะเจอ Trouble
มี Solutions ต่างๆ สำหรับ Readers-Writers Problem เช่น Locks หรือ Semaphores แต่ Relevant Solution ที่ผมอยาก Discuss คือ Isolation Queue
สมมติเรามี Array of Integers และ Submit Asynchronous Work Reference Array นี้ ถ้า Work ของเรา Read Array เท่านั้นไม่ Modify เรา Safe แต่ทันทีที่ Modify Array ใน Async Task ใดๆ เรา Introduce Instability ใน App
Tricky Problem เพราะ App Run 10 ครั้ง No Issues แล้ว Crash ครั้งที่ 11 Tool หนึ่ง Handy สำหรับ Situation นี้คือ Thread Sanitizer ใน Xcode Enable Option นี้ช่วย Identify Potential Race Conditions

สาธิต Problem ดู Example (Admittedly Contrived):
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
var array = [1,2,3,4,5]
override func viewDidLoad() {
for _ in 0...1 { race() }
}
func race() {
concurrent.async {
for i in self.array {
// read access
print(i)
}
}
concurrent.async {
for i in 0..<10 {
self.array.append(i) // write access
}
}
}
}
หนึ่งใน async Tasks Modify Array โดย Append Values ถ้า Run บน Simulator อาจไม่ Crash แต่ Run พอสมควร (หรือเพิ่ม Loop Frequency บรรทัด 7) จะ Crash ท้ายที่สุด ถ้า Enable Thread Sanitizer จะได้ Warning ทุกครั้ง Run App
จัดการ Race Condition นี้ เราเพิ่ม Isolation Queue ใช้ Barrier Flag Flag นี้ Allow Outstanding Tasks บน Queue Finish แต่ Block Further Tasks Execute จน Barrier Task Complete
คิด Barrier เหมือน Janitor ทำความสะอาด Public Restroom (Shared Resource) มี Multiple (Concurrent) Stalls ภายใน ที่คนใช้ได้
Upon Arrival Janitor วาง Cleaning Sign (Barrier) Block Newcomers Enter จน Cleaning Done แต Janitor ไม่ Start Clean จนคนภายใน Finish Business ทั้งหมด Once They Leave Janitor Clean ใน Isolation
When Done Janitor Remove Sign (Barrier) ให้คนคิวภายนอก Enter ได้
Code ดูเป็นแบบนี้:
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
let isolation = DispatchQueue(label: "com.besher.isolation", attributes: .concurrent)
private var _array = [1,2,3,4,5]
var threadSafeArray: [Int] {
get {
return isolation.sync { _array }
}
set {
isolation.async(flags: .barrier) {
self._array = newValue
}
}
}
override func viewDidLoad() {
for _ in 0...15 { race() }
}
func race() {
concurrent.async {
for i in self.threadSafeArray { print(i) }
}
concurrent.async {
for i in 0..<10 { self.threadSafeArray.append(i) }
}
}
}
เราเพิ่ม Isolation Queue ใหม่ และ Restrict Access ไป Private Array ด้วย Getter และ Setter ที่วาง Barrier ตอน Modify Array
Getter ต้องเป็น sync เพื่อ Return Value โดยตรง Setter เป็น async ได้ เพราะไม่ต้อง Block Caller ขณะ Write
เราอาจใช้ Serial Queue โดยไม่มี Barrier แก้ Race Condition แต่จะเสีย Advantage ของ Concurrent Read Access ไป Array อาจทำ Sense ใน Case ของคุณ คุณ Decide
Conclusion
ขอบคุณมากที่อ่านจนจบ! หวังว่าคุณเรียนรู้อะไรใหม่ จากบทความนี้ ผมจะทิ้ง Summary และ General Advice:
Summary
- Queues เสมอ Start Tasks แบบ FIFO
- Queues เสมอ Concurrent Relative To Other Queues
- Sync vs Async เกี่ยวกับ Source
- Serial vs Concurrent เกี่ยวกับ Destination
- Sync เทียบเท่า 'Blocking'
- Async Return Control ให้ Caller ทันที
- Serial ใช้ Single Thread และ Guarantee Execution Order
- Concurrent ใช้ Multiple Threads และ Risk Thread Explosion
- Think About Concurrency Early ใน Design Cycle
- Synchronous Code เข้าใจง่ายและ Debug ง่าย
- Avoid Rely บน Global Concurrent Queues ถ้าเป็นไปได้
- Consider Start With Serial Queue Per Subsystem
- Switch ไป Concurrent Queue ก็ต่อเมื่อเห็น Measurable Performance Benefit
ผมชอบ Metaphor จาก Swift Concurrency Manifesto ว่า 'Island of Serialization in a Sea of Concurrency' Sentiment นี้ Share ใน Tweet โดย Matt Diephouse:
The secret to writing concurrent code is to make most of it serial. Restrict concurrency to a small, outer layer. (Serial core, concurrent shell.)
— Matt Diephouse (@mdiep) December 18, 2019
e.g. instead of using a lock to manage 5 properties, create a new type that wraps them and use a single property inside the lock.
ตอน Apply Concurrency ด้วย Philosophy นี้ ผมคิดว่าจะช่วยให้ Concurrent Code ที่ Reason About ได้โดยไม่ Lost ใน Mess ของ Callbacks
ถ้ามี Questions หรือ Comments อย่าลังเล Reach Out ผมบน Twitter
Besher Al Maleh
Cover photo by Onur K on Unsplash
Download the companion app here:
almaleh/Dispatcher Companion app to my article on concurrency. Contribute to almaleh/Dispatcher development by creating an account on GitHub.
almaleh GitHub 
Check out some of my other articles:
Fireworks — A visual particles editor for Swift Generate Swift code on the fly for macOS and iOS as you design and iterate on your particle effects
Besher Al Maleh Flawless iOS
You don't (always) need [weak self] In this article, we'll talk about weak self inside of Swift closures to avoid retain cycles & explore cases where it may or may not be necessary to capture self weakly.
Besher Al Maleh Flawless iOS 
Further reading:
Introduction Explains how to implement concurrent code paths in an application. Concurrent Programming: APIs and Challenges · objc.io objc.io publishes books on advanced techniques and practices for iOS and OS X development
Florian Kugler
Low-Level Concurrency APIs · objc.io objc.io publishes books on advanced techniques and practices for iOS and OS X development
Daniel Eggert
https://khanlou.com/2016/04/the-GCD-handbook/
Concurrent vs serial queues in GCD I'm struggling to fully understand the concurrent and serial queues in GCD. I have some issues and hoping someone can answer me clearly and at the point. I'm reading that serial queues are created...
Bogdan Alexandru Stack Overflow 
WWDC Videos:
Modernizing Grand Central Dispatch Usage - WWDC 2017 - Videos - Apple Developer macOS 10.13 and iOS 11 have reinvented how Grand Central Dispatch and the Darwin kernel collaborate, enabling your applications to run...
Apple Developer
Building Responsive and Efficient Apps with GCD - WWDC 2015 - Videos - Apple Developer watchOS and iOS Multitasking place increased demands on your application's efficiency and responsiveness. With expert guidance from the...
Apple Developer 