Thread trong phát triển di động — khái niệm, loại và quản lý luồng

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

Thread — đơn vị cơ bản của thời gian xử lý CPU, có ngăn xếp riêng và thực thi độc lập với các luồng khác. Trong phát triển di động, các luồng được sử dụng để thực thi song song các tác vụ, giúp giao diện luôn phản hồi trong các thao tác kéo dài. Android hỗ trợ java.lang.Thread, Executors và Kotlin Coroutines, iOS — Thread (Objective-C), GCD và OperationQueue. Theo Tài liệu Thread Android, việc tạo một luồng gốc yêu cầu cấp phát ~1 MB cho ngăn xếp bởi hệ điều hành.

Chính yếu

  • Thread — đơn vị lập lịch CPU tối thiểu: mỗi luồng độc lập và có ngăn xếp riêng
  • Tạo luồng cần ~1 MB ngăn xếp trên Android và 512 KB trên iOS, do đó pool hiệu quả hơn tạo trực tiếp
  • Android: Thread, Executors, HandlerThread, Coroutines — bốn mức trừu tượng luồng
  • iOS: Thread (cấp thấp), GCD (DispatchQueue), OperationQueue (cấp cao)
  • Thread safety — truy cập chung vào dữ liệu mutable cần đồng bộ hóa: locks, atomic, serial queues

Thread là gì

Thread (luồng thực thi) — là một chuỗi lệnh độc lập mà hệ điều hành có thể lập lịch trên một lõi CPU. Mỗi tiến trình (ứng dụng) chứa ít nhất một luồng — Main Thread. Các luồng bổ sung được tạo ra để thực thi song song các tác vụ. Mỗi luồng có ngăn xếp phần mềm riêng (với biến cục bộ), bộ đếm lệnh (PC) và thanh ghi. Bộ nhớ heap được chia sẻ cho tất cả các luồng của tiến trình.

Trong hệ điều hành di động, các luồng được lập lịch bằng đa nhiệm ưu tiên (preemptive multitasking): HĐH có thể ngắt luồng bất kỳ lúc nào và chuyển điều khiển cho luồng khác (context switch). Chuyển đổi ngữ cảnh là thao tác tốn kém (1-10 micro giây), vì nó yêu cầu lưu/phục hồi thanh ghi CPU, cập nhật TLB và xóa bộ nhớ đệm. Đó là lý do tại sao số lượng luồng quá nhiều (hàng trăm, hàng nghìn) làm giảm hiệu suất — HĐH dành nhiều thời gian cho chuyển đổi hơn là thực thi.

Luồng và tiến trình — là các khái niệm khác nhau. Tiến trình là một phiên bản ứng dụng với bộ nhớ ảo được cấp phát riêng. Luồng bên trong tiến trình chia sẻ bộ nhớ này với các luồng khác. Trong Android, mỗi thành phần ứng dụng (Activity, Service, BroadcastReceiver) hoạt động trong một tiến trình, nhưng có thể thực thi trên các luồng khác nhau. Ứng dụng iOS cũng là một tiến trình duy nhất với khả năng tạo các luồng bổ sung qua GCD hoặc Thread.

Vòng đời của luồng: trạng thái và chuyển đổi

Mỗi luồng trong Java/Kotlin (Android) và NSThread (iOS) trải qua năm trạng thái: New (được tạo), Runnable (sẵn sàng thực thi), Running (đang chạy trên CPU), Blocked/Waiting (chờ tài nguyên hoặc thông báo), Terminated (kết thúc). Các chuyển đổi giữa các trạng thái được quản lý bởi bộ lập lịch HĐH và các nguyên hàm đồng bộ hóa. Nhà phát triển có thể ảnh hưởng đến mức ưu tiên của luồng (Thread.setPriority()) và trạng thái của nó (sleep, join, interrupt).

Trong Android, một luồng chuyển sang trạng thái Blocked khi cố gắng chiếm monitor đang bận (synchronized), gọi Object.wait() hoặc Thread.sleep(). Trong iOS — khi gọi NSCondition.wait(), pthread_cond_wait() hoặc dispatch_semaphore_wait(). Ở trạng thái Blocked, luồng không tiêu thụ CPU nhưng chiếm bộ nhớ (ngăn xếp). Một luồng có thể bị ngắt (interrupted) từ luồng khác, nhận InterruptedException (Java) hoặc kiểm tra isCancelled (Kotlin Coroutines).

Trạng tháiMô tảPhương thức chuyển đổi
NewLuồng được tạo nhưng chưa chạyHàm tạo Thread()
RunnableLuồng sẵn sàng thực thi, chờ CPUthread.start()
RunningLuồng đang chạy trên lõi CPUBộ lập lịch HĐH
Blocked/WaitingLuồng chờ tài nguyên, monitor hoặc thông báosynchronized, wait(), sleep()
TerminatedLuồng đã kết thúc run() hoặc bị ngắtrun() kết thúc, interrupt()

Context Switch và chi phí của nó

Context switch (chuyển đổi ngữ cảnh) — thao tác HĐH lưu trạng thái của luồng hiện tại (thanh ghi, PC, TLB) và tải trạng thái đã lưu của luồng khác. Trong hệ thống di động (Linux + ART, XNU cho iOS), context switch mất 1-10 micro giây. Nếu một luồng thực thi tác vụ trong 100 micro giây và context switch mất 5, thì 5% thời gian bị lãng phí. Để giảm thiểu context switch, iOS sử dụng GCD với work stealing, Android — các pool với fixedThreadCount.

Thread trong Android: từ Thread đến Coroutines

Android đã tiến hóa từ java.lang.Thread cấp thấp đến coroutines hiện đại. Mỗi mức trừu tượng mang lại nhiều khả năng hơn với chi phí thấp hơn. Thread là lớp cơ bản, nhưng việc tạo trực tiếp không được khuyến nghị: luồng mới không được quản lý bởi pool, khó giám sát và hủy bỏ. AsyncTask (không dùng nữa từ API 30) là một bước tiến, nhưng bị rò rỉ bộ nhớ và xử lý cấu hình bất tiện.

HandlerThread là một lớp con đặc biệt của Thread với Looper, có thể xử lý hàng đợi tin nhắn. Nó được sử dụng để thực thi tuần tự các tác vụ trên luồng nền, ví dụ ghi dữ liệu vào Room hoặc tệp tin. HandlerThread được tạo bằng cách gọi start(), sau đó qua Handler(handlerThread.looper) có thể gửi tin nhắn và Runnable. Gọi handlerThread.quit() dừng Looper và kết thúc luồng.

kotlin
// Android: Thread, HandlerThread và Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors

class ThreadExample {

    // 1. Tạo Thread trực tiếp (không khuyến nghị)
    fun directThread() {
        val thread = Thread(Runnable {
            Thread.sleep(1000)
            print("Direct thread executed")
        })
        thread.start()
    }

    // 2. HandlerThread cho tác vụ nền tuần tự
    fun handlerThreadExample() {
        val handlerThread = HandlerThread("BackgroundQueue")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            // Thực thi tuần tự trên luồng nền
            Thread.sleep(500)
            print("HandlerThread: tác vụ hoàn thành")
        }

        // Dừng luồng (thực thi khi tác vụ hoàn tất)
        handlerThread.quitSafely()
    }

    // 3. Executors — pool luồng
    fun executorExample() {
        val executor = Executors.newFixedThreadPool(4)
        for (i in 1..10) {
            executor.execute {
                print("Task $i on thread ${Thread.currentThread().getName()}")
            }
        }
        executor.shutdown()
    }

    // 4. Kotlin Coroutines — tiêu chuẩn hiện đại
    suspend fun coroutineExample() = kotlinx.coroutines.withContext(
        kotlinx.coroutines.Dispatchers.Default
    ) {
        print("Coroutine on thread: ${Thread.currentThread().getName()}")
    }
}

Ví dụ ThreadExample cho thấy cả bốn mức trừu tượng luồng trong Android. Tạo Thread trực tiếp là cách tiếp cận cấp thấp nhất và kém hiệu quả nhất. HandlerThread hữu ích cho các tác vụ tuần tự nền. Executors.newFixedThreadPool(4) tạo pool 4 luồng để thực thi song song tối đa 10 tác vụ. Kotlin Coroutines với Dispatchers.Default — cách hiện đại, hiệu quả và an toàn.

HandlerThread: tác vụ nền tuần tự

HandlerThread — lớp con chuyên biệt của Thread với Looper tích hợp và hàng đợi tin nhắn. Nó được tạo bằng cách gọi start(), sau đó qua Handler(handlerThread.looper) có thể gửi Runnable và tin nhắn. HandlerThread thực thi các tác vụ theo đúng thứ tự tuần tự — tác vụ tiếp theo không bắt đầu cho đến khi tác vụ trước kết thúc. Điều này thuận tiện cho việc ghi dữ liệu vào Room hoặc tệp tin, nơi thứ tự thao tác rất quan trọng. Gọi quitSafely() dừng Looper sau khi hoàn thành tác vụ hiện tại.

Thread trong iOS: Thread, GCD và OperationQueue

iOS cũng cung cấp ba mức làm việc với luồng. Thread (Thread trong Swift, NSThread trong Objective-C) — API cấp thấp, tạo trực tiếp luồng gốc. GCD (Grand Central Dispatch) qua DispatchQueue — công cụ chính cho nhà phát triển iOS, tự động quản lý pool luồng. OperationQueue — trừu tượng cấp cao trên GCD với hỗ trợ phụ thuộc, ưu tiên và hủy bỏ.

Việc sử dụng trực tiếp Thread trong phát triển iOS hiện đại cực kỳ hiếm — GCD cung cấp tất cả khả năng cần thiết với quản lý bộ nhớ và luồng tự động. Thread chỉ được sử dụng cho các trường hợp đặc biệt: thiết lập thread-local storage (threadDictionary), tạo RunLoop cho luồng nền hoặc tích hợp với thư viện C mong đợi pthread_t.

swift
import Foundation

class ThreadManager {

    // 1. Thread (cấp thấp)
    func createThread() {
        let thread = Thread {
            // Mã được thực thi trên luồng mới
            print("Current thread: \(Thread.current)")
        }
        thread.name = "com.app.worker"
        thread.qualityOfService = .utility
        thread.start()
    }

    // 2. GCD — DispatchQueue
    func gcdExample() {
        // Hàng đợi song song
        let queue = DispatchQueue(label: "com.app.concurrent",
                                 qos: .utility,
                                 attributes: .concurrent)

        queue.async {
            print("GCD async task")
        }

        // Barrier để đồng bộ hóa ghi
        queue.async(flags: .barrier) {
            // Truy cập độc quyền trong khi ghi
            print("Barrier write: exclusive access")
        }
    }

    // 3. OperationQueue với phụ thuộc
    func operationQueueExample() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 2
        queue.qualityOfService = .background

        let download = BlockOperation {
            print("Downloading...")
        }
        let process = BlockOperation {
            print("Processing...")
        }
        let save = BlockOperation {
            print("Saving...")
        }

        // Phụ thuộc: download -> process -> save
        process.addDependency(download)
        save.addDependency(process)

        queue.addOperations([download, process, save], waitUntilFinished: false)
    }
}

// Bộ sưu tập thread-safe qua GCD barrier
class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.concurrent",
                                       attributes: .concurrent)

    var count: Int {
        return queue.sync { array.count } // concurrent read
    }

    func append(_ element: T) {
        queue.async(flags: .barrier) { // exclusive write
            self.array.append(element)
        }
    }
}

Lớp ThreadSafeArray minh họa mẫu Đọc đồng thời / Ghi độc quyền qua GCD barrier. Đọc qua queue.sync{} thực thi song song từ nhiều luồng. Ghi qua queue.async(flags: .barrier) chặn tất cả thao tác khác (cả đọc và ghi) cho đến khi hoàn thành ghi. Điều này hiệu quả hơn khối synchronized vì không chặn người đọc khi không có ghi.

iOS Thread vs GCD: khi nào dùng Thread trực tiếp

Sử dụng trực tiếp Thread trong iOS hợp lý trong ba trường hợp: cho thread-local storage (Thread.current.threadDictionary) — lưu dữ liệu gắn với luồng; để tạo RunLoop đặc biệt trên luồng nền với performSelector:onThread:; để tích hợp với thư viện C/C++ mong đợi pthread_t. Trong tất cả các trường hợp khác, GCD qua DispatchQueue được ưu tiên hơn — nó tự động quản lý pool luồng và mức tiêu thụ năng lượng.

Đồng bộ hóa luồng: locks, atomic, serial queues

Race condition (điều kiện tranh đua) xảy ra khi hai hoặc nhiều luồng đồng thời truy cập dữ liệu chung và ít nhất một luồng thực hiện ghi. Kết quả phụ thuộc vào thứ tự thực thi (timing) và không thể dự đoán. Để ngăn chặn race condition, sử dụng các nguyên hàm đồng bộ hóa. Trong phát triển di động, có sẵn khóa (synchronized, NSLock), thao tác nguyên tử (AtomicInteger, thuộc tính atomic iOS) và hàng đợi (serial queue).

Chọn nguyên hàm phụ thuộc vào kịch bản. Cho bộ đếm đơn giản và cờ, thao tác nguyên tử là đủ (AtomicInteger, thuộc tính atomic). Cho các đoạn quan trọng với nhiều thao tác — khóa (synchronized, NSLock). Cho cấu trúc dữ liệu phức tạp — serial DispatchQueue hoặc GCD barrier. Khóa dễ hiểu hơn nhưng dễ bị deadlock và livelock. Hàng đợi phức tạp hơn nhưng an toàn hơn.

kotlin
// Đồng bộ hóa trong Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Counter {

    // 1. AtomicInteger — cho bộ đếm đơn giản
    private val atomicCount = AtomicInteger(0)
    fun incrementAtomic() = atomicCount.incrementAndGet()

    // 2. synchronized — cho đoạn quan trọng
    @Synchronized
    fun synchronizedOperation() {
        // Chỉ một luồng tại một thời điểm
        doWork()
    }

    // 3. Mutex từ coroutine — suspend-safe
    private val mutex = Mutex()
    suspend fun mutexOperation() {
        mutex.withLock {
            // protected code — thread-safe
            doWork()
        }
    }

    private fun doWork() { /* critical section */ }
}

// Ví dụ Deadlock: A khóa B, B khóa A
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun methodA() = synchronized(lockA) {
        Thread.sleep(100)
        synchronized(lockB) { print("OK") }
    }

    fun methodB() = synchronized(lockB) {
        Thread.sleep(100)
        synchronized(lockA) { print("OK") }
    }
}

Counter minh họa ba cách tiếp cận đồng bộ hóa. AtomicInteger.incrementAndGet() — thao tác nguyên tử không khóa (CAS). @Synchronized — monitor tích hợp Java, khóa toàn bộ đối tượng. Mutex.withLock — mutex coroutine, tạm dừng coroutine thay vì khóa luồng (hiệu quả hơn). DeadlockExample cho thấy deadlock cổ điển: hai luồng chiếm khóa theo thứ tự khác nhau.

Thread Pools: tại sao Executors tốt hơn Thread

Thread Pool (pool luồng) — tập hợp các luồng được tạo trước, được tái sử dụng để thực thi tác vụ. Thay vì tạo luồng mới cho mỗi tác vụ (tốn kém), pool lấy một luồng có sẵn từ pool. Nếu không có luồng trống, tác vụ được xếp vào hàng đợi. Pool tự động quản lý kích thước: luồng mới được tạo khi tải cao, luồng nhàn rỗi kết thúc. Điều này giảm chi phí tạo luồng hàng chục lần.

Trong Android, Executors.newFixedThreadPool(4) tạo pool 4 luồng. Nếu 10 tác vụ đến cùng lúc, 4 bắt đầu ngay, 6 chờ trong hàng đợi. Executors.newCachedThreadPool() tạo luồng khi cần (không giới hạn) và kết thúc luồng nhàn rỗi sau 60 giây. Đối với iOS, GCD tự động cung cấp pool hàng đợi toàn cục, kích thước tương ứng với số lõi CPU và tải hiện tại.

Trong Kotlin Coroutines, pool luồng được ẩn bên trong các dispatcher. Dispatchers.Default sử dụng pool có kích thước bằng số lõi CPU (tối thiểu 2). Dispatchers.IO — 64 luồng (đủ cho hàng trăm tác vụ IO-bound, vì hầu hết sẽ chờ I/O mà không chiếm CPU). Mỗi dispatcher tự động điều chỉnh kích thước pool theo tải, tiết kiệm pin khi nhàn rỗi.

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

Thread là gì trong phát triển di động?

Thread — đơn vị thực thi mã cơ bản trong ứng dụng. Mỗi tiến trình có thể có nhiều luồng chia sẻ bộ nhớ nhưng có ngăn xếp riêng. Trong phát triển di động, các luồng được sử dụng để thực thi song song tác vụ mà không chặn UI. Android sử dụng Thread, Executors, HandlerThread và Coroutines. iOS sử dụng Thread, GCD (DispatchQueue) và OperationQueue.

Tại sao không nên tạo Thread trực tiếp?

Tạo Thread yêu cầu cấp phát ~1 MB ngăn xếp trong Android và ~512 KB trong iOS — đây là thao tác tốn kém. Với 1000 tác vụ, tạo trực tiếp 1000 luồng sẽ cần ~1 GB chỉ cho ngăn xếp cộng với chi phí context switch. Thay vì Thread, hãy sử dụng pool (Executors, GCD) hoặc coroutines — chúng tái sử dụng luồng, giảm chi phí hàng chục lần.

Race condition là gì và cách tránh?

Race condition — hành vi không thể dự đoán khi nhiều luồng đồng thời truy cập dữ liệu chung có ghi. Có thể tránh bằng ba cách: sử dụng kiểu nguyên tử (AtomicInteger), khóa (synchronized, NSLock) hoặc tuần tự hóa truy cập qua hàng đợi (DispatchQueue serial, Actor trong Kotlin). Thực hành tốt nhất là giảm thiểu trạng thái mutable chung và sử dụng immutability.

Thread khác coroutine như thế nào?

Thread — đối tượng hệ thống gốc, chiếm ~1 MB ngăn xếp và gắn với lõi HĐH. Coroutine — đơn vị thực thi nhẹ của Kotlin, không gắn với luồng cụ thể và có thể tạm dừng (suspend) mà không chặn. Một luồng có thể thực thi hàng nghìn coroutine. Coroutine hiệu quả hơn về bộ nhớ và cho phép viết mã bất đồng bộ không callback.

Làm thế nào để phát hiện deadlock trong ứng dụng di động?

Deadlock biểu hiện dưới dạng treo hoàn toàn ứng dụng mà không có ANR. Trong Android, sử dụng Thread.getAllStackTraces() để dump stack của tất cả luồng — hai luồng sẽ chờ khóa của nhau. Trong iOS — Thread.callStackSymbols. Công cụ: Android Studio Profiler (tab Threads), Instruments (iOS, Thread State View). Phòng ngừa: chiếm khóa theo thứ tự cố định, sử dụng tryLock với timeout.

Tổng kết

  • Thread — đơn vị CPU tối thiểu: thực thi độc lập với ngăn xếp riêng, bộ nhớ heap chung
  • Năm trạng thái của luồng: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android đã tiến hóa từ Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS cung cấp Thread, GCD (DispatchQueue) và OperationQueue — từ thấp đến cao
  • Race condition được giải quyết bằng khóa (synchronized, NSLock), kiểu nguyên tử và serial queues
  • Deadlock xảy ra khi chiếm khóa chéo — phòng ngừa bằng thứ tự cố định
  • Thread Pool hiệu quả hơn tạo Thread mới: tái sử dụng luồng, giảm chi phí context switch

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