ทุกแอปพลิเคชันมือถือทำงานหลายอย่างพร้อมกัน: โหลดข้อมูลจากเครือข่าย ประมวลผลการสัมผัสของผู้ใช้ ทำให้อินเทอร์เฟซเคลื่อนไหว และบันทึกไฟล์ หากโค้ดทั้งหมดนี้ทำงานในเธร็ดเดียว แอปพลิเคชันจะค้างเมื่อมีความล่าช้าทางเครือข่าย มัลติเธร็ด (multithreading) และการทำงานพร้อมกันเป็นแนวคิดสำคัญที่ช่วยให้แอปพลิเคชันตอบสนองและมีประสิทธิภาพ ในบทความนี้เราจะกล่าวถึงเครื่องมือหลักทั้งหมด: ตั้งแต่ Main Thread และ RunLoop ไปจนถึง Coroutines ใน Kotlin และ Combine บน iOS เนื้อหาอ้างอิงจากเอกสารทางการของ Apple GCD
ประเด็นสำคัญ
มัลติเธร็ดคือความสามารถของแอปพลิเคชันในการดำเนินการโค้ดหลายส่วนพร้อมกัน แต่ละส่วนทำงานใน เธร็ด (Thread) ที่แยกจากกัน — กระบวนการน้ำหนักเบาที่มีสแต็กการเรียกของตัวเอง ในการพัฒนามือถือ เธร็ดแบ่งออกเป็นสองประเภท: Main Thread (เธร็ด UI) และ Background Threads (เธร็ดพื้นหลัง)
ระบบปฏิบัติการจัดการการกระจายเธร็ดไปยังแกนประมวลผลด้วยตัวเอง อุปกรณ์สมัยใหม่มี 6–8 แกน ดังนั้นการดำเนินการแบบขนานสามารถเร่งความเร็วงานได้จริง อย่างไรก็ตาม การสร้างเธร็ดเป็นการดำเนินการที่แพง ดังนั้นจึงไม่แนะนำให้ทำงานกับ Thread โดยตรง แต่จะใช้สิ่งที่เป็นนามธรรมระดับสูงกว่าแทน: DispatchQueue, OperationQueue, CoroutineDispatcher
การทำงานพร้อมกัน (Concurrency) เป็นแนวคิดที่กว้างกว่ามัลติเธร็ด การทำงานพร้อมกันหมายความว่างานสามารถดำเนินการ "พร้อมกัน" ได้แม้บนแกนเดียวผ่านการสลับบริบท อะซิงโครนัส (Async/Await) เป็นโมเดลการเขียนโปรแกรมที่งานไม่ได้บล็อกเธร็ด แต่คืนการควบคุมในขณะที่รอผลลัพธ์ ภาษาสมัยใหม่ (Kotlin, Swift, Dart) มีการสนับสนุน Async/Await ในตัว
ที่ IT Sectr เราให้ความสำคัญเป็นพิเศษกับสถาปัตยกรรมมัลติเธร็ดที่ถูกต้องตั้งแต่เริ่มต้นโครงการ ข้อผิดพลาดในระยะแรกนำไปสู่บักที่จับได้ยาก: การแข่งข้อมูล การตายเบา และความไม่เสถียรของแอปพลิเคชันภายใต้โหลด ทุกโครงการของเราผ่านการตรวจสอบสถาปัตยกรรมการทำงานพร้อมกันในขั้นตอนการวางแผน
Main Thread (เธร็ดหลัก) — เธร็ดเดียวในแอปพลิเคชันมือถือที่สามารถเข้าถึง UI บน Android เรียกว่า UI Thread บน iOS — Main Thread การดำเนินการอินเทอร์เฟซทั้งหมด — การเปลี่ยนข้อความ อนิเมชัน การประมวลผลการสัมผัส — ดำเนินการบน Main Thread เท่านั้น หากดำเนินการหนัก (การโหลดไฟล์, การแยกวิเคราะห์ JSON) บนเธร็ดหลัก อินเทอร์เฟซจะหยุดตอบสนอง บน Android สิ่งนี้นำไปสู่ ANR (Application Not Responding) บน iOS — หน้าจอ "ค้าง"
Background Threads (เธร็ดพื้นหลัง) มีไว้สำหรับทุกอย่างที่ไม่เกี่ยวข้องกับ UI: คำขอเครือข่าย การดำเนินการฐานข้อมูล การประมวลผลภาพ การเข้ารหัส หลังจากเสร็จสิ้น ผลลัพธ์จะถูกส่งไปยัง Main Thread เพื่อแสดงผล แต่ละแพลตฟอร์มมีเครื่องมือของตัวเองสำหรับสลับระหว่างเธร็ด: DispatchQueue.main.async ใน iOS, runOnUiThread หรือ withContext(Dispatchers.Main) ใน Android
RunLoop — วงรอบการประมวลผลเหตุการณ์บนเธร็ดหลักของ iOS RunLoop รอเหตุการณ์ (การสัมผัส ตัวจับเวลา การแจ้งเตือน) และจัดส่งไปยังตัวจัดการที่เหมาะสม บน Android สิ่งที่เทียบเท่าคือ Looper ที่เชื่อมโยงกับแต่ละ Main Thread Main Looper ดึงข้อความจากคิวอย่างไม่สิ้นสุดและส่งไปยัง Handler เพื่อประมวลผล การเข้าใจ RunLoop และ Looper ช่วยหลีกเลี่ยงการรั่วไหลของหน่วยความจำและการ "กระตุก" ของอินเทอร์เฟซ
Grand Central Dispatch (GCD) — ไลบรารีของ Apple สำหรับจัดการมัลติเธร็ดในระดับภาษา C GCD ทำงานกับ DispatchQueue — คิวงาน นักพัฒนาไม่ได้สร้างเธร็ดด้วยตนเอง GCD จัดการพูลเธร็ด (Thread Pool) โดยกระจายงานไปยังแกนประมวลผลที่มีอยู่ DispatchQueue มีสองประเภท: Serial Queue (คิวแบบเรียงลำดับ — งานดำเนินการทีละรายการ) และ Concurrent Queue (คิวพร้อมกัน — งานสามารถดำเนินการพร้อมกันได้)
Main DispatchQueue — คิวแบบเรียงลำดับที่ผูกกับเธร็ดหลัก Global Queues — คิวพร้อมกันที่มีลำดับความสำคัญต่างกัน (QoS — Quality of Service): userInteractive, userInitiated, utility, background การเลือก QoS ที่ถูกต้องสำคัญต่อประสิทธิภาพ: .userInteractive — สำหรับงานที่ส่งผลต่อ UI (อนิเมชัน, การเรนเดอร์); .background — สำหรับงานที่ไม่สำคัญด้านเวลา (การซิงโครไนซ์, การล้างแคช)
OperationQueue — สิ่งที่เป็นนามธรรมเหนือ GCD ที่มีความสามารถเพิ่มเติม: การยกเลิกงาน, การตั้งค่าการพึ่งพาระหว่างการดำเนินการ, การควบคุมจำนวนสูงสุดของการดำเนินการพร้อมกัน การดำเนินการเป็นออบเจกต์ของคลาส Operation (หรือ BlockOperation) ตัวอย่าง: หากคุณต้องโหลดรูปภาพ จากนั้นใช้ฟิลเตอร์ และหลังจากนั้นจึงแสดง — OperationQueue ที่มีการพึ่งพาจัดการได้อย่างสมบูรณ์แบบ ใน GCD คุณต้องซิงโครไนซ์ขั้นตอนเหล่านี้ด้วยตนเองโดยใช้ DispatchGroup หรือเซมาฟอร์
Async/Await ใน Swift 5.5+ — ทางเลือกสมัยใหม่ของ GCD คำสำคัญ async และ await ทำให้โค้ดอะซิงโครนัสเป็นเชิงเส้นและอ่านง่าย ฟังก์ชันถูกทำเครื่องหมายเป็น async และการเรียกจะรอผ่าน await ระบบจัดการการสลับบริบทด้วยตัวเอง: โดยค่าเริ่มต้น ฟังก์ชัน async ทำงานบนเธร็ดพื้นหลัง ในขณะที่การอัปเดต UI ทำงานบน MainActor @MainActor — คุณลักษณะที่รับประกันการดำเนินการโค้ดบนเธร็ดหลัก
Coroutines (โคโรทีน) — เธร็ดน้ำหนักเบาสำหรับ Kotlin ที่พัฒนาโดย JetBrains ต่างจากเธร็ดทั่วไป โคโรทีนไม่ได้ผูกกับ Thread เฉพาะ โคโรทีนหลายพันตัวสามารถทำงานบนหลายเธร็ดได้โดยไม่มีโอเวอร์เฮดที่มีนัยสำคัญ CoroutineScope จัดการวงจรชีวิตของโคโรทีน: viewModelScope ผูกกับ ViewModel, lifecycleScope — กับ Activity/Fragment เมื่อสโคปถูกทำลาย โคโรทีนลูกทั้งหมดจะถูกยกเลิกโดยอัตโนมัติ
Dispatchers กำหนดว่าพูลเธร็ดใดที่โคโรทีนทำงาน: Dispatchers.Main — เธร็ด UI; Dispatchers.IO — สำหรับคำขอเครือข่ายและการดำเนินการดิสก์; Dispatchers.Default — สำหรับการคำนวณที่ใช้ CPU มาก การเปลี่ยนดิสแพตเชอร์ใช้ withContext โคโรทีนรองรับการทำงานพร้อมกันแบบมีโครงสร้าง (structured concurrency): โคโรทีนแต่ละตัวมีพาเรนต์ และเมื่อพาเรนต์ถูกยกเลิก โคโรทีนลูกทั้งหมดจะถูกยกเลิก ซึ่งป้องกันการรั่วไหลของหน่วยความจำและงานที่ค้าง
Flow — สตรีมข้อมูลอะซิงโครนัสแบบเย็นจากไลบรารีโคโรทีน Flow ปล่อยค่าตามลำดับ: (1) โปรดิวเซอร์สร้างข้อมูล, (2) โอเปอเรเตอร์แปลงสตรีม, (3) คอลเลกเตอร์ใช้ผลลัพธ์ ต่างจาก LiveData, Flow รองรับเชนโอเปอเรเตอร์ที่ซับซ้อน (map, filter, flatMapConcat, catch) และปลอดภัยต่อเธร็ดอย่างสมบูรณ์ StateFlow และ SharedFlow — รูปแบบร้อนของ Flow เหมาะสำหรับสถานะ UI และเหตุการณ์ครั้งเดียว (Snackbar, การนำทาง)
Channel — สิ่งที่เป็นนามธรรมของโคโรทีนอีกอย่างสำหรับส่งข้อมูลระหว่างโคโรทีน Channel ทำงานเหมือนคิว: ผู้ส่งหนึ่งราย (send) และผู้รับหนึ่งรายหรือมากกว่า (receive) แชนเนลที่มีบัฟเฟอร์ (Channel(UNLIMITED), Channel(BUFFERED)) อนุญาตให้กำหนดค่าพฤติกรรมเมื่อล้น Channel มักใช้กับ Flow เพื่อเชื่อมต่อ API ที่ใช้ callback เข้ากับโคโรทีน: callbackFlow { … }
ที่ IT Sectr เราใช้โคโรทีนและ Flow อย่างแข็งขันในทุกโครงการ Android ซึ่งช่วยให้เขียนโคเด้ดอะซิงโครนัสที่ดูเหมือนซิงโครนัส ทดสอบง่าย (runTest, TestDispatcher) และไม่ต้องการการจัดการเธร็ดด้วยตนเอง ตัวอย่างของโคโรทีนอย่างง่ายพร้อมการโหลดข้อมูล:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
การเขียนโปรแกรมเชิงรับ — กระบวนทัศน์ที่ข้อมูลแพร่กระจายเป็นสตรีมอะซิงโครนัส (Observable, Publisher) RxJava/RxKotlin — การนำไปใช้ที่ได้รับความนิยมมากที่สุดสำหรับ Android, พอร์ตจาก .NET Rx RxSwift — ไลบรารีที่คล้ายกันสำหรับ iOS ส่วนประกอบหลัก: Observable (แหล่งเหตุการณ์), Observer (ผู้สมัครสมาชิก), Scheduler (การจัดการเธร็ด), Operators (การแปลงสตรีม)
Combine — เฟรมเวิร์กของ Apple สำหรับการเขียนโปรแกรมเชิงรับที่เปิดตัวใน iOS 13 Combine ใช้โปรโตคอล Publisher (ผู้เผยแพร่) และ Subscriber (ผู้สมัครสมาชิก) ต่างจาก RxSwift, Combine ถูกสร้างไว้ใน SDK และรวมเข้ากับ SwiftUI อย่างใกล้ชิด โอเปอเรเตอร์ใน Combine: map, filter, combineLatest, zip, debounce, throttle — ครอบคลุมสถานการณ์ส่วนใหญ่: ตั้งแต่การผูกข้อมูลกับ UI ไปจนถึงดีเบานซ์คำค้นหา
Future และ Promise — รูปแบบสำหรับทำงานกับผลลัพธ์อะซิงโครนัสเดียว Future แทนค่าที่จะพร้อมใช้งานในภายหลัง Promise คือคำมั่นสัญญาที่จะให้ค่า ใน Rx นี่คือ Single (การตอบสนองที่สำเร็จหนึ่งครั้งหรือข้อผิดพลาด) ใน Combine — Future Publisher ในทางปฏิบัติ Future/Promise สะดวกสำหรับคำขอ API เดี่ยว ในขณะที่ Observable/Publisher — สำหรับสตรีมต่อเนื่อง (ตำแหน่งทางภูมิศาสตร์, การป้อนข้อความ)
Callback และ Delegate — รูปแบบคลาสสิกสำหรับการดำเนินการอะซิงโครนัส Callback — ฟังก์ชันที่ส่งเป็นอาร์กิวเมนต์และเรียกเมื่อการดำเนินการเสร็จสมบูรณ์ Delegate — ออบเจกต์ที่ใช้โปรโตคอลพร้อมเมธอดตัวจัดการเหตุการณ์ ข้อเสีย: "callback hell" (callback ซ้อนกัน) และความซับซ้อนของการจัดการข้อผิดพลาด NotificationCenter (iOS) และ EventBus (Android) — กลไกการกระจายเหตุการณ์ มีประโยชน์สำหรับการสื่อสารแบบหลวม ๆ แต่นำไปสู่การพึ่งพาโดยนัย
มัลติเธร็ดเปิดประตูสู่ประสิทธิภาพสูง แต่ในขณะเดียวกันก็สร้างความเสี่ยงของข้อผิดพลาดที่จับได้ยาก ที่พบบ่อยที่สุด: Race Condition (สภาวะการแข่ง), Deadlock (การตายเบา), Livelock (การล็อกแบบมีชีวิต) และ Starvation (การอดอยากของเธร็ด) การเข้าใจปัญหาเหล่านี้เป็นทักษะจำเป็นสำหรับนักพัฒนามือถือทุกคน
Race Condition เกิดขึ้นเมื่อสองเธร็ดขึ้นไปอ่านและเขียนข้อมูลเดียวกันพร้อมกันโดยไม่มีการซิงโครไนซ์ ผลลัพธ์ขึ้นอยู่กับว่าเธร็ดใดดำเนินการก่อน ตัวอย่างคลาสสิก: สองเธร็ดเพิ่มค่าตัวนับ การดำเนินการ "อ่าน → เพิ่ม → เขียน" ไม่เป็นอะตอม ดังนั้นเมื่อดำเนินการพร้อมกัน การเพิ่มหนึ่งครั้งจะ "สูญหาย" วิธีแก้ไข — ใช้ การดำเนินการแบบอะตอม (AtomicInteger, AtomicReference) หรือการล็อก (Mutex, Semaphore, synchronized)
Deadlock — สถานการณ์ที่แต่ละเธร็ดถือทรัพยากรและรอทรัพยากรที่ถูกยึดโดยเธร็ดอื่น ไม่มีเธร็ดใดสามารถดำเนินการต่อได้ เงื่อนไขการเกิด: การกีดกันร่วมกัน, การถือและรอ, ไม่มีการยึดคืน, การรอแบบวนรอบ การป้องกัน: กำหนดลำดับการได้มาซึ่งล็อกเดียว, ใช้ tryLock กับหมดเวลา, ใช้ อัลกอริทึมแบบ Lock-Free (ConcurrentHashMap, CopyOnWriteArrayList)
Livelock — เธร็ดไม่ได้ถูกล็อก แต่ส่งต่อทรัพยากรให้กันและกันอย่างต่อเนื่องโดยไม่ทำงานที่มีประโยชน์ ตัวอย่าง: สองคนพบกันในทางเดินและทั้งคู่หลบทาง เคลื่อนที่ไปในทิศทางเดียวกัน Starvation — เธร็ดไม่ได้รับการเข้าถึงทรัพยากรเพราะเธร็ดอื่นสกัดกั้นอย่างต่อเนื่อง วิธีแก้ไข: ล็อกที่เป็นธรรม (fair locks), ลำดับความสำคัญของเธร็ดด้วยความระมัดระวัง
เพื่อป้องกันปัญหามัลติเธร็ด จะใช้พื้นฐานการซิงโครไนซ์: Mutex (การกีดกันร่วมกัน), Semaphore (การจำกัดจำนวนการเข้าถึงพร้อมกัน), Lock (อินเทอร์เฟซกับ tryLock), Synchronized (ล็อกระดับ JVM), @MainActor (Swift — รับประกันการดำเนินการบนเธร็ดหลัก) บน Android ยังมี ThreadPool (พูลเธร็ด) ผ่าน Executors.newFixedThreadPool, newCachedThreadPool อย่างไรก็ตาม การจัดการพูลด้วยตนเองเป็นสิทธิพิเศษของโครงการเดิม; ในโครงการใหม่ควรใช้โคโรทีน
| เครื่องมือ | แพลตฟอร์ม | ประเภท | คุณสมบัติ |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | คิวงาน | เรียงลำดับ/พร้อมกัน, ลำดับความสำคัญ QoS, Thread Pool จัดการโดยระบบ |
| OperationQueue | iOS | คิวการดำเนินการ | การพึ่งพา, การยกเลิก, maxConcurrentOperationCount |
| Coroutines + Flow | Android | โคโรทีน | น้ำหนักเบา, การทำงานพร้อมกันแบบมีโครงสร้าง, StateFlow, Channel |
| RxJava / RxKotlin | Android | สตรีมเชิงรับ | Observable, Schedulers, ชุดโอเปอเรเตอร์ที่หลากหลาย |
| Combine | iOS | สตรีมเชิงรับ | Publisher/Subscriber, การรวมกับ SwiftUI |
| Async/Await + Task | iOS / Android | โมเดลอะซิงโครนัส | โค้ดเชิงเส้น, @MainActor, การทำงานพร้อมกันแบบมีโครงสร้าง |
คำถามที่พบบ่อย
Main Thread (เธร็ด UI) รับผิดชอบการเรนเดอร์อินเทอร์เฟซและการประมวลผลการสัมผัส Background Thread ดำเนินการงานพื้นหลัง — โหลดข้อมูล, การคำนวณ, งานเครือข่าย การบล็อก Main Thread ทำให้อินเทอร์เฟซค้าง (ANR บน Android, frozen UI บน iOS)
Race Condition — สภาวะการแข่งเมื่อสองเธร็ดเข้าถึงข้อมูลที่ใช้ร่วมกันพร้อมกันและผลลัพธ์ขึ้นอยู่กับลำดับการดำเนินการ หลีกเลี่ยงได้โดยการซิงโครไนซ์: Mutex, Semaphore, Lock, Synchronized, @MainActor หรือการดำเนินการแบบอะตอม
Coroutines เป็นมาตรฐานสมัยใหม่สำหรับ Android (JetBrains, รองรับโดย Google) RxJava/RxKotlin เป็นแนวทางเชิงรับพร้อมชุดโอเปอเรเตอร์ที่หลากหลาย Coroutines ง่ายกว่าสำหรับการเรียกอะซิงโครนัส, RxJava มีประสิทธิภาพมากกว่าสำหรับสตรีมข้อมูลที่ซับซ้อน ที่ IT Sectr เราใช้ Coroutines + Flow สำหรับโครงการใหม่
Deadlock — การบล็อกซึ่งกันและกันเมื่อสองเธร็ดรอทรัพยากรของกันและกัน Livelock — เธร็ดไม่ได้ถูกล็อกแต่ส่งต่อทรัพยากรอย่างต่อเนื่องโดยไม่ทำงานที่มีประโยชน์ ปัญหาทั้งสองแก้ไขได้ด้วยลำดับการล็อกที่เหมาะสมและหมดเวลา
DispatchQueue เป็นสิ่งที่เป็นนามธรรมของ Grand Central Dispatch (GCD) สำหรับจัดการเธร็ด Main Queue ดำเนินงานบนเธร็ดหลัก Global Queues — บนเธร็ดพื้นหลัง Serial Queue รับประกันการดำเนินการตามลำดับ, Concurrent Queue — แบบขนาน ในโครงการสมัยใหม่ GCD มักถูกแทนที่ด้วย Async/Await และ Task
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ