Thread Pool trong phát triển di động — khái niệm cơ bản, nhóm luồng và nguyên lý hoạt động

Tác giả: IT Sectr Đã đăng: 2026-03-18 Thời gian đọc: 11 phút

Thread Pool — là cơ chế quản lý luồng trong đó một nhóm luồng được tạo trước được tái sử dụng để thực thi các tác vụ, tránh chi phí tạo và hủy luồng. Trong phát triển di động, thread pool được sử dụng cho các hoạt động nền: yêu cầu mạng, xử lý hình ảnh, làm việc với cơ sở dữ liệu. Theo tài liệu Google Android (2025), ExecutorService là cách được khuyến nghị để quản lý luồng nền trong Android. Trong iOS, OperationQueue và GCD DispatchQueue với hàng đợi concurrent toàn cục đóng vai trò tương tự.

Những điểm chính

  • Thread Pool — nhóm các luồng có thể tái sử dụng để thực thi tác vụ nền mà không có chi phí tạo luồng.
  • ExecutorService trong Android quản lý nhóm qua ThreadPoolExecutor với các tham số có thể cấu hình.
  • OperationQueue trong iOS đóng gói thread pool qua maxConcurrentOperationCount.
  • Core pool size — số lượng luồng tối thiểu luôn sẵn sàng để thực thi tác vụ.
  • Work queue lưu trữ các tác vụ đang chờ luồng trống trong nhóm.

Thread Pool là gì?

Thread Pool (nhóm luồng) — là một mẫu kiến trúc trong đó một số lượng luồng cố định được tạo trước và tái sử dụng để thực thi nhiều tác vụ. Thay vì tạo luồng mới cho mỗi thao tác (tốn kém: khoảng 1 MB stack mỗi luồng trong JVM), các tác vụ được đặt vào hàng đợi và được thực thi bởi các luồng có sẵn trong nhóm. Trong phát triển di động, thread pool rất quan trọng đối với hiệu suất — Android và iOS giới hạn số lượng luồng trên mỗi ứng dụng.

Tại sao Thread Pool quan trọng trong phát triển di động

Tạo luồng là một thao tác tốn kém: cấp phát stack, đăng ký hệ thống, chuyển đổi ngữ cảnh. Trên thiết bị di động với tài nguyên hạn chế, việc tạo luồng không kiểm soát dẫn đến OOM (OutOfMemoryError) trên Android và throttling trên iOS. Thread Pool giải quyết cả hai vấn đề: giới hạn số lượng luồng tối đa chạy đồng thời và tái sử dụng các luồng đã tạo. Google khuyến nghị ExecutorService thay vì raw Thread(), Apple khuyến nghị OperationQueue thay vì Thread.

Tham sốKhông có nhóm (raw Thread)Có Thread Pool
Tạo luồngCho mỗi tác vụMột lần khi tạo nhóm
Luồng tối đaKhông giới hạn (rủi ro OOM)Giới hạn bởi core/max pool size
Sử dụngThấp (luồng chết sau tác vụ)Cao (luồng được tái sử dụng)
Quản lýThủ công (join, interrupt)Tự động (ExecutorService)
Tiêu thụ bộ nhớTăng theo mỗi tác vụCố định

Thread Pool hoạt động như thế nào trong phát triển di động?

Thread pool hoạt động theo nguyên lý Nhà sản xuất-Người tiêu dùng: các tác vụ (Runnable/Callable) được đặt vào hàng đợi chặn (BlockingQueue). Các luồng từ nhóm đợi tác vụ trong hàng đợi và lấy chúng để thực thi. Thuật toán: nếu số luồng trống ít hơn corePoolSize, một luồng mới được tạo. Nếu đạt corePoolSize, tác vụ được đặt vào hàng đợi. Nếu hàng đợi đầy và số luồng ít hơn maximumPoolSize, một luồng bổ sung được tạo. Nếu vượt quá maximumPoolSize, tác vụ bị từ chối qua RejectedExecutionHandler.

Core Pool Size so với Maximum Pool Size

Core pool size — số lượng luồng được giữ trong nhóm ngay cả khi nhàn rỗi. Maximum pool size — số lượng luồng tối đa có thể được tạo khi hàng đợi tràn. Sự khác biệt giữa chúng là các luồng bổ sung (overflow) được tạo tạm thời và kết thúc sau thời gian chờ nhàn rỗi. Trên thiết bị di động, nên đặt corePoolSize bằng maximumPoolSize để tránh tải đột biến từ việc tạo luồng.

Work Queue và RejectedExecutionHandler

BlockingQueue lưu trữ các tác vụ đang chờ thực thi. Các triển khai phổ biến nhất: LinkedBlockingQueue (không giới hạn), ArrayBlockingQueue (có giới hạn) và SynchronousQueue (không lưu trữ — tác vụ được chuyển trực tiếp đến luồng). Khi hàng đợi và nhóm đầy, RejectedExecutionHandler được kích hoạt. Các chính sách tiêu chuẩn: AbortPolicy (ném RejectedExecutionException), CallerRunsPolicy (thực thi trong luồng của người gửi), DiscardPolicy và DiscardOldestPolicy.

kotlin
// Tạo Thread Pool trong Android
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // Tối thiểu 2 luồng
    maximumPoolSize = 4,     // Tối đa 4 luồng
    keepAliveTime = 30L,     // Thời gian sống của luồng tràn
    unit = TimeUnit.SECONDS,
    workQueue = LinkedBlockingQueue<Runnable>(16),
    threadFactory = Executors.defaultThreadFactory(),
    handler = ThreadPoolExecutor.CallerRunsPolicy()
)

// Gửi tác vụ
threadPool.execute {
    val result = api.fetchData()
    runOnUiThread { showData(result) }
}

// Tắt nhóm
threadPool.shutdown()
// Chờ tất cả tác vụ hoàn thành
threadPool.awaitTermination(10, TimeUnit.SECONDS)

Thread Pool trong Android: ExecutorService

Android cung cấp một số triển khai thread pool qua java.util.concurrent. Executors — một nhà máy với các cấu hình có sẵn: newFixedThreadPool(n) (nhóm cố định), newCachedThreadPool() (không giới hạn, luồng được tạo khi cần), newSingleThreadExecutor() (một luồng — thực thi tuần tự). Đối với các dự án di động, nên sử dụng newFixedThreadPool với giới hạn hợp lý (2-4 luồng), vì nhóm cached có thể tạo quá nhiều luồng.

ThreadPoolExecutor trong Android

ThreadPoolExecutor (TPE) — triển khai đầy đủ của ExecutorService với các tham số có thể cấu hình. Trên Android, TPE được sử dụng bên trong AsyncTask, IntentService và JobIntentService. Các tham số corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue và RejectedExecutionHandler cho phép tinh chỉnh hành vi của nhóm. Khuyến nghị cho Android: corePoolSize = số lõi CPU - 1 (cho tác vụ IO-bound) hoặc số lõi (cho tác vụ CPU-bound). Đối với ứng dụng điển hình: 2-4 luồng.

kotlin
// Cấu hình có sẵn của Executors
// 1. Nhóm cố định 3 luồng
val fixedPool = Executors.newFixedThreadPool(3)

// 2. Nhóm cached (không khuyến nghị cho di động)
val cachedPool = Executors.newCachedThreadPool()

// 3. Một luồng (tuần tự)
val singlePool = Executors.newSingleThreadExecutor()

// 4. Bộ lập lịch (tác vụ định kỳ)
val scheduler = Executors.newScheduledThreadPool(2)

// Sử dụng với Callable và Future
val future: Future<String> = fixedPool.submit(Callable {
    "Result: ${api.call()}"
})
// Lấy kết quả (chặn luồng)
val result = future.get(5, TimeUnit.SECONDS)

// Tắt nhóm
fixedPool.shutdownNow()

CoroutineDispatcher như thread pool

Coroutine Kotlin cung cấp CoroutineDispatcher — một sự trừu tượng tương tự thread pool. Dispatchers.IO sử dụng nhóm 64 luồng (có giới hạn). Dispatchers.Default — nhóm bằng số lõi CPU. CoroutineDispatcher không yêu cầu tắt thủ công và được quản lý tự động. Để tinh chỉnh, hãy tạo ExecutorCoroutineDispatcher tùy chỉnh qua Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Coroutine không thay thế thread pool mà bao bọc nó.

Thread Pool trong iOS: OperationQueue và GCD

iOS cung cấp hai cơ chế chính để quản lý thread pool: OperationQueue (API cấp cao dựa trên GCD) và GCD DispatchQueue (API cấp thấp bằng C). OperationQueue đóng gói thread pool qua thuộc tính maxConcurrentOperationCount. Theo mặc định, OperationQueue sử dụng mức tối đa do hệ thống xác định (phụ thuộc vào tải hệ thống). DispatchQueue.global() cung cấp hàng đợi concurrent với nhóm luồng hệ thống.

OperationQueue và maxConcurrentOperationCount

OperationQueue quản lý nhóm luồng qua maxConcurrentOperationCount. Giá trị 1 tạo hàng đợi tuần tự (tương tự nhóm một luồng). Giá trị lớn hơn 1 tạo nhóm concurrent với giới hạn đã chỉ định. Theo mặc định, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (tối ưu hệ thống, thường 4-8 luồng). Operation hỗ trợ phụ thuộc, ưu tiên và hủy. Mỗi thao tác được thực thi trên bất kỳ luồng có sẵn nào từ nhóm hệ thống.

swift
// OperationQueue với nhóm 3 luồng
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility

// Tạo thao tác
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) }
}

// Phụ thuộc: operation2 chờ operation1
operation2.addDependency(operation1)

// Thêm vào hàng đợi
queue.addOperations([operation1, operation2], waitUntilFinished: false)

// Hủy tất cả thao tác
queue.cancelAllOperations()

GCD DispatchQueue như thread pool

DispatchQueue — là thread pool của Apple. Hàng đợi concurrent (qos: .utility) sử dụng nhóm luồng hệ thống, được tối ưu hóa cho tải thiết bị hiện tại. Các mức QoS khác nhau (userInteractive, userInitiated, utility, background) ánh xạ đến các nhóm khác nhau với mức ưu tiên khác nhau. DispatchGroup cho phép đồng bộ hóa nhiều tác vụ. DispatchWorkItem hỗ trợ hủy và qualityOfService. Để kiểm soát chi tiết, hãy tạo hàng đợi concurrent tùy chỉnh qua DispatchQueue(label: qos: attributes: .concurrent).

swift
// GCD DispatchQueue như thread pool
let customQueue = DispatchQueue(
    label: "com.app.background",
    qos: .utility,
    attributes: .concurrent,
    autoreleaseFrequency: .workItem
)

// Gửi tác vụ vào nhóm
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }

// DispatchGroup để đồng bộ hóa
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() // Cả hai tác vụ đã hoàn thành
}

// Giới hạn đồng thời qua semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
    pool.async {
        semaphore.wait()
        download(url)
        semaphore.signal()
    }
}

Tham số cấu hình Thread Pool

Cấu hình thread pool ảnh hưởng trực tiếp đến hiệu suất ứng dụng. Tham số sai dẫn đến sử dụng CPU dưới mức (quá ít luồng) hoặc quá tải hệ thống (quá nhiều). Đối với ứng dụng di động, giá trị tối ưu khác với phía máy chủ do tài nguyên hạn chế và mức tiêu thụ năng lượng. Các tham số chính là: corePoolSize, maxPoolSize, dung lượng hàng đợi và keepAliveTime.

Tính toán kích thước nhóm tối ưu

Công thức cho tác vụ IO-bound: corePoolSize = số lõi CPU × 2 (luồng chờ I/O). Cho tác vụ CPU-bound: corePoolSize = số lõi CPU (luồng liên tục bận tính toán). Trên thiết bị di động hiện đại (6-8 lõi), điều này cho 6-8 luồng cho CPU-bound và 12-16 cho IO-bound. Kiểm tra thực tế cho thấy đối với ứng dụng di động điển hình, 3-4 luồng là tối ưu — nhiều luồng hơn làm tăng mức tiêu thụ năng lượng mà không cải thiện hiệu suất.

Dung lượng hàng đợi và hành vi tràn

Kích thước hàng đợi công việc (work queue) xác định bao nhiêu tác vụ có thể chờ thực thi. Hàng đợi không giới hạn (LinkedBlockingQueue không giới hạn) có thể dẫn đến OOM khi tác vụ đến nhanh. Hàng đợi có giới hạn (ArrayBlockingQueue với kích thước cố định) từ chối tác vụ khi đầy. Đối với ứng dụng di động, nên sử dụng ArrayBlockingQueue với dung lượng 16-32 tác vụ. CallerRunsPolicy là RejectedExecutionHandler tốt nhất cho di động: nó làm chậm người gửi (backpressure) thay vì mất tác vụ.

Tham sốKhuyến nghị cho di độngLý do
corePoolSize2-4Tài nguyên hạn chế của thiết bị di động
maxPoolSizecorePoolSize (hoặc +1-2)Tránh tải đột biến từ việc tạo luồng
keepAliveTime15-30 giâyGiải phóng bộ nhớ nhanh mà không tạo thường xuyên
Dung lượng hàng đợi16-32Cân bằng giữa đệm và rủi ro OOM
HandlerCallerRunsPolicyBackpressure không mất tác vụ

Lỗi điển hình khi làm việc với nhóm luồng

Các nhà phát triển ứng dụng di động thường mắc lỗi khi sử dụng thread pool dẫn đến sập, rò rỉ bộ nhớ và hoạt động không ổn định. Phổ biến nhất: không gọi shutdown() cho ExecutorService, tạo nhóm mới cho mỗi thao tác, nhóm quá lớn, deadlock giữa các tác vụ, sử dụng CachedThreadPool trên Android.

Deadlock trong Thread Pool

Deadlock xảy ra khi một tác vụ trong nhóm chờ kết quả của tác vụ khác từ cùng nhóm, nhưng tất cả luồng đều bận chờ. Ví dụ: tác vụ A gửi tác vụ B vào cùng nhóm và gọi future.get() — nếu nhóm đã cạn, tác vụ A chờ tác vụ B, và tác vụ B không thể thực thi vì không có luồng trống. Giải pháp: sử dụng các nhóm riêng biệt cho các cấp tác vụ khác nhau hoặc callback bất đồng bộ thay vì .get() chặn.

kotlin
// Deadlock trong Thread Pool
val pool = Executors.newFixedThreadPool(1)

// Tác vụ A chờ tác vụ B — deadlock!
val futureA = pool.submit {
    // Tác vụ này sẽ không bao giờ thực thi
    val futureB = pool.submit { 42 }
    futureB.get() // Chặn vĩnh viễn
}

// Sửa: nhóm riêng biệt
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()

workerPool.submit {
    callbackPool.submit {
        // Đang chạy trong nhóm riêng — deadlock không thể xảy ra
    }
}

// Hoặc sử dụng CompletableFuture
workerPool.submit {
    CompletableFuture
        .supplyAsync { 42 }
        .thenAccept { result ->
            println(result)
        }
}

Nhóm không kết thúc và rò rỉ

ExecutorService được tạo trong Activity phải được tắt trong onDestroy(). Nếu không, các luồng sẽ treo trong bộ nhớ ngay cả sau khi Activity bị hủy. Giải pháp: giữ nhóm trong phạm vi Application hoặc ViewModel, không phải Activity. Đối với coroutine, sử dụng viewModelScope hoặc lifecycleScope. Nếu nhóm được tạo bên trong Activity, hãy đảm bảo gọi pool.shutdown() trong onDestroy(). Đối với kiểm thử, sử dụng es.shutdownNow() để dừng ngay lập tức.

Câu hỏi thường gặp

Thread Pool khác luồng thông thường như thế nào?

Thread Pool tái sử dụng các luồng đã tạo để thực thi nhiều tác vụ. Luồng thông thường (raw Thread) được tạo, thực thi một tác vụ và bị hủy. Tạo luồng tốn khoảng 1 MB bộ nhớ và ~1 ms thời gian. Thread Pool giảm chi phí, giới hạn số lượng luồng tối đa và cung cấp API quản lý (shutdown, awaitTermination).

Có bao nhiêu luồng trong nhóm cho ứng dụng di động?

Đối với ứng dụng di động điển hình, 2-4 luồng là tối ưu. Cho tác vụ CPU-bound — số lõi CPU. Cho tác vụ IO-bound — số lõi × 2. Nhiều luồng hơn làm tăng mức tiêu thụ năng lượng và chuyển đổi ngữ cảnh mà không cải thiện hiệu suất. Trên Android, sử dụng Process.availableProcessors() để xác định lõi. Trên iOS — ProcessInfo.processInfo.processorCount.

CachedThreadPool là gì và tại sao nguy hiểm trên Android?

CachedThreadPool tạo luồng khi cần và tái sử dụng luồng hiện có. Vấn đề: nó không giới hạn số lượng luồng tối đa. Nếu 100 tác vụ đến cùng lúc, 100 luồng sẽ được tạo. Điều này dẫn đến OOM trên Android (mỗi luồng ~1 MB). Sử dụng newFixedThreadPool(n) với giới hạn rõ ràng. CachedThreadPool chỉ chấp nhận được cho các tác vụ bùng nổ ngắn hạn với khối lượng nhỏ được đảm bảo.

Có cần gọi shutdown() cho ExecutorService không?

, nếu nhóm không thuộc về một container được quản lý (như coroutine). shutdown() dừng nhận tác vụ mới và kết thúc các luồng sau khi hoàn thành tác vụ hiện tại. Không có shutdown(), các luồng treo trong bộ nhớ và ứng dụng không kết thúc. Đối với Activity, hãy gọi nó trong onDestroy(). Đối với ViewModel, sử dụng coroutineScope. Tắt nhóm là phần bắt buộc của quản lý tài nguyên, tương tự như đóng Cursor hoặc InputStream.

OperationQueue và DispatchQueue có phải là thread pool?

, OperationQueue và DispatchQueue là các thread pool do iOS cung cấp. OperationQueue giới hạn đồng thời qua maxConcurrentOperationCount. DispatchQueue.global() sử dụng nhóm luồng hệ thống mà không kiểm soát trực tiếp. Không giống Java ThreadPoolExecutor, bạn không quản lý corePoolSize hay dung lượng hàng đợi — hệ thống tự động tối ưu hóa nhóm dựa trên tải hiện tại và mức tiêu thụ năng lượng của thiết bị.

Tổng kết

  • Thread Pool — nhóm luồng có thể tái sử dụng cho tác vụ nền, giảm chi phí tạo luồng.
  • Android sử dụng ThreadPoolExecutor và Executors.newFixedThreadPool(n) với giới hạn kích thước nhóm rõ ràng.
  • iOS cung cấp OperationQueue với maxConcurrentOperationCount và GCD DispatchQueue với nhóm QoS.
  • Core pool size — số luồng tối thiểu; maximum pool size — tối đa khi hàng đợi tràn.
  • Deadlock trong nhóm xảy ra khi một tác vụ bị chặn chờ tác vụ khác từ cùng nhóm.
  • CallerRunsPolicy được ưu tiên cho ứng dụng di động — nó làm chậm người gửi mà không mất tác vụ.
  • Đối với ứng dụng di động điển hình, kích thước nhóm tối ưu là 2-4 luồng với shutdown() để dọn dẹp.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm