Background Thread trong phát triển di động: khái niệm, nhiệm vụ và phương pháp sử dụng

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

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 thực hiện các thao tác chặn UI: mạng, tệp, JSON, tính toán
  • iOS: DispatchQueue.global(qos:) và OperationQueue cho tác vụ nền
  • Android: Dispatchers.IO (mạng/tệp), Dispatchers.Default (tính toán), WorkManager (tác vụ nền)
  • Coroutines — tiêu chuẩn hiện đại cho công việc nền: withContext(Dispatchers.IO) chuyển luồng không cần callback hell
  • Kết quả từ Background Thread luôn được trả về Main Thread để cập nhật UI

Background Thread là gì

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.

Background Thread trên iOS: GCD và DispatchQueue.global

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.

swift
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.

Hàng đợi nền Serial vs Concurrent

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").

Background Thread trên Android: Executors và Dispatchers

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.

kotlin
// 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).

Coroutines như tiêu chuẩn hiện đại cho tác vụ nền

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.

kotlin
// 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ự.

Đồng thời có cấu trúc: ngăn chặn rò rỉ

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: tác vụ nền cho Android

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).

kotlin
// 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.

PeriodicWorkRequest cho tác vụ nền định kỳ

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.

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

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 trong ứng dụng di động là gì?

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).

Sự khác biệt giữa Dispatchers.IO và Dispatchers.Default là gì?

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.

Làm thế nào để chuyển sang luồng nền trên iOS?

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").

Có thể tạo bao nhiêu luồng nền trong ứng dụng di động?

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.

Có cần quay lại Main Thread sau coroutine không?

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

  • Background Thread — luồng cho thao tác không nên chạy trên Main Thread: mạng, tệp, phân tích JSON, tính toán
  • iOS: DispatchQueue.global(qos:) và OperationQueue là API chính cho tác vụ nền với hỗ trợ QoS
  • Android: Executors, HandlerThread, WorkManager cho Java; Dispatchers.IO/Default + coroutines cho Kotlin
  • Coroutines với withContext chuyển luồng không cần callback và không chặn (cơ chế suspend)
  • WorkManager đảm bảo thực thi tác vụ nền ngay cả sau khi khởi động lại thiết bị, tôn trọng các ràng buộc
  • Lỗi: tạo Thread mới thay vì dùng nhóm, điều kiện race khi truy cập trạng thái thay đổi, rò rỉ do thiếu gắn kết vòng đời
  • Kết quả từ luồng nền luôn được trả về Main Thread: qua Dispatchers.Main (Android) hoặc DispatchQueue.main.async (iOS)

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