Thread — หน่วยพื้นฐานของเวลาโปรเซสเซอร์ ที่มีสแต็คของตัวเองและทำงานอย่างอิสระจากเธรดอื่น ในการพัฒนามือถือ เธรดถูกใช้สำหรับการทำงานแบบขนานของงานต่าง ๆ เพื่อให้อินเทอร์เฟซยังคงตอบสนองได้ดีระหว่างการดำเนินการที่ใช้เวลานาน Android รองรับ java.lang.Thread, Executors และ Kotlin Coroutines, iOS รองรับ Thread (Objective-C), GCD และ OperationQueue ตาม เอกสาร Android Thread การสร้างเธรดแบบเนทีฟต้องการการจัดสรรสแต็คประมาณ 1 MB โดยระบบปฏิบัติการ
ประเด็นสำคัญ
Thread (เธรดการทำงาน) — คือลำดับคำสั่งอิสระที่ระบบปฏิบัติการสามารถจัดตารางเวลาบนแกน CPU แต่ละกระบวนการ (แอปพลิเคชัน) มีอย่างน้อยหนึ่งเธรด — Main Thread เธรดเพิ่มเติมถูกสร้างขึ้นสำหรับการทำงานแบบขนานของงาน แต่ละเธรดมีสแต็คซอฟต์แวร์ของตัวเอง (พร้อมตัวแปรท้องถิ่น), ตัวนับคำสั่ง (PC) และรีจิสเตอร์ หน่วยความจำฮีป (heap) ถูกแชร์ระหว่างเธรดทั้งหมดของกระบวนการ
ในระบบปฏิบัติการมือถือ เธรดถูกจัดตารางเวลาด้วยการทำงานหลายอย่างแบบยึด抢先 (preemptive multitasking): ระบบปฏิบัติการสามารถขัดจังหวะการทำงานของเธรดได้ทุกเมื่อและโอนการควบคุมไปยังเธรดอื่น (context switch) การสลับบริบทเป็นการดำเนินการที่แพง (1-10 ไมโครวินาที) เนื่องจากต้องบันทึก/กู้คืนรีจิสเตอร์ CPU อัปเดต TLB และล้างแคช ดังนั้นจำนวนเธรดที่มากเกินไป (ร้อยหรือพัน) ทำให้ประสิทธิภาพลดลง — ระบบปฏิบัติการใช้เวลาในการสลับมากกว่าการทำงาน
เธรดและกระบวนการ — เป็นแนวคิดที่แตกต่างกัน กระบวนการคืออินสแตนซ์ของแอปพลิเคชันที่มีหน่วยความจำเสมือนเฉพาะ เธรดภายในกระบวนการแชร์หน่วยความจำนี้กับเธรดอื่น ใน Android แต่ละคอมโพเนนต์ของแอปพลิเคชัน (Activity, Service, BroadcastReceiver) ทำงานในกระบวนการเดียว แต่สามารถดำเนินการบนเธรดที่แตกต่างกัน แอปพลิเคชัน iOS ก็เป็นกระบวนการเดียวที่มีความสามารถในการสร้างเธรดเพิ่มเติมผ่าน GCD หรือ Thread
แต่ละเธรด ใน Java/Kotlin (Android) และ NSThread (iOS) ผ่านห้าสถานะ: New (สร้าง), Runnable (พร้อมทำงาน), Running (กำลังทำงานบน CPU), Blocked/Waiting (รอทรัพยากรหรือการแจ้งเตือน), Terminated (สิ้นสุด) การเปลี่ยนผ่านระหว่างสถานะถูกจัดการโดยตัวจัดตารางเวลาของระบบปฏิบัติการและไพรมิทีฟการซิงโครไนซ์ นักพัฒนาสามารถมีอิทธิพลต่อลำดับความสำคัญของเธรด (Thread.setPriority()) และสถานะของมัน (sleep, join, interrupt)
ใน Android เธรดเปลี่ยนไปยังสถานะ Blocked เมื่อพยายามยึดมอนิเตอร์ที่ถูกใช้งาน (synchronized), เรียก Object.wait() หรือ Thread.sleep() ใน iOS — เมื่อเรียก NSCondition.wait(), pthread_cond_wait() หรือ dispatch_semaphore_wait() ในสถานะ Blocked เธรดไม่กิน CPU แต่กินหน่วยความจำ (สแต็ค) เธรดสามารถถูกขัดจังหวะ (interrupted) จากเธรดอื่น โดยได้รับ InterruptedException (Java) หรือตรวจสอบ isCancelled (Kotlin Coroutines)
| สถานะ | คำอธิบาย | วิธีการเปลี่ยนผ่าน |
|---|---|---|
| New | เธรดถูกสร้างแต่ยังไม่ได้เริ่ม | คอนสตรัคเตอร์ Thread() |
| Runnable | เธรดพร้อมทำงาน รอ CPU | thread.start() |
| Running | เธรดกำลังทำงานบนแกน CPU | ตัวจัดตารางเวลาของ OS |
| Blocked/Waiting | เธรดรอทรัพยากร มอนิเตอร์ หรือการแจ้งเตือน | synchronized, wait(), sleep() |
| Terminated | เธรดทำงาน run() เสร็จหรือถูกขัดจังหวะ | run() เสร็จ, interrupt() |
Context switch (การสลับบริบท) — การดำเนินการที่ระบบปฏิบัติการบันทึกสถานะของเธรดปัจจุบัน (รีจิสเตอร์, PC, TLB) และโหลดสถานะที่บันทึกไว้ของเธรดอื่น ในระบบมือถือ (Linux + ART, XNU สำหรับ iOS) context switch ใช้เวลา 1-10 ไมโครวินาที ถ้าเธรดทำงานเสร็จใน 100 ไมโครวินาทีและ context switch ใช้ 5 ไมโครวินาที แสดงว่าเสียเวลา 5% เพื่อลด context switch iOS ใช้ GCD แบบ work stealing, Android ใช้พูลแบบ fixedThreadCount
Android ได้วิวัฒนาการจาก java.lang.Thread ระดับต่ำสู่ coroutines สมัยใหม่ แต่ละระดับของนามธรรมให้ความสามารถมากขึ้นด้วยค่าใช้จ่ายที่น้อยลง Thread เป็นคลาสพื้นฐาน แต่การสร้างโดยตรงไม่แนะนำ: เธรดใหม่ไม่ถูกจัดการโดยพูล ยากต่อการตรวจสอบและยกเลิก AsyncTask (เลิกใช้งานตั้งแต่ API 30) เป็นก้าวไปข้างหน้า แต่ประสบปัญหาการรั่วไหลของหน่วยความจำและการจัดการคอนฟิกูเรชันที่ไม่สะดวก
HandlerThread เป็นคลาสย่อยพิเศษของ Thread ที่มี Looper ซึ่งสามารถประมวลผลคิวข้อความ ถูกใช้สำหรับการทำงานตามลำดับของงานบนเธรดพื้นหลัง เช่น การเขียนข้อมูลลงใน Room หรือไฟล์ HandlerThread ถูกสร้างโดยเรียก start() จากนั้นผ่าน Handler(handlerThread.looper) สามารถส่งข้อความและ Runnable การเรียก handlerThread.quit() หยุด Looper และสิ้นสุดเธรด
// Android: Thread, HandlerThread และ Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. การสร้าง Thread โดยตรง (ไม่แนะนำ)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Direct thread executed")
})
thread.start()
}
// 2. HandlerThread สำหรับงานพื้นหลังแบบลำดับ
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// การทำงานตามลำดับบนเธรดพื้นหลัง
Thread.sleep(500)
print("HandlerThread: งานเสร็จสมบูรณ์")
}
// หยุดเธรด (ทำงานเมื่องานเสร็จ)
handlerThread.quitSafely()
}
// 3. Executors — พูลเธรด
fun executorExample() {
val executor = Executors.newFixedThreadPool(4)
for (i in 1..10) {
executor.execute {
print("Task $i on thread ${Thread.currentThread().getName()}")
}
}
executor.shutdown()
}
// 4. Kotlin Coroutines — มาตรฐานสมัยใหม่
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
ตัวอย่าง ThreadExample แสดงนามธรรมเธรดทั้งสี่ระดับใน Android การสร้าง Thread โดยตรงเป็นวิธีระดับต่ำสุดและมีประสิทธิภาพน้อยที่สุด HandlerThread มีประโยชน์สำหรับงานลำดับพื้นหลัง Executors.newFixedThreadPool(4) สร้างพูล 4 เธรดสำหรับการทำงานแบบขนานสูงสุด 10 งาน Kotlin Coroutines กับ Dispatchers.Default — วิธีที่ทันสมัย มีประสิทธิภาพ และปลอดภัย
HandlerThread — คลาสย่อยเฉพาะของ Thread ที่มี Looper ในตัวและคิวข้อความ มันถูกสร้างโดยเรียก start() จากนั้นผ่าน Handler(handlerThread.looper) สามารถส่ง Runnable และข้อความ HandlerThread ทำงานตามลำดับอย่างเคร่งครัด — งานถัดไปจะไม่เริ่มจนกว่างานก่อนหน้าจะเสร็จ สะดวกสำหรับการเขียนข้อมูลลงใน Room หรือไฟล์ที่ลำดับการดำเนินการสำคัญ การเรียก quitSafely() หยุด Looper หลังจากงานปัจจุบันเสร็จ
iOS ก็มีสามระดับในการทำงานกับเธรด Thread (Thread ใน Swift, NSThread ใน Objective-C) — API ระดับต่ำที่สร้างเธรดเนทีฟโดยตรง GCD (Grand Central Dispatch) ผ่าน DispatchQueue — เครื่องมือหลักสำหรับนักพัฒนา iOS จัดการพูลเธรดโดยอัตโนมัติ OperationQueue — นามธรรมระดับสูงเหนือ GCD ที่รองรับการพึ่งพา ลำดับความสำคัญ และการยกเลิก
การใช้ Thread โดยตรงในการพัฒนา iOS สมัยใหม่พบได้ยากมาก — GCD มีความสามารถที่จำเป็นทั้งหมดพร้อมการจัดการหน่วยความจำและเธรดอัตโนมัติ Thread ใช้เฉพาะในกรณีพิเศษ: การตั้งค่า thread-local storage (threadDictionary), การสร้าง RunLoop สำหรับเธรดพื้นหลัง หรือการรวมเข้ากับไลบรารี C ที่คาดหวัง pthread_t
import Foundation
class ThreadManager {
// 1. Thread (ระดับต่ำ)
func createThread() {
let thread = Thread {
// โค้ดทำงานบนเธรดใหม่
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// คิวขนาน
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async task")
}
// Barrier สำหรับการซิงโครไนซ์การเขียน
queue.async(flags: .barrier) {
// การเข้าถึงเฉพาะระหว่างการเขียน
print("Barrier write: exclusive access")
}
}
// 3. OperationQueue พร้อมการพึ่งพา
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Downloading...")
}
let process = BlockOperation {
print("Processing...")
}
let save = BlockOperation {
print("Saving...")
}
// การพึ่งพา: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// คอลเลกชัน thread-safe ผ่าน GCD barrier
class ThreadSafeArray<T> {
private var array: [T] = []
private let queue = DispatchQueue(label: "com.app.concurrent",
attributes: .concurrent)
var count: Int {
return queue.sync { array.count } // concurrent read
}
func append(_ element: T) {
queue.async(flags: .barrier) { // exclusive write
self.array.append(element)
}
}
}
คลาส ThreadSafeArray แสดงรูปแบบการอ่านพร้อมกัน / เขียนเฉพาะผ่าน GCD barrier การอ่านผ่าน queue.sync{} ทำงานแบบขนานจากหลายเธรด การเขียนผ่าน queue.async(flags: .barrier) บล็อกการดำเนินการอื่นทั้งหมด (ทั้งอ่านและเขียน) จนกว่าการเขียนจะเสร็จ มีประสิทธิภาพมากกว่าบล็อก synchronized เพราะไม่บล็อกผู้อ่านเมื่อไม่มีการเขียน
การใช้ Thread โดยตรงใน iOS สมเหตุสมผลในสามกรณี: สำหรับ thread-local storage (Thread.current.threadDictionary) — การเก็บข้อมูลที่ผูกกับเธรด; สำหรับการสร้าง RunLoop พิเศษบนเธรดพื้นหลังด้วย performSelector:onThread:; สำหรับการรวมเข้ากับไลบรารี C/C++ ที่คาดหวัง pthread_t ในกรณีอื่นทั้งหมด GCD ผ่าน DispatchQueue ดีกว่า — จัดการพูลเธรดและการใช้พลังงานโดยอัตโนมัติ
Race condition (สภาวะการแข่งขัน) เกิดขึ้นเมื่อเธรดสองเธรดขึ้นไปเข้าถึงข้อมูลร่วมกันพร้อมกัน และอย่างน้อยหนึ่งเธรดทำการเขียน ผลลัพธ์ขึ้นอยู่กับลำดับการทำงาน (timing) และไม่สามารถคาดเดาได้ เพื่อป้องกัน race condition ใช้ไพรมิทีฟการซิงโครไนซ์ ในการพัฒนามือถือ มีล็อค (synchronized, NSLock), การดำเนินการอะตอมิก (AtomicInteger, คุณสมบัติ atomic ของ iOS) และคิว (serial queue)
การเลือกไพรมิทีฟ ขึ้นอยู่กับสถานการณ์ สำหรับตัวนับและธงอย่างง่าย การดำเนินการอะตอมิกก็เพียงพอ (AtomicInteger, คุณสมบัติ atomic) สำหรับส่วนวิกฤตที่มีหลายการดำเนินการ — ล็อค (synchronized, NSLock) สำหรับโครงสร้างข้อมูลที่ซับซ้อน — serial DispatchQueue หรือ GCD barrier ล็อคเข้าใจง่ายกว่าแต่อ่อนแอต่อ deadlock และ livelock คิวซับซ้อนกว่าแต่ปลอดภัยกว่า
// การซิงโครไนซ์ใน Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — สำหรับตัวนับอย่างง่าย
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — สำหรับส่วนวิกฤต
@Synchronized
fun synchronizedOperation() {
// ทีละหนึ่งเธรดเท่านั้น
doWork()
}
// 3. Mutex จาก coroutine — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// ตัวอย่าง Deadlock: A ล็อค B, B ล็อค A
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun methodA() = synchronized(lockA) {
Thread.sleep(100)
synchronized(lockB) { print("OK") }
}
fun methodB() = synchronized(lockB) {
Thread.sleep(100)
synchronized(lockA) { print("OK") }
}
}
Counter แสดงสามวิธีในการซิงโครไนซ์ AtomicInteger.incrementAndGet() — การดำเนินการอะตอมิกแบบไม่มีล็อค (CAS) @Synchronized — มอนิเตอร์ในตัวของ Java ล็อคทั้งออบเจ็กต์ Mutex.withLock — มิวเท็กซ์ของ coroutine ระงับ coroutine แทนที่จะบล็อกเธรด (มีประสิทธิภาพกว่า) DeadlockExample แสดง deadlock แบบคลาสสิก: สองเธรดรับล็อคในลำดับที่ต่างกัน
Thread Pool (พูลเธรด) — ชุดของเธรดที่สร้างไว้ล่วงหน้าที่ถูกนำกลับมาใช้ใหม่สำหรับการทำงาน แทนที่จะสร้างเธรดใหม่สำหรับแต่ละงาน (แพง) พูลนำเธรดว่างจากพูล ถ้าไม่มีเธรดว่าง งานจะถูกใส่ในคิว พูลจัดการขนาดโดยอัตโนมัติ: เธรดใหม่ถูกสร้างขึ้นเมื่อโหลดสูง เธรดที่ว่างจะถูกสิ้นสุด ซึ่งลดค่าใช้จ่ายในการสร้างเธรดลงหลายสิบเท่า
ใน Android Executors.newFixedThreadPool(4) สร้างพูล 4 เธรด ถ้ามี 10 งานมาพร้อมกัน 4 งานเริ่มทันที 6 งานรอในคิว Executors.newCachedThreadPool() สร้างเธรดตามความจำเป็น (ไม่จำกัด) และสิ้นสุดเธรดที่ว่างหลังจาก 60 วินาที สำหรับ iOS GCD จัดหาพูลคิวทั่วโลกโดยอัตโนมัติ ซึ่งขนาดสอดคล้องกับจำนวนแกน CPU และโหลดปัจจุบัน
ใน Kotlin Coroutines พูลเธรดถูกซ่อนอยู่ใน dispatcher Dispatchers.Default ใช้พูลขนาดเท่ากับจำนวนแกน CPU (ขั้นต่ำ 2) Dispatchers.IO — 64 เธรด (เพียงพอสำหรับงาน IO-bound หลายร้อยงาน เพราะส่วนใหญ่จะรอ I/O โดยไม่ใช้ CPU) แต่ละ dispatcher ปรับขนาดพูลตามโหลดโดยอัตโนมัติ ประหยัดพลังงานแบตเตอรี่เมื่อว่าง
คำถามที่พบบ่อย
Thread — หน่วยพื้นฐานของการทำงานของโค้ดในแอปพลิเคชัน แต่ละกระบวนการสามารถมีหลายเธรดที่แชร์หน่วยความจำแต่มีสแต็คของตัวเอง ในการพัฒนามือถือ เธรดถูกใช้สำหรับการทำงานแบบขนานของงานโดยไม่บล็อก UI Android ใช้ Thread, Executors, HandlerThread และ Coroutines iOS ใช้ Thread, GCD (DispatchQueue) และ OperationQueue
การสร้าง Thread ต้องการการจัดสรรสแต็ค ~1 MB ใน Android และ ~512 KB ใน iOS — เป็นการดำเนินการที่แพง สำหรับ 1,000 งาน การสร้าง 1,000 เธรดโดยตรงจะต้องใช้ ~1 GB สำหรับสแต็คอย่างเดียว บวกกับค่าใช้จ่ายของ context switch แทนที่จะใช้ Thread ให้ใช้พูล (Executors, GCD) หรือ coroutine — พวกมันนำเธรดกลับมาใช้ใหม่ ลดค่าใช้จ่ายลงหลายสิบเท่า
Race condition — พฤติกรรมที่ไม่สามารถคาดเดาได้เมื่อหลายเธรดเข้าถึงข้อมูลร่วมกันพร้อมกันด้วยการเขียน สามารถหลีกเลี่ยงได้สามวิธี: ใช้ประเภทอะตอมิก (AtomicInteger), ใช้ล็อค (synchronized, NSLock) หรือจัดลำดับการเข้าถึงผ่านคิว (DispatchQueue serial, Actor ใน Kotlin) แนวปฏิบัติที่ดีที่สุดคือลดสถานะ mutable ร่วมกันให้น้อยที่สุดและใช้ immutability
Thread — ออบเจ็กต์ระบบแบบเนทีฟ กินสแต็ค ~1 MB และผูกกับแกน OS Coroutine — หน่วยการทำงานแบบเบาของ Kotlin ที่ไม่ผูกกับเธรดเฉพาะและสามารถระงับ (suspend) ได้โดยไม่บล็อก เธรดเดียวสามารถทำงาน coroutine ได้หลายพัน Coroutine มีประสิทธิภาพด้านหน่วยความจำมากกว่าและช่วยให้เขียนโค้ดอะซิงโครนัสโดยไม่ต้องใช้ callback
Deadlock แสดงออกเป็นการค้างของแอปพลิเคชันโดยไม่มี ANR ใน Android ใช้ Thread.getAllStackTraces() เพื่อ dump สแต็คของเธรดทั้งหมด — สองเธรดจะรอล็อคของกันและกัน ใน iOS — Thread.callStackSymbols เครื่องมือ: Android Studio Profiler (แท็บ Threads), Instruments (iOS, Thread State View) การป้องกัน: รับล็อคในลำดับที่固定, ใช้ tryLock แบบมี timeout
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม