Thread Pool — เป็นกลไกการจัดการเธรดที่พูลของเธรดที่ถูกสร้างไว้ล่วงหน้าถูกนำมาใช้ซ้ำเพื่อดำเนินงาน หลีกเลี่ยงค่าใช้จ่ายเกินจำเป็นในการสร้างและทำลายเธรด ในการพัฒนาแอปมือถือ เธรดพูลใช้สำหรับการทำงานเบื้องหลัง: คำขอเครือข่าย การประมวลผลภาพ การทำงานกับฐานข้อมูล ตามเอกสารของ Google Android (2025) ExecutorService เป็นวิธีที่แนะนำสำหรับการจัดการเธรดเบื้องหลังใน Android ใน iOS OperationQueue และ GCD DispatchQueue กับคิว concurrent ทั่วโลกมีบทบาทคล้ายคลึงกัน
ประเด็นสำคัญ
Thread Pool (กลุ่มเธรด) — เป็นรูปแบบสถาปัตยกรรมที่จำนวนเธรดคงที่ถูกสร้างไว้ล่วงหน้าและนำมาใช้ซ้ำเพื่อดำเนินงานหลายงาน แทนที่จะสร้างเธรดใหม่สำหรับแต่ละการดำเนินการ (ซึ่งมีค่าใช้จ่ายสูง: ประมาณ 1 MB สแต็กต่อเธรดใน JVM) งานจะถูกวางในคิวและดำเนินการโดยเธรดที่ว่างจากพูล ในการพัฒนาแอปมือถือ เธรดพูลมีความสำคัญต่อประสิทธิภาพ — Android และ iOS จำกัดจำนวนเธรดต่อแอปพลิเคชัน
การสร้างเธรดเป็นการดำเนินการที่มีค่าใช้จ่ายสูง: การจัดสรรสแต็ก การลงทะเบียนระบบ การสลับบริบท บนอุปกรณ์มือถือที่มีทรัพยากรจำกัด การสร้างเธรดที่ไม่สามารถควบคุมได้นำไปสู่ OOM (OutOfMemoryError) บน Android และการจำกัดบน iOS Thread Pool แก้ปัญหาทั้งสอง: จำกัดจำนวนสูงสุดของเธรดที่ทำงานพร้อมกันและนำเธรดที่สร้างไว้แล้วกลับมาใช้ใหม่ Google แนะนำ ExecutorService แทน raw Thread() Apple แนะนำ OperationQueue แทน Thread
| พารามิเตอร์ | ไม่มีพูล (raw Thread) | มี Thread Pool |
|---|---|---|
| การสร้างเธรด | สำหรับแต่ละงาน | ครั้งเดียวเมื่อสร้างพูล |
| เธรดสูงสุด | ไม่จำกัด (เสี่ยง OOM) | จำกัดโดย core/max pool size |
| การใช้ประโยชน์ | ต่ำ (เธรดตายหลังงาน) | สูง (เธรดถูกนำมาใช้ซ้ำ) |
| การจัดการ | ด้วยตนเอง (join, interrupt) | อัตโนมัติ (ExecutorService) |
| การใช้หน่วยความจำ | เพิ่มขึ้นตามแต่ละงาน | คงที่ |
เธรดพูลทำงานตามหลักการ Producer-Consumer: งาน (Runnable/Callable) จะถูกวางในคิวบล็อก (BlockingQueue) เธรดจากพูลรอ工作在คิวและนำไปดำเนินการ อัลกอริทึม: หากจำนวนเธรดว่างน้อยกว่า corePoolSize จะสร้างเธรดใหม่ หากถึง corePoolSize งานจะถูกวางในคิว หากคิวเต็มและจำนวนเธรดน้อยกว่า maximumPoolSize จะสร้างเธรดเพิ่ม หากเกิน maximumPoolSize งานจะถูกปฏิเสธผ่าน RejectedExecutionHandler
Core pool size — จำนวนเธรดที่ถูกเก็บในพูลแม้ในขณะว่าง Maximum pool size — จำนวนเธรดสูงสุดที่สามารถสร้างได้เมื่อคิวล้น ความแตกต่างระหว่างเธรดทั้งสองคือเธรดเพิ่มเติม (overflow) ที่ถูกสร้างขึ้นชั่วคราวและสิ้นสุดหลังหมดเวลาไม่ได้ใช้งาน บนอุปกรณ์มือถือ แนะนำให้ตั้ง corePoolSize เท่ากับ maximumPoolSize เพื่อหลีกเลี่ยงโหลดสูงสุดจากการสร้างเธรด
BlockingQueue เก็บงานที่รอดำเนินการ การใช้งานที่นิยมที่สุด: LinkedBlockingQueue (ไม่จำกัด), ArrayBlockingQueue (จำกัด) และ SynchronousQueue (ไม่มีการจัดเก็บ — งานถูกส่งตรงไปยังเธรด) เมื่อคิวและพูลเต็ม RejectedExecutionHandler จะถูกเรียกใช้นโยบายมาตรฐาน: AbortPolicy (โยน RejectedExecutionException), CallerRunsPolicy (ดำเนินการในเธรดของผู้ส่ง), DiscardPolicy และ DiscardOldestPolicy
// การสร้าง Thread Pool ใน Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // ขั้นต่ำ 2 เธรด
maximumPoolSize = 4, // สูงสุด 4 เธรด
keepAliveTime = 30L, // อายุของเธรดล้น
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// การส่งงาน
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// การปิดพูล
threadPool.shutdown()
// รอให้งานทั้งหมดเสร็จ
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android มีการใช้งานเธรดพูลหลายแบบผ่าน java.util.concurrent Executors — โรงงานที่มีการกำหนดค่าพร้อมใช้: newFixedThreadPool(n) (พูลคงที่), newCachedThreadPool() (ไม่จำกัด, สร้างเธรดตามต้องการ), newSingleThreadExecutor() (เธรดเดียว — ดำเนินการตามลำดับ) สำหรับโปรเจกต์มือถือ แนะนำ newFixedThreadPool ด้วยขีดจำกัดที่เหมาะสม (2-4 เธรด) เนื่องจากพูลแคชสามารถสร้างเธรดมากเกินไป
ThreadPoolExecutor (TPE) — การใช้งานเต็มรูปแบบของ ExecutorService ด้วยพารามิเตอร์ที่กำหนดค่าได้ บน Android TPE ถูกใช้ภายใน AsyncTask, IntentService และ JobIntentService พารามิเตอร์ corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue และ RejectedExecutionHandler ช่วยปรับแต่งพฤติกรรมของพูล คำแนะนำสำหรับ Android: corePoolSize = จำนวนคอร์ CPU - 1 (สำหรับงาน IO-bound) หรือจำนวนคอร์ (สำหรับงาน CPU-bound) สำหรับแอปพลิเคชันทั่วไป: 2-4 เธรด
// การกำหนดค่าพร้อมใช้ของ Executors
// 1. พูลคงที่ 3 เธรด
val fixedPool = Executors.newFixedThreadPool(3)
// 2. พูลแคช (ไม่แนะนำสำหรับมือถือ)
val cachedPool = Executors.newCachedThreadPool()
// 3. เธรดเดียว (การเรียงลำดับ)
val singlePool = Executors.newSingleThreadExecutor()
// 4. ตัวกำหนดเวลา (งานเป็นระยะ)
val scheduler = Executors.newScheduledThreadPool(2)
// การใช้กับ Callable และ Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// การรับผลลัพธ์ (บล็อกเธรด)
val result = future.get(5, TimeUnit.SECONDS)
// การปิดพูล
fixedPool.shutdownNow()
Coroutine ของ Kotlin มี CoroutineDispatcher — นามธรรมที่คล้ายกับเธรดพูล Dispatchers.IO ใช้พูล 64 เธรด (จำกัด) Dispatchers.Default — พูลเท่ากับจำนวนคอร์ CPU CoroutineDispatcher ไม่ต้องการการปิดด้วยตนเองและถูกจัดการโดยอัตโนมัติ สำหรับการปรับแต่งขั้นสูง ให้สร้าง ExecutorCoroutineDispatcher ที่กำหนดเองผ่าน Executors.newFixedThreadPool(2).asCoroutineDispatcher() Coroutine ไม่ได้แทนที่เธรดพูล แต่ห่อหุ้มมัน
iOS มีกลไกหลักสองอย่างสำหรับจัดการเธรดพูล: OperationQueue (API ระดับสูงที่สร้างบน GCD) และ GCD DispatchQueue (API ระดับต่ำ C) OperationQueue ห่อหุ้มเธรดพูลผ่านคุณสมบัติ maxConcurrentOperationCount โดยค่าเริ่มต้น OperationQueue ใช้ค่าสูงสุดที่ระบบกำหนด (ขึ้นอยู่กับโหลดของระบบ) DispatchQueue.global() มีคิว concurrent กับพูลเธรดของระบบ
OperationQueue จัดการพูลเธรดผ่าน maxConcurrentOperationCount ค่า 1 สร้างคิวแบบเรียงลำดับ (คล้ายพูลเธรดเดียว) ค่ามากกว่า 1 สร้างพูล concurrent ตามขีดจำกัดที่ระบุ โดยค่าเริ่มต้น maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (ค่าที่เหมาะสมของระบบ ปกติ 4-8 เธรด) Operation รองรับการพึ่งพา ลำดับความสำคัญ และการยกเลิก แต่ละการดำเนินการทำงานบนเธรดว่างใดก็ได้จากพูลของระบบ
// OperationQueue กับพูล 3 เธรด
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// การสร้างการดำเนินการ
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// การพึ่งพา: operation2 รอ operation1
operation2.addDependency(operation1)
// การเพิ่มในคิว
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// การยกเลิกการดำเนินการทั้งหมด
queue.cancelAllOperations()
DispatchQueue — เป็นเธรดพูลของ Apple คิว concurrent (qos: .utility) ใช้พูลเธรดของระบบ ซึ่งปรับให้เหมาะสมกับโหลดปัจจุบันของอุปกรณ์ ระดับ QoS ที่แตกต่างกัน (userInteractive, userInitiated, utility, background) จับคู่กับพูลต่างกันที่มีลำดับความสำคัญต่างกัน DispatchGroup ช่วยให้ซิงโครไนซ์หลายงาน DispatchWorkItem รองรับการยกเลิกและ qualityOfService สำหรับการควบคุมละเอียด ให้สร้างคิว concurrent ที่กำหนดเองผ่าน DispatchQueue(label: qos: attributes: .concurrent)
// GCD DispatchQueue ในฐานะเธรดพูล
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// การส่งงานไปยังพูล
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup สำหรับการซิงโครไนซ์
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // งานทั้งสองเสร็จ
}
// การจำกัดการทำงานพร้อมกันผ่าน semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
การกำหนดค่าเธรดพูลส่งผลโดยตรงต่อประสิทธิภาพของแอปพลิเคชัน พารามิเตอร์ที่ไม่ถูกต้อง นำไปสู่การใช้ CPU ต่ำเกินไป (เธรดน้อยเกินไป) หรือโหลดระบบเกิน (มากเกินไป) สำหรับแอปพลิเคชันมือถือ ค่าที่เหมาะสมที่สุดแตกต่างจากฝั่งเซิร์ฟเวอร์เนื่องจากทรัพยากรจำกัดและการใช้พลังงาน พารามิเตอร์หลักคือ: corePoolSize, maxPoolSize, ความจุคิว และ keepAliveTime
สูตรสำหรับงาน IO-bound: corePoolSize = จำนวนคอร์ CPU × 2 (เธรดรอ I/O) สำหรับงาน CPU-bound: corePoolSize = จำนวนคอร์ CPU (เธรดยุ่งกับการคำนวณตลอดเวลา) บนอุปกรณ์มือถือสมัยใหม่ (6-8 คอร์) จะได้ 6-8 เธรดสำหรับ CPU-bound และ 12-16 สำหรับ IO-bound การทดสอบเชิงปฏิบัติแสดงให้เห็นว่าสำหรับแอปมือถือทั่วไป 3-4 เธรดเหมาะสมที่สุด — เธรดมากขึ้นเพิ่มการใช้พลังงานโดยไม่เพิ่มประสิทธิภาพ
ขนาดของคิวงาน (work queue) กำหนดว่างานสามารถรอดำเนินการได้กี่งาน คิวไม่จำกัด (LinkedBlockingQueue ไม่มีขีดจำกัด) อาจทำให้เกิด OOM เมื่องานมาถึงอย่างรวดเร็ว คิวจำกัด (ArrayBlockingQueue ขนาดคงที่) ปฏิเสธงานเมื่อเต็ม สำหรับแอปพลิเคชันมือถือ แนะนำ ArrayBlockingQueue ด้วยความจุ 16-32 งาน CallerRunsPolicy เป็น RejectedExecutionHandler ที่ดีที่สุดสำหรับมือถือ: มันทำให้ผู้ส่งช้าลง (แรงดันย้อนกลับ) แทนที่จะสูญเสียงาน
| พารามิเตอร์ | คำแนะนำสำหรับมือถือ | เหตุผล |
|---|---|---|
| corePoolSize | 2-4 | ทรัพยากรจำกัดของอุปกรณ์มือถือ |
| maxPoolSize | corePoolSize (หรือ +1-2) | หลีกเลี่ยงโหลดสูงสุดจากการสร้างเธรด |
| keepAliveTime | 15-30 วินาที | ปล่อยหน่วยความจำเร็วโดยไม่ต้องสร้างบ่อย |
| ความจุคิว | 16-32 | สมดุลระหว่างบัฟเฟอร์และความเสี่ยง OOM |
| Handler | CallerRunsPolicy | แรงดันย้อนกลับโดยไม่สูญเสียงาน |
นักพัฒนาแอปพลิเคชันมือถือมักทำผิดพลาดเมื่อใช้เธรดพูลซึ่งนำไปสู่การคราช หน่วยความจำรั่ว และการทำงานไม่เสถียร ที่พบบ่อยที่สุด: ไม่เรียก shutdown() สำหรับ ExecutorService, สร้างพูลใหม่ทุกการดำเนินการ, พูลใหญ่เกินไป, deadlock ระหว่างงาน, ใช้ CachedThreadPool บน Android
Deadlock เกิดขึ้นเมื่องานในพูลรอผลลัพธ์ของอีกงานจากพูลเดียวกัน แต่เธรดทั้งหมดไม่ว่าง ตัวอย่าง: งาน A ส่งงาน B ไปยังพูลเดียวกันและเรียก future.get() — หากพูลหมด งาน A รองาน B และงาน B ไม่สามารถดำเนินการได้เพราะไม่มีเธรดว่าง วิธีแก้: ใช้พูลแยกสำหรับงานระดับต่างๆ หรือ callback แบบอะซิงก์แทน .get() ที่บล็อก
// Deadlock ใน Thread Pool
val pool = Executors.newFixedThreadPool(1)
// งาน A รองาน B — deadlock!
val futureA = pool.submit {
// งานนี้จะไม่มีวันถูกดำเนินการ
val futureB = pool.submit { 42 }
futureB.get() // บล็อกตลอดไป
}
// วิธีแก้: พูลแยก
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// กำลังทำงานในพูลแยก — deadlock เป็นไปไม่ได้
}
}
// หรือใช้ CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
ExecutorService ที่สร้างใน Activity ต้องถูกปิดใน onDestroy() หากไม่ทำ เธรดจะค้างในหน่วยความจำแม้หลังจาก Activity ถูกทำลาย วิธีแก้: เก็บพูลในขอบเขต Application หรือ ViewModel ไม่ใช่ใน Activity สำหรับ coroutine ใช้ viewModelScope หรือ lifecycleScope หากพูลถูกสร้างภายใน Activity ต้องเรียก pool.shutdown() ใน onDestroy() สำหรับการทดสอบ ใช้ es.shutdownNow() เพื่อหยุดทันที
คำถามที่พบบ่อย
Thread Pool นำเธรดที่สร้างไว้แล้วกลับมาใช้ใหม่เพื่อดำเนินงานหลายงาน เธรดปกติ (raw Thread) ถูกสร้าง ดำเนินการหนึ่งงาน และถูกทำลาย การสร้างเธรดใช้หน่วยความจำประมาณ 1 MB และเวลาประมาณ 1 ms Thread Pool ลดค่าใช้จ่ายเกินจำเป็น จำกัดจำนวนเธรดสูงสุด และมี API สำหรับการจัดการ (shutdown, awaitTermination)
สำหรับแอปมือถือทั่วไป 2-4 เธรด เหมาะที่สุด สำหรับงาน CPU-bound — จำนวนคอร์ CPU สำหรับงาน IO-bound — จำนวนคอร์ × 2 เธรดมากขึ้นเพิ่มการใช้พลังงานและการสลับบริบทโดยไม่เพิ่มประสิทธิภาพ บน Android ใช้ Process.availableProcessors() เพื่อหาจำนวนคอร์ บน iOS — ProcessInfo.processInfo.processorCount
CachedThreadPool สร้างเธรดตามต้องการและนำเธรดที่มีอยู่กลับมาใช้ใหม่ ปัญหา: มันไม่จำกัดจำนวนเธรดสูงสุด หาก 100 งานมาถึงพร้อมกัน จะสร้าง 100 เธรด ซึ่งนำไปสู่ OOM บน Android (แต่ละเธรด ~1 MB) ใช้ newFixedThreadPool(n) ด้วยขีดจำกัดที่ชัดเจน CachedThreadPool ยอมรับได้เฉพาะงาน burst ระยะสั้นที่มีปริมาณน้อยที่รับประกัน
ใช่ หากพูลไม่ได้เป็นของคอนเทนเนอร์ที่จัดการ (เช่น coroutine) shutdown() หยุดรับงานใหม่และสิ้นสุดเธรดหลังจากงานปัจจุบันเสร็จ หากไม่มี shutdown() เธรดจะค้างในหน่วยความจำและแอปพลิเคชันไม่สิ้นสุด สำหรับ Activity เรียกใน onDestroy() สำหรับ ViewModel ใช้ coroutineScope การปิดพูลเป็นส่วนบังคับของการจัดการทรัพยากร คล้ายกับการปิด Cursor หรือ InputStream
ใช่ OperationQueue และ DispatchQueue เป็นเธรดพูลที่ iOS จัดให้ OperationQueue จำกัดการทำงานพร้อมกันผ่าน maxConcurrentOperationCount DispatchQueue.global() ใช้พูลเธรดของระบบโดยไม่ต้องควบคุมโดยตรง ต่างจาก Java ThreadPoolExecutor คุณไม่จัดการ corePoolSize หรือความจุคิว — ระบบปรับพูลให้เหมาะสมโดยอัตโนมัติตามโหลดปัจจุบันและการใช้พลังงานของอุปกรณ์
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม