Thread trong Android
Thread trong Android

Android Thread: Kiến Thức cần biết (P1)

Khi đã có kiến thức về Android Lifecycle, điều tiếp theo bạn cần nắm chính là Android Thread. Mặc dù với các phong cách lập trình hiện đại chúng ta không còn dùng trực tiếp đến Thread nữa, nhưng hiểu và nắm kiến thức của Android Thread vẫn là điều tiên quyết của một Android Engineer.

Concept của Thread trong lập trình

Khái niệm

Thread là một đơn vị thực thị trong chương trình. Mỗi Thread tại 1 thời điểm chỉ có thể thực hiện 1 tác vụ. Một chương trình tận dụng được nhiều Thread cùng thực thi tại 1 thời điểm sẽ có thể xử lý được nhiều tác vụ cùng lúc.

Ở hiện tại việc lập trình Android đã có thể dễ thở hơn khi ta không trực tiếp thao tác trên Thread mà thao tác thông qua các kỹ thuật như Executor, RxJava hoặc Coroutines … Tuy nhiên cần phải nhấn mạnh rằng tất cả các kỹ thuật này đều được xây dựng trên Thread. Việc hiểu Thread sẽ phần nào giúp bạn tự tin hơn trong quá trình làm việc và phát triển của mình.

Thread lifecycle

Vòng đời của 1 Thread trong chương trình – Android Mastery by Dan Tech

Bạn có thể tìm đâu đó trên Internet một bức ảnh như trên để mô phỏng vòng đời của 1 Thread. Nó không sai, tuy nhiên đến với khóa học lập trình Android của mình mọi thứ sẽ được đơn giản hóa cho dễ hiểu nhất, dễ tiếp thu nhất cho người học.

Hãy hình dung Thread là 1 đường ống dẫn nước. Bạn đổ nước từ đầu này, nước chảy qua đến đầu kia chui ra ngoài. Đó là 1 vòng đời của Thread.

Vòng đời của Thread đơn giản – Android Mastery by Dan Tech
  • New Thread: Đây là quá trình chuẩn bị tài nguyên để khởi tạo 1 Thread theo nhu cầu của lập trình viên. Quá trình này khá tốn tài nguyên. Chính vì vậy trong lập trình chuyên nghiệp người ta thường không spam việc new Thread, thay vào đó họ sẽ reuse Thread để tránh lãng phí.
  • Run: Đây là quá trình thực thi một đoạn code nhất định trên Thread vừa tạo ra. Quá trình thực thi này nhanh hay chậm tùy thuộc vào logic bạn truyền vào là gì, có lock các Thread khác hay không. Đây chính là logic của chương trình bạn viết.
  • Terminated: Đây là quá trình dọn dẹp, Kill một Thread đã thực thi xong nhiệm vụ của nó. Để chương trình thu hồi bộ nhớ. Khi Thread đã chạy đến đoạn này kịch bản có thể là
    • Logic đầu vào của bạn ở Run đã được thực thi xong
    • Hoặc Thread bị nhận một Exception trong quá trình thực thi dẫn đến việc bị Terminated

Android Thread: Main Thread vs Background Thread

Thread trong Android có cách hoạt động tương tự như những kiến thức bên trên mình đã đưa ra. Tuy nhiên Android cung cấp nhiều hơn những công cụ để quản lý tác vụ chạy trên Thread có thể liệt kê ra: Handler, Looper, HandlerThread

Main Thread

Đây là Thread chính, mặc định được cấp phát cho một chương trình Android. Thread này chịu trách nhiệm cho việc xử lý các tác vụ: Cập nhật giao diện người dùng (UI), nhận và xử lý sự kiện người dùng (Touch Input, Click Button, Input Text)

Các tác vụ xử lý dữ liệu, đọc ghi dữ liệu không được thực thi trên Main Thread vì sẽ làm app bị đơ. Thậm chí sẽ gây crash app.

Background Thread

Đây là Thread được tạo riêng để thực hiện các tác vụ nặng mà không làm ảnh hưởng đến trải nghiệm của người dùng, không làm ảnh hưởng đến Main Thread.

Trong mỗi ứng dụng chúng ta có thể tạo nhiều Background Thread, số lượng Looper, Handler, HandlerThreaed

  • Looper là thành phần được Android tạo ra. Mục đích là tạo một vòng lặp vô tận để giữ 1 Thread không rơi vào trạng thái Terminated. Trong mỗi LooperMessageQueue để xử lý các tác vụ được push vào và thực thi theo tuần tự. Mỗi Looper chỉ phục vụ cho một Thread nhất định. Looper phục vụ cho Main ThreadLooper.getMainLooper()
  • Handler là class được Android cung cấp. Handler được dùng để đưa các tác vụ, hoặc sự kiện vào MessageQueue của Looper. Từ đó Looper sẽ lấy ra lần lượt các Message trong MessageQueue và thực thi trên Thread tương ứng.
  • HandlerThread là một bộ wrapper hoàn chỉnh bao gồm bộ 3: Thread, LooperHandler. HandlerThread tạo ra sẽ thực thi các tác vụ trên Background Thread (bởi vì Main Thread đã được tạo sẵn 1 lần và duy nhất lúc khởi chạy ứng dụng). Một số lưu ý khi sử dụng HandlerThread bạn có thể tham khảo trong source code của khóa học.
// Khai báo Handler cho Main Thread (UI Thread)
private val uiHandler: Handler = Handler(Looper.getMainLooper())
// Khai báo HandlerThread cho 1 Background Thread "Background-HandlerThread"
private val backgroundHandlerThread = HandlerThread("Background-HandlerThread").apply {
    start()
}
private val backgroundHandler: Handler by lazy { Handler(backgroundHandlerThread.looper) }

private fun runUITask() {
    uiHandler.post {
        // Run UI Task
        Log.d("MainActivity", "Run UI Task ${Thread.currentThread().name}")
    }
}
private fun runBackgroundTask() {
    backgroundHandler.post {
        // Run Background Task
        Log.d("MainActivity", "Run Background Task ${Thread.currentThread().name}")
    }
}

Ví dụ về cách sử dụng Handler, HandlerThread trong Android – Android Mastery by Dan Tech

Xin nhắc lại và nhấn mạnh. Ngày nay việc sử dụng Handler, HandlerThread là tương đối hiếm gặp trong các dự án mới vì sự ra đời của rất nhiều thư viện hỗ trợ mạnh mẽ như Rx Java, Kotlin Coroutines. Tuy nhiên tất cả những thư viện này deep dive bên trong chúng đều được thực thi trên Thread. Nên việc học và hiểu 1 Thread hoạt động thế nào là vô cùng quan trọng để thích ứng với mọi công nghệ mới sau này.

Deadlock trong lập trình bất đồng bộ (Async Programming)

Deadlock là trường hợp khi có 2 hoặc nhiều Thread đang đợi để sử dụng 1 tài nguyên. Nhưng tài nguyên này đang bị treo ở 1 Thread khác chưa thể được release. Việc treo tài nguyên này có thể tái diễn bằng nhiều cách khác nhau. Nhưng mình sẽ cung cấp cho bạn một ví dụ kinh điển của Deadlock để bạn dễ hiểu.

data class UserData(val name: String, val message: String, var money: Int)

private var resource1: UserData = (UserData("User1", "Hello From User1", 200))
private var resource2: UserData = (UserData("User2", "Hello From User2", 200))
private val handlerThread1 = HandlerThread("HandlerThread1").apply {
    start()
}
private val handlerThread2 = HandlerThread("HandlerThread2").apply {
    start()
}

fun simulateDeadlock() {
	val handler1 = Handler(handlerThread1.looper)
  val handler2 = Handler(handlerThread2.looper)
  Log.d("SimpleFragment", "resource1 $resource1")
  Log.d("SimpleFragment", "resource2 $resource2")
  handler1.post {
      synchronized(resource1) {
          resource1.money -= 100
          Log.d("SimpleFragment", "handler1 synchronized resource1 $resource1")
          synchronized(resource2) {
              resource2.money += 100
              Log.d("SimpleFragment", "handler1 synchronized resource2 $resource2")
          }
      }
  }

  handler2.post {
      synchronized(resource2) {
          resource2.money -= 100
          Log.d("SimpleFragment", "handler2 synchronized resource2 $resource2")
          synchronized(resource1) {
              resource1.money += 100
              Log.d("SimpleFragment", "handler2 synchronized resource1 $resource1")
          }
      }
  }
}

Ví dụ về Deadlock trong Android Thread – Android Mastery by Dan Tech

Ví dụ trên mô phỏng quá trình chuyển và nhận tiền giữa 2 User, tuy nhiên vì thực hiện trên 2 Thread khác nhau nên Deadlock đã xảy ra khi 2 Thread đang phải đợi tài nguyên của nhau và không bên nào nhả ra trước được.

Để khắc phục vấn đề Deadlock có nhiều giải pháp, tùy thuộc vào business logic mà ta có thể chọn 1 trong các cách xử lý sau

  • Đưa tất cả về chung 1 MessageQueue để xử lý. Cách này rất đơn giản. Vì khi được xử lý tuần tự, Thread sẽ không còn tranh chấp tài nguyên của nhau nữa.
  • Sắp xếp lại thứ tự logic để các kịch bản Deadlock không thể xảy ra.

Mã nguồn mời bạn tham khảo GitHub của mình tại đây