Background Thread — một luồng thực thi không liên quan đến giao diện người dùng, được thiết kế cho các thao tác kéo dài: yêu cầu mạng, làm việc với tệp, phân tích JSON, nén ảnh, mã hóa và truy vấn cơ sở dữ liệu. Trên iOS, các luồng nền được quản lý qua GCD (DispatchQueue.global) và OperationQueue; trên Android, qua Executors, WorkManager và Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Theo Tài liệu Apple DispatchQueue, sau khi hoàn thành thao tác nền, kết quả phải được trả về Main Thread để cập nhật giao diện.
Những điểm chính
Background Thread — bất kỳ luồng nào trong ứng dụng không phải là Main Thread và không có quyền truy cập UI. Nhiệm vụ của nó là giải phóng luồng chính khỏi các thao tác nặng để giao diện luôn phản hồi nhanh. Hệ điều hành phân phối các luồng nền trên các nhân CPU, cho phép thực thi nhiều tác vụ song song. iOS tự động quản lý nhóm luồng qua GCD, Android qua nhóm Java Executors.
Không giống như Main Thread xử lý sự kiện tuần tự (lần lượt từng cái), các luồng nền có thể chạy song song, chỉ bị giới hạn bởi số lượng nhân CPU. Ví dụ, trên thiết bị 8 nhân có thể chạy tới 8 tác vụ nền song song mà không bị chậm đáng kể. Tuy nhiên, số lượng luồng quá nhiều (hàng trăm) dẫn đến thread starvation — cạnh tranh nhân CPU và gia tăng chi phí chuyển đổi ngữ cảnh (context switch).
Quality of Service (QoS) — cơ chế iOS cho phép chỉ định mức ưu tiên của tác vụ nền. Giá trị: .userInteractive (cao nhất, gần như Main Thread), .userInitiated (người dùng đang chờ kết quả), .default (tiêu chuẩn), .utility (người dùng không trực tiếp chờ), .background (thấp nhất, cho đồng bộ và lập chỉ mục). Trên Android, tương đương là Thread.setPriority() từ 1 đến 10, nhưng Android cũng sử dụng cgroups để quản lý nhóm ưu tiên luồng.
DispatchQueue.global(qos:) — cách chính để lấy hàng đợi nền trên iOS. GCD (Grand Central Dispatch) tự động tạo nhóm luồng và phân phối tác vụ trên các nhân. Lệnh gọi DispatchQueue.global(qos: .background).async {} gửi một khối vào hàng đợi nền với mức ưu tiên thấp nhất. Cho các tác vụ cần kết quả ngay lập tức, hãy dùng .userInitiated hoặc .utility.
OperationQueue — một lớp trừu tượng cấp cao hơn trên GCD, cho phép thiết lập phụ thuộc giữa các thao tác, số lượng thao tác đồng thời tối đa (maxConcurrentOperationCount) và mức ưu tiên. OperationQueue tiện lợi cho chuỗi đa tác vụ phức tạp: tải tệp → giải nén → lưu vào bộ nhớ đệm. Mặc định, OperationQueue sử dụng luồng nền trừ khi được chỉ định khác.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue với 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("Đã tải: \(url.lastPathComponent)")
}
}
}
}
// GCD: hàng đợi nền toàn cục với các QoS khác nhau
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Ưu tiên cao — người dùng đang chờ kết quả
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // mô phỏng công việc
return "Kết quả tính toán"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
Trong ví dụ, OperationQueue tải hai ảnh song song (maxConcurrentOperationCount = 2) với QoS nền qua qualityOfService = .utility. Phương thức GCD backgroundTaskWithQoS sử dụng hàng đợi toàn cục với .userInitiated cho tác vụ mà người dùng đang chờ kết quả. Cả hai cách tiếp cận đều kết thúc bằng việc quay lại DispatchQueue.main để cập nhật UI — đây là yêu cầu bắt buộc trên iOS.
GCD hỗ trợ hai loại hàng đợi: serial (tuần tự) và concurrent (đồng thời). Hàng đợi serial thực thi tác vụ lần lượt từng cái — tiện lợi cho việc truy cập tài nguyên chia sẻ (tệp, DB) mà không cần khóa. Hàng đợi concurrent thực thi tác vụ song song, phân phối chúng trên các nhân khả dụng. DispatchQueue.global luôn là concurrent. Để tạo hàng đợi serial, sử dụng DispatchQueue(label: "com.app.queue").
Android cung cấp nhiều mức trừu tượng cho luồng nền. Cách tiếp cận cổ điển là java.util.concurrent.Executors.newFixedThreadPool(n) hoặc Executors.newCachedThreadPool(). Cách tiếp cận hiện đại là Kotlin Coroutines với Dispatchers.IO (cho I/O: mạng, tệp, DB) và Dispatchers.Default (cho tác vụ nặng về CPU: sắp xếp, xử lý ảnh). WorkManager dành cho các tác vụ nền bị trì hoãn và được đảm bảo.
HandlerThread — một lớp Android chuyên biệt để tạo luồng nền với Looper riêng (hàng đợi tin nhắn). Không giống Executors, HandlerThread cho phép gửi tin nhắn và Runnable qua Handler. Nó được dùng cho các thao tác cần xếp hàng đợi (ví dụ: ghi tuần tự vào DB). Sau khi sử dụng, cần gọi quit() hoặc quitSafely() để giải phóng tài nguyên.
// Android: Executors và Dispatchers Coroutines
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Cách tiếp cận cổ điển qua Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Cách tiếp cận hiện đại qua Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Thao tác tệp — đang thực thi trong nhóm nền
readFromFile()
}
// Kết quả tự động trả về Dispatchers.Main
}
// Tác vụ nặng CPU trên Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Sắp xếp, lọc — đang thực thi trên nhóm Default
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // mô phỏng đọc từ tệp
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
Ví dụ DataRepository cho thấy sự tiến hóa của luồng nền trên Android. Phương thức cũ loadDataLegacy sử dụng Executors.newFixedThreadPool(4) với Handler để quay lại Main Thread. Phương thức hiện đại loadDataCoroutines sử dụng withContext(Dispatchers.IO) — coroutine tạm dừng trong khi thực thi mà không chặn luồng và tự động tiếp tục trên Main Thread. Dispatchers.Default được khuyến nghị cho các thao tác CPU-bound (sắp xếp, lọc, biến đổi dữ liệu).
Kotlin Coroutines — không chỉ là cách làm việc với luồng, mà là một mô hình hoàn toàn khác: các tác vụ bất đồng bộ không gắn với một luồng cụ thể và có thể tạm dừng mà không chặn. Điều này có nghĩa là khi ở trong nền, coroutine không chiếm giữ luồng mà giải phóng nó cho các tác vụ khác. Cơ chế tạm dừng cho phép chạy hàng trăm nghìn tác vụ đồng thời trên một nhóm 4–8 luồng mà không bị thread starvation.
Ba bộ điều phối chính: Dispatchers.Main (UI, một luồng), Dispatchers.IO (mặc định 64 luồng cho thao tác chặn: mạng, tệp, DB), Dispatchers.Default (bằng số nhân CPU, cho tính toán nặng). Kết hợp chúng qua withContext, nhà phát triển chuyển đổi giữa các luồng mà không cần tạo callback. withContext là hàm suspend không trả quyền điều khiển cho đến khi tác vụ hoàn thành.
// Coroutines: tổ hợp các tác vụ nền
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Tải dữ liệu song song từ nhiều nguồn
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — tạm dừng cho đến khi tất cả tác vụ hoàn thành
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 }
Hàm loadUserProfile khởi chạy ba tác vụ nền song song qua async. fetchUser và fetchPosts là IO-bound (mạng), thực thi trên Dispatchers.IO. processAvatar là CPU-bound (xử lý ảnh), thực thi trên Dispatchers.Default. await() tạm dừng coroutine cho đến khi tất cả tác vụ hoàn thành. Tổng thời gian thực thi bằng thời gian tối đa trong ba tác vụ (500 ms cho fetchPosts), không phải tổng của chúng. Đây là lợi thế chính của coroutines so với thực thi tuần tự.
Structured concurrency — nguyên tắc mà mỗi coroutine có một phạm vi cha, và hủy cha sẽ tự động hủy các coroutine con. Trên Android, lifecycleScope hủy tất cả coroutine khi Activity bị hủy. viewModelScope làm điều đó khi ViewModel bị dọn dẹp. Điều này ngăn chặn rò rỉ tác vụ nền: nếu người dùng đóng màn hình, khi ở trong nền coroutine sẽ không tiếp tục tải dữ liệu không còn cần thiết.
WorkManager — thư viện Android Jetpack để thực thi các tác vụ nền phải được hoàn thành ngay cả sau khi khởi động lại thiết bị hoặc đóng ứng dụng. Không giống Executors và coroutines tồn tại trong tiến trình ứng dụng, WorkManager giao tác vụ cho bộ điều phối hệ thống đảm bảo thực thi trong điều kiện phù hợp (có mạng, pin sạc, dung lượng trống). WorkManager phù hợp cho đồng bộ dữ liệu, tải log và sao lưu.
Một tác vụ trong WorkManager là một lớp kế thừa Worker (hoặc CoroutineWorker cho coroutines). Worker.doWork() thực thi trên luồng nền do WorkManager cung cấp. Kết quả được trả về qua Result.success(), Result.retry() hoặc Result.failure(). Các tác vụ có thể được xâu chuỗi: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager tự chọn thời điểm thực thi tối ưu dựa trên các ràng buộc (Constraints).
// WorkManager với 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 {
// Đang thực thi trên Dispatchers.Default (mặc định)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Mô phỏng đồng bộ hóa
delay(1000)
}
}
// Khởi chạy tác vụ WorkManager với ràng buộc
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 kế thừa CoroutineWorker — phiên bản Worker hỗ trợ coroutines. doWork() thực thi trên Dispatchers.Default, chuyển sang IO cho thao tác mạng qua withContext. Constraints đảm bảo đồng bộ chỉ chạy khi có mạng và pin không dưới mức thấp. BackoffCriteria với EXPONENTIAL tăng khoảng cách giữa các lần thử lại: 10, 20, 40 giây.
Cho các tác vụ định kỳ (đồng bộ mỗi 15 phút, gửi phân tích mỗi giờ), WorkManager cung cấp PeriodicWorkRequestBuilder. Khoảng thời gian tối thiểu là 15 phút. Không giống OneTimeWorkRequest, PeriodicWorkRequest không đảm bảo tuân thủ chính xác khoảng thời gian — hệ thống có thể gộp nhiều tác vụ định kỳ để tiết kiệm pin. Cho khoảng thời gian chính xác, hãy dùng AlarmManager, nhưng lưu ý các hạn chế của Android 12+ về báo thức chính xác.
Lỗi đầu tiên — tạo Thread mới cho mỗi tác vụ. new Thread().start() tạo một luồng native cấp phát ~1 MB cho stack. Với 100 tác vụ song song, đó là 100 MB chỉ cho stack, cộng với chi phí chuyển đổi ngữ cảnh. Hãy dùng nhóm luồng: Executors.newFixedThreadPool(n) (Android) hoặc DispatchQueue.global() (iOS) — chúng tái sử dụng luồng, giảm chi phí xuống nhiều lần.
Lỗi thứ hai — truy cập trạng thái có thể thay đổi từ nhiều luồng nền mà không đồng bộ hóa. Nếu hai luồng nền cùng ghi vào cùng một ArrayList hoặc HashMap, điều kiện race xảy ra: ConcurrentModificationException trên Android, hỏng dữ liệu trên iOS. Giải pháp: dùng collection an toàn luồng (ConcurrentHashMap, CopyOnWriteArrayList) hoặc tuần tự hóa truy cập qua một hàng đợi duy nhất (DispatchQueue serial).
Lỗi thứ ba — tác vụ nền không có quản lý vòng đời. Khởi chạy coroutine trong phạm vi toàn cục mà không gắn với vòng đời Activity hoặc ViewModel dẫn đến rò rỉ: tác vụ tiếp tục chạy sau khi màn hình bị hủy. Trên Android, sử dụng lifecycleScope (Activity/Fragment) hoặc viewModelScope (ViewModel). Trên iOS, sử dụng weak self trong closure và hủy tác vụ khi deinit.
Câu hỏi thường gặp
Background Thread — luồng thực thi các thao tác không liên quan đến UI: yêu cầu mạng, đọc/ghi tệp, phân tích JSON, tính toán. Nó giải phóng Main Thread khỏi công việc nặng, giữ cho giao diện phản hồi nhanh. Trên iOS, luồng nền được quản lý qua GCD (DispatchQueue.global); trên Android, qua Executors hoặc Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO được thiết kế cho thao tác I/O chặn: đọc tệp, yêu cầu mạng, làm việc với DB. Nó có nhóm 64 luồng. Dispatchers.Default dành cho tác vụ nặng về CPU: sắp xếp, lọc, xử lý ảnh. Nhóm của nó bằng số nhân CPU. Dùng Dispatchers.Default cho thao tác I/O có thể chặn tất cả nhân, và Dispatchers.IO cho tác vụ CPU có thể tạo quá nhiều luồng.
DispatchQueue.global(qos: .background).async { } gửi một khối vào hàng đợi nền toàn cục. Sau khi hoàn thành công việc nền, cần quay lại luồng chính qua DispatchQueue.main.async { } để cập nhật UI. Cho tác vụ nền tuần tự, dùng OperationQueue với maxConcurrentOperationCount = 1 hoặc DispatchQueue(label: "serial").
Số lượng khuyến nghị luồng nền bằng số nhân CPU cộng 1 cho tác vụ IO-bound. Trên thiết bị 8 nhân hiện đại, đó là 9 luồng. Tạo hàng trăm luồng dẫn đến thread starvation: HĐH tốn nhiều thời gian chuyển đổi ngữ cảnh hơn là thực thi tác vụ. GCD trên iOS và Executors trên Android tự động tối ưu nhóm luồng cho thiết bị hiện tại.
Trong Kotlin Coroutines, việc quay lại Main Thread xảy ra tự động nếu coroutine được khởi chạy trong phạm vi Main (lifecycleScope.launch, viewModelScope.launch). Hàm withContext(Dispatchers.IO) tạm dừng coroutine trên luồng IO, và sau khi hoàn thành tự động tiếp tục trên bộ điều phối nơi nó được khởi chạy (thường là Main). Không cần gọi tường minh DispatchQueue.main.async.
Tổng kết
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.
Đọc thêm