Background Thread — เธรดการทำงานที่ไม่เกี่ยวข้องกับส่วนติดต่อผู้ใช้ ออกแบบมาสำหรับการดำเนินการที่ใช้เวลานาน: คำขอเครือข่าย การทำงานกับไฟล์ การแยกวิเคราะห์ JSON การบีบอัดภาพ การเข้ารหัส และการสอบถามฐานข้อมูล ใน iOS เธรดเบื้องหลังจะถูกจัดการผ่าน GCD (DispatchQueue.global) และ OperationQueue ใน Android ผ่าน Executors, WorkManager และ Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default) ตาม เอกสารประกอบ Apple DispatchQueue หลังจากเสร็จสิ้นการดำเนินการเบื้องหลัง ผลลัพธ์จะต้องถูกส่งกลับไปยัง Main Thread เพื่ออัปเดตอินเทอร์เฟซ
ประเด็นสำคัญ
Background Thread — เธรดใดๆ ในแอปพลิเคชันที่ไม่ใช่ Main Thread และไม่สามารถเข้าถึง UI ได้ หน้าที่ของมันคือปลดปล่อยเธรดหลักจากการดำเนินการที่หนักหน่วงเพื่อให้อินเทอร์เฟซยังคงตอบสนองได้ดี ระบบปฏิบัติการกระจายเธรดเบื้องหลังไปยังคอร์ของ CPU ทำให้สามารถทำงานหลายอย่างพร้อมกันได้ iOS จัดการพูลเธรดโดยอัตโนมัติผ่าน GCD ส่วน Android จัดการผ่านพูล Java Executors
ต่างจาก Main Thread ที่ประมวลผลเหตุการณ์ตามลำดับ (ทีละรายการ) เธรดเบื้องหลังสามารถทำงานแบบขนานได้ โดยจำกัดเพียงจำนวนคอร์ของ CPU เท่านั้น ตัวอย่างเช่น บนอุปกรณ์ 8 คอร์ สามารถทำงานเบื้องหลังแบบขนานได้สูงสุด 8 งานโดยไม่ช้าลงอย่างมีนัยสำคัญ อย่างไรก็ตาม จำนวนเธรดที่มากเกินไป (หลายร้อย) นำไปสู่ thread starvation — การแข่งขันเพื่อแย่งคอร์และค่าใช้จ่ายที่เพิ่มขึ้นจากการสลับบริบท (context switch)
Quality of Service (QoS) — กลไกของ iOS ที่อนุญาตให้ระบุลำดับความสำคัญของงานเบื้องหลัง ค่า: .userInteractive (สูงสุด เกือบเท่า Main Thread), .userInitiated (ผู้ใช้กำลังรอผลลัพธ์), .default (มาตรฐาน), .utility (ผู้ใช้ไม่ได้รอโดยตรง), .background (ต่ำสุด สำหรับการซิงค์และการจัดทำดัชนี) ใน Android สิ่งที่เทียบเท่าคือ Thread.setPriority() ตั้งแต่ 1 ถึง 10 แต่ Android ยังใช้ cgroups สำหรับการจัดการลำดับความสำคัญของเธรดแบบกลุ่ม
DispatchQueue.global(qos:) — วิธีหลักในการรับคิวเบื้องหลังบน iOS GCD (Grand Central Dispatch) สร้างพูลเธรดโดยอัตโนมัติและกระจายงานไปยังคอร์ การเรียก DispatchQueue.global(qos: .background).async {} ส่งบล็อกไปยังคิวเบื้องหลังที่มีลำดับความสำคัญต่ำที่สุด สำหรับงานที่ต้องการผลลัพธ์ทันที ให้ใช้ .userInitiated หรือ .utility
OperationQueue — สิ่งที่เป็นนามธรรมระดับสูงกว่า GCD ที่อนุญาตให้ตั้งค่าการพึ่งพาระหว่างการดำเนินการ จำนวนการดำเนินการพร้อมกันสูงสุด (maxConcurrentOperationCount) และลำดับความสำคัญ OperationQueue สะดวกสำหรับลูกโซ่การทำงานหลายอย่างที่ซับซ้อน: ดาวน์โหลดไฟล์ → แตกไฟล์ → บันทึกลงแคช โดยค่าเริ่มต้น OperationQueue จะใช้เธรดเบื้องหลัง เว้นแต่จะระบุเป็นอย่างอื่น
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue ที่มี maxConcurrentOperationCount = 2
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("ดาวน์โหลดแล้ว: \(url.lastPathComponent)")
}
}
}
}
// GCD: คิวเบื้องหลังส่วนกลางที่มี QoS ต่างกัน
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// ลำดับความสำคัญสูง — ผู้ใช้กำลังรอผลลัพธ์
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // จำลองการทำงาน
return "ผลลัพธ์การคำนวณ"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
ในตัวอย่าง OperationQueue โหลดภาพสองภาพแบบขนาน (maxConcurrentOperationCount = 2) ด้วย QoS เบื้องหลังผ่าน qualityOfService = .utility เมธอด GCD backgroundTaskWithQoS ใช้คิวส่วนกลางกับ .userInitiated สำหรับงานที่ผู้ใช้กำลังรอผลลัพธ์ ทั้งสองวิธีสิ้นสุดด้วยการกลับไปที่ DispatchQueue.main เพื่ออัปเดต UI — นี่เป็นข้อกำหนดบังคับบน iOS
GCD รองรับคิวสองประเภท: แบบอนุกรม (serial) และแบบพร้อมกัน (concurrent) คิวแบบอนุกรมทำงานทีละงาน — สะดวกสำหรับการเข้าถึงทรัพยากรที่ใช้ร่วมกัน (ไฟล์, DB) โดยไม่ต้องล็อก คิวแบบพร้อมกันทำงานแบบขนาน โดยกระจายไปยังคอร์ที่ว่าง DispatchQueue.global เป็นแบบพร้อมกันเสมอ หากต้องการสร้างคิวแบบอนุกรม ให้ใช้ DispatchQueue(label: "com.app.queue")
Android มีระดับนามธรรมหลายระดับสำหรับเธรดเบื้องหลัง วิธีการแบบคลาสสิกคือ java.util.concurrent.Executors.newFixedThreadPool(n) หรือ Executors.newCachedThreadPool() วิธีการสมัยใหม่คือ Kotlin Coroutines กับ Dispatchers.IO (สำหรับ I/O: เครือข่าย ไฟล์ DB) และ Dispatchers.Default (สำหรับงานที่ใช้ CPU มาก: การเรียงลำดับ การประมวลผลภาพ) WorkManager สำหรับงานเบื้องหลังที่เลื่อนออกไปและรับประกัน
HandlerThread — คลาสพิเศษของ Android สำหรับสร้างเธรดเบื้องหลังที่มี Looper ของตัวเอง (คิวข้อความ) ต่างจาก Executors ตรงที่ HandlerThread อนุญาตให้ส่งข้อความและ Runnable ผ่าน Handler ใช้สำหรับการดำเนินการที่ต้องจัดคิว (เช่น การเขียน DB แบบตามลำดับ) หลังจากใช้งาน ต้องเรียก quit() หรือ quitSafely() เพื่อปล่อยทรัพยากร
// Android: Executors และ Dispatchers ของ Coroutines
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// วิธีการแบบคลาสสิกผ่าน Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// วิธีการสมัยใหม่ผ่าน Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// การดำเนินการไฟล์ — กำลังทำงานในพูลเบื้องหลัง
readFromFile()
}
// ผลลัพธ์ถูกส่งกลับไปยัง Dispatchers.Main โดยอัตโนมัติ
}
// งานที่ใช้ CPU มากบน Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// การเรียงลำดับ การกรอง — กำลังทำงานบนพูล Default
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // จำลองการอ่านจากไฟล์
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
ตัวอย่าง DataRepository แสดงวิวัฒนาการของเธรดเบื้องหลังบน Android เมธอดแบบเก่า loadDataLegacy ใช้ Executors.newFixedThreadPool(4) กับ Handler เพื่อกลับไปยัง Main Thread เมธอดสมัยใหม่ loadDataCoroutines ใช้ withContext(Dispatchers.IO) — coroutine จะถูกระงับระหว่างการทำงานโดยไม่บล็อกเธรด และกลับมาทำงานต่อโดยอัตโนมัติบน Main Thread Dispatchers.Default แนะนำสำหรับการดำเนินการที่ผูกกับ CPU (การเรียงลำดับ การกรอง การแปลงข้อมูล)
Kotlin Coroutines — ไม่ใช่แค่วิธีการทำงานกับเธรด แต่เป็นโมเดลที่แตกต่างโดยพื้นฐาน: งานแบบอะซิงโครนัสไม่ได้ผูกติดกับเธรดเฉพาะและสามารถระงับได้โดยไม่บล็อก ซึ่งหมายความว่าเมื่ออยู่ในเบื้องหลัง coroutine จะไม่ครอบครองเธรด แต่จะปล่อยให้เธรดนั้นว่างสำหรับงานอื่นๆ กลไกการระงับช่วยให้ทำงานพร้อมกันหลายแสนงานบนพูลที่มี 4–8 เธรดได้โดยไม่มี thread starvation
ดิสแพตเชอร์หลักสามตัว: Dispatchers.Main (UI, หนึ่งเธรด), Dispatchers.IO (64 เธรดโดยค่าเริ่มต้นสำหรับการดำเนินการที่บล็อก: เครือข่าย ไฟล์ DB), Dispatchers.Default (เท่ากับจำนวนคอร์ CPU สำหรับการคำนวณที่เข้มข้น) โดยการรวมเข้าด้วยกันผ่าน withContext นักพัฒนาสามารถสลับระหว่างเธรดได้โดยไม่ต้องสร้าง callback withContext เป็นฟังก์ชัน suspend ที่ไม่ส่งคืนการควบคุมจนกว่างานจะเสร็จสิ้น
// Coroutines: การประกอบงานเบื้องหลัง
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// การโหลดข้อมูลแบบขนานจากแหล่งต่างๆ
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — ระงับจนกว่างานทั้งหมดจะเสร็จสมบูรณ์
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
ฟังก์ชัน loadUserProfile เริ่มงานเบื้องหลังสามงานแบบขนานผ่าน async fetchUser และ fetchPosts เป็นงานที่ผูกกับ I/O (เครือข่าย) ดำเนินการบน Dispatchers.IO processAvatar เป็นงานที่ผูกกับ CPU (การประมวลผลภาพ) ดำเนินการบน Dispatchers.Default await() ระงับ coroutine จนกว่างานทั้งหมดจะเสร็จสมบูรณ์ เวลาการทำงานทั้งหมดเท่ากับเวลาสูงสุดในสามงาน (500 ms สำหรับ fetchPosts) ไม่ใช่ผลรวมของมัน นี่คือข้อได้เปรียบสำคัญของ coroutines เหนือการทำงานตามลำดับ
Structured concurrency — หลักการที่ coroutine แต่ละตัวมีขอบเขตแม่ และการยกเลิกแม่จะยกเลิก coroutine ลูกโดยอัตโนมัติ ใน Android lifecycleScope จะยกเลิก coroutine ทั้งหมดเมื่อ Activity ถูกทำลาย viewModelScope จะทำเช่นนั้นเมื่อ ViewModel ถูกล้าง ซึ่งป้องกันการรั่วไหลของงานเบื้องหลัง: หากผู้ใช้ปิดหน้าจอ ขณะอยู่ในเบื้องหลัง coroutine จะไม่โหลดข้อมูลที่ไม่จำเป็นอีกต่อไป
WorkManager — ไลบรารี Android Jetpack สำหรับดำเนินงานเบื้องหลังที่ต้องทำให้เสร็จแม้หลังจากรีสตาร์ทอุปกรณ์หรือปิดแอปพลิเคชัน ต่างจาก Executors และ coroutines ที่อยู่ในกระบวนการของแอปพลิเคชัน WorkManager ส่งมอบงานให้กับดิสแพตเชอร์ระบบซึ่งรับประกันการทำงานภายใต้เงื่อนไขที่เหมาะสม (เครือข่ายพร้อมใช้งาน ประจุแบตเตอรี่ พื้นที่ว่าง) WorkManager เหมาะสำหรับการซิงค์ข้อมูล อัปโหลดบันทึก และสำรองข้อมูล
งานใน WorkManager คือคลาสที่สืบทอด Worker (หรือ CoroutineWorker สำหรับ coroutines) Worker.doWork() ทำงานบนเธรดเบื้องหลังที่ WorkManager จัดเตรียมให้ ผลลัพธ์จะถูกส่งคืนผ่าน Result.success(), Result.retry() หรือ Result.failure() งานสามารถต่อเป็นลูกโซ่ได้: oneTimeWorkRequest.andThen(nextRequest).enqueue() WorkManager เลือกเวลาดำเนินการที่เหมาะสมที่สุดโดยพิจารณาจากข้อจำกัด (Constraints)
// WorkManager กับ coroutines
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// กำลังทำงานบน Dispatchers.Default (โดยค่าเริ่มต้น)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// การจำลองการซิงโครไนซ์
delay(1000)
}
}
// การเริ่มงาน WorkManager พร้อมข้อจำกัด
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
SyncWorker สืบทอด CoroutineWorker — เวอร์ชันของ Worker ที่รองรับ coroutines doWork() ทำงานบน Dispatchers.Default โดยสลับไปยัง IO สำหรับการดำเนินการเครือข่ายผ่าน withContext Constraints รับประกันว่าการซิงค์จะเริ่มเมื่อเครือข่ายพร้อมใช้งานและประจุแบตเตอรี่ไม่ต่ำกว่าระดับต่ำ BackoffCriteria กับ EXPONENTIAL เพิ่มช่วงเวลาระหว่างการลองใหม่: 10, 20, 40 วินาที
สำหรับงานเป็นประจำ (การซิงค์ทุก 15 นาที การส่งการวิเคราะห์ทุกชั่วโมง) WorkManager มี PeriodicWorkRequestBuilder ช่วงเวลาขั้นต่ำคือ 15 นาที ต่างจาก OneTimeWorkRequest ตรงที่ PeriodicWorkRequest ไม่รับประกันการปฏิบัติตามช่วงเวลาที่แน่นอน — ระบบอาจรวมงานเป็นระยะหลายงานเข้าด้วยกันเพื่อประหยัดแบตเตอรี่ สำหรับช่วงเวลาที่แม่นยำ ให้ใช้ AlarmManager แต่คำนึงถึงข้อจำกัดของ Android 12+ เกี่ยวกับการปลุกที่แน่นอน
ข้อผิดพลาดแรก — การสร้าง Thread ใหม่สำหรับทุกงาน new Thread().start() สร้างเธรดเนทีฟที่จัดสรร ~1 MB สำหรับสแต็ก สำหรับ 100 งานแบบขนาน นั่นคือ 100 MB สำหรับสแต็กเท่านั้น บวกกับค่าใช้จ่ายในการสลับบริบท ใช้พูลเธรด: Executors.newFixedThreadPool(n) (Android) หรือ DispatchQueue.global() (iOS) — พวกมันนำเธรดกลับมาใช้ใหม่ ลดค่าใช้จ่ายลงหลายเท่า
ข้อผิดพลาดที่สอง — การเข้าถึงสถานะที่เปลี่ยนแปลงได้จากหลายเธรดเบื้องหลังโดยไม่มีการซิงโครไนซ์ หากสองเธรดเบื้องหลังเขียนไปยัง ArrayList หรือ HashMap เดียวกันพร้อมกัน จะเกิด race condition: ConcurrentModificationException บน Android, ข้อมูลเสียหายบน iOS วิธีแก้ปัญหา: ใช้คอลเล็กชันที่ปลอดภัยต่อเธรด (ConcurrentHashMap, CopyOnWriteArrayList) หรือจัดลำดับการเข้าถึงผ่านคิวเดียว (DispatchQueue serial)
ข้อผิดพลาดที่สาม — งานเบื้องหลังที่ไม่มีการจัดการวงจรชีวิต การเริ่ม coroutine ในขอบเขตส่วนกลางโดยไม่ผูกกับวงจรชีวิตของ Activity หรือ ViewModel นำไปสู่การรั่วไหล: งานยังคงทำงานต่อไปหลังจากหน้าจอถูกทำลาย ใน Android ให้ใช้ lifecycleScope (Activity/Fragment) หรือ viewModelScope (ViewModel) ใน iOS ให้ใช้ weak self ใน closure และยกเลิกงานเมื่อ deinit
คำถามที่พบบ่อย
Background Thread — เธรดที่ใช้ดำเนินการที่ไม่เกี่ยวข้องกับ UI: คำขอเครือข่าย การอ่าน/เขียนไฟล์ การแยกวิเคราะห์ JSON การคำนวณ มันช่วยปลดปล่อย Main Thread จากงานหนัก ทำให้อินเทอร์เฟซตอบสนองได้ดี ในiOS เธรดเบื้องหลังถูกจัดการผ่าน GCD (DispatchQueue.global) ใน Android ผ่าน Executors หรือ Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default)
Dispatchers.IO ออกแบบมาสำหรับการดำเนินการ I/O ที่บล็อก: การอ่านไฟล์ คำขอเครือข่าย การทำงานกับ DB มีพูล 64 เธรด Dispatchers.Default สำหรับงานที่ใช้ CPU มาก: การเรียงลำดับ การกรอง การประมวลผลภาพ พูลของมันเท่ากับจำนวนคอร์ CPU การใช้ Dispatchers.Default สำหรับการดำเนินการ I/O อาจบล็อกทุกคอร์ และการใช้ Dispatchers.IO สำหรับงาน CPU อาจสร้างเธรดจำนวนมากเกินไป
DispatchQueue.global(qos: .background).async { } ส่งบล็อกไปยังคิวเบื้องหลังส่วนกลาง หลังจากงานเบื้องหลังเสร็จสิ้น ต้องกลับไปยังเธรดหลักผ่าน DispatchQueue.main.async { } เพื่ออัปเดต UI สำหรับงานเบื้องหลังตามลำดับ ให้ใช้ OperationQueue กับ maxConcurrentOperationCount = 1 หรือ DispatchQueue(label: "serial")
จำนวนที่แนะนำ ของเธรดเบื้องหลังเท่ากับจำนวนคอร์ CPU บวก 1 สำหรับงานที่ผูกกับ I/O บนอุปกรณ์ 8 คอร์สมัยใหม่ นั่นคือ 9 เธรด การสร้างหลายร้อยเธรดนำไปสู่ thread starvation: OS ใช้เวลาในการสลับบริบทมากกว่าการทำงาน GCD บน iOS และ Executors บน Android จะปรับพูลเธรดให้เหมาะสมโดยอัตโนมัติสำหรับอุปกรณ์ปัจจุบัน
ใน Kotlin Coroutines การกลับไปยัง Main Thread เกิดขึ้นโดยอัตโนมัติหาก coroutine เริ่มต้นในขอบเขต Main (lifecycleScope.launch, viewModelScope.launch) ฟังก์ชัน withContext(Dispatchers.IO) จะระงับ coroutine บนเธรด IO และหลังจากเสร็จสิ้น จะกลับมาทำงานต่อโดยอัตโนมัติบนดิสแพตเชอร์ที่เริ่มต้นไว้ (โดยปกติคือ Main) ไม่จำเป็นต้องเรียก DispatchQueue.main.async อย่างชัดเจน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม