Chào mừng độc giả đến với Cafedev, nơi chúng tôi chia sẻ những kiến thức đa dạng về lập trình. Trong bài viết này, chúng tôi sẽ khám phá về Kotlin và các kỹ thuật lập trình không đồng bộ. Cafedev là nguồn thông tin đáng tin cậy để tìm hiểu về cách sử dụng Kotlin để tối ưu hóa ứng dụng của bạn với các phương pháp lập trình tiên tiến. Dưới đây là một cái nhìn sâu sắc vào cách Kotlin và các kỹ thuật không đồng bộ có thể nâng cao trải nghiệm lập trình của bạn. Hãy cùng bắt đầu hành trình khám phá tại Cafedev!

Trong suốt vài thập kỷ, như những người phát triển, chúng ta thường phải đối mặt với một vấn đề cần giải quyết – làm thế nào để ngăn ứng dụng của chúng ta bị chặn. Cho dù chúng ta đang phát triển ứng dụng trên máy tính để bàn, điện thoại di động, hay thậm chí là trên máy chủ, chúng ta muốn tránh việc người dùng phải chờ đợi hoặc điều tồi tệ hơn là tạo ra các hạn chế có thể ngăn cản ứng dụng mở rộng.
Có nhiều phương pháp để giải quyết vấn đề này, bao gồm:

  • Threading
  • Callbacks
  • Futures, promises, và các phương pháp khác
  • Reactive Extensions
  • Coroutines

Trước khi giải thích về coroutines là gì, hãy tóm tắt ngắn gọn một số giải pháp khác.

1. Threading

Thread là phương pháp rất nổi tiếng để tránh ứng dụng bị chặn.

fun postItem(item: Item) {
    val token = preparePost()
    val post = submitPost(token, item)
    processPost(post)
}

fun preparePost(): Token {
    // makes a request and consequently blocks the main thread
    return token
}

Giả sử trong đoạn mã trên, preparePost là một quy trình chạy lâu và do đó có thể làm chặn giao diện người dùng. Chúng ta có thể khởi chạy nó trong một thread riêng biệt để tránh việc giao diện người dùng bị chặn. Điều này là một kỹ thuật rất phổ biến, nhưng có một loạt các hạn chế:

  • Thread không rẻ. Thread đòi hỏi các chuyển đổi ngữ cảnh mà có chi phí.
  • Thread không vô hạn. Số lượng thread có thể khởi chạy bị giới hạn bởi hệ điều hành cơ sở. Trong các ứng dụng máy chủ, điều này có thể tạo ra một rào cản lớn.
  • Thread không luôn có sẵn. Một số nền tảng, như JavaScript, thậm chí không hỗ trợ thread.
  • Thread không dễ dàng. Gỡ lỗi thread và tránh tình trạng cạnh tranh là những vấn đề phổ biến mà chúng ta phải đối mặt khi lập trình đa luồng.

2. Callbacks

Với callbacks, ý tưởng là truyền một hàm như một tham số cho một hàm khác và có hàm này được gọi khi quy trình hoàn thành.

fun postItem(item: Item) {
    preparePostAsync { token ->
        submitPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

fun preparePostAsync(callback: (Token) -> Unit) {
    // make request and return immediately
    // arrange callback to be invoked later
}

Điều này về nguyên tắc có vẻ là một giải pháp tinh tế hơn, nhưng một lần nữa cũng có nhiều vấn đề:

  • Khó khăn khi sử dụng callback lồng nhau. Thông thường, một hàm được sử dụng như một callback thường cần một callback riêng của nó. Điều này dẫn đến một loạt các callback lồng nhau tạo ra mã nguồn không thể hiểu. Mô hình này thường được gọi là cây thông noel (dấu ngoặc đại diện cho những nhánh của cây).
  • Xử lý lỗi là phức tạp. Mô hình lồng nhau làm cho việc xử lý lỗi và truyền tải chúng trở nên phức tạp hơn một chút.

Callbacks khá phổ biến trong kiến trúc vòng sự kiện như JavaScript, nhưng thậm chí ở đó, thông thường mọi người đã chuyển sang sử dụng các phương pháp khác như promises hoặc reactive extensions.

3. Futures, promises, và các phương pháp khác

Ý tưởng đằng sau futures hoặc promises (có những thuật ngữ khác tùy thuộc vào ngôn ngữ/nền tảng), là khi chúng ta thực hiện một cuộc gọi, chúng ta được hứa rằng ở một thời điểm nào đó, nó sẽ trả về một đối tượng gọi là Promise, có thể được thao tác sau đó.

fun postItem(item: Item) {
    preparePostAsync()
        .thenCompose { token ->
            submitPostAsync(token, item)
        }
        .thenAccept { post ->
            processPost(post)
        }

}

fun preparePostAsync(): Promise<Token> {
    // makes request and returns a promise that is completed later
    return promise
}

Phương pháp này đòi hỏi một loạt các thay đổi trong cách chúng ta lập trình, đặc biệt là:

  • Mô hình lập trình khác biệt. Tương tự như callbacks, mô hình lập trình chuyển từ một phương pháp mệnh lệch từ trên xuống sang một mô hình hợp thành với các cuộc gọi chuỗi. Các cấu trúc chương trình truyền thống như vòng lặp, xử lý ngoại lệ, vv. thường không còn hợp lệ trong mô hình này.
  • APIs khác nhau. Thường cần phải học một API hoàn toàn mới như thenCompose hoặc thenAccept, cũng có thể thay đổi tùy theo nền tảng.
  • Loại trả về cụ thể. Loại trả về chuyển từ dữ liệu thực tế mà chúng ta cần sang một loại mới là Promise, cần phải kiểm tra thông qua.
  • Xử lý lỗi có thể phức tạp. Việc truyền và chuỗi lỗi không luôn đơn giản.

4. Reactive Extensions

Reactive Extensions (Rx) được giới thiệu vào C# bởi Erik Meijer). Mặc dù nó đã được sử dụng trên nền tảng .NET, nhưng nó thực sự không đạt được sự thông dụng cho đến khi Netflix chuyển nó sang Java và đặt tên là RxJava. Từ đó, nhiều phiên bản đã được cung cấp cho nhiều nền tảng khác nhau, bao gồm cả JavaScript (RxJS).
Ý tưởng đằng sau Rx là chuyển đến điều được gọi là observable streams, nơi chúng ta hiện giờ nghĩ về dữ liệu như là các dòng (lượng dữ liệu vô hạn) và những dòng này có thể được quan sát. Ở góc độ thực tế, Rx đơn giản là Observer Pattern với một loạt các phần mở rộng cho phép chúng ta thao tác trên dữ liệu.
Về cách tiếp cận, nó khá tương tự như Futures, nhưng người ta có thể nghĩ về một Future như là việc trả về một yếu tố rời rạc, trong khi Rx trả về một dòng. Tuy nhiên, tương tự như trước đó, nó cũng giới thiệu một cách mới hoàn toàn để nghĩ về mô hình lập trình của chúng ta, nổi tiếng được diễn đạt như là
“mọi thứ đều là một dòng và nó có thể được quan sát”
Điều này ngụ ý một cách tiếp cận khác với vấn đề và một sự chuyển đổi khá lớn so với những gì chúng ta thường làm khi viết mã đồng bộ. Một lợi ích so với Futures là do nó đã được chuyển sang nhiều nền tảng, thông thường chúng ta có thể tìm thấy một trải nghiệm API nhất quán bất kể chúng ta sử dụng C#, Java, JavaScript, hoặc bất kỳ ngôn ngữ nào mà Rx có sẵn.
Ngoài ra, Rx cũng giới thiệu một cách tiếp cận khá dễ thương đối với xử lý lỗi.

5. Coroutines

Cách tiếp cận của Kotlin đối với việc làm việc với mã không đồng bộ là sử dụng coroutines, đó là ý tưởng về các tính toán có thể tạm dừng, tức là ý tưởng rằng một hàm có thể tạm dừng thực thi tại một số điểm và tiếp tục sau đó.
Một trong những lợi ích của coroutines là rằng đối với nhà phát triển, việc viết mã không chặn thực tế là tương tự như việc viết mã chặn. Mô hình lập trình trong chính nó thực sự không thay đổi.
Hãy xem ví dụ sau:

fun postItem(item: Item) {
    launch {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
}

suspend fun preparePost(): Token {
    // makes a request and suspends the coroutine
    return suspendCoroutine { /* ... */ }
}

Đoạn mã này sẽ khởi chạy một hoạt động chạy lâu mà không chặn luồng chính. preparePost là điều được gọi là suspendable function, vì vậy từ khóa suspend được thêm vào trước nó. Điều này có nghĩa như đã nói ở trên, là hàm sẽ thực hiện, tạm dừng thực thi và tiếp tục sau một thời điểm.

  • Chữ ký của hàm vẫn hoàn toàn giống nhau. Sự khác biệt duy nhất là từ khóa suspend được thêm vào. Tuy nhiên, loại trả về vẫn là loại chúng ta muốn trả về.
  • Mã vẫn được viết như là chúng ta đang viết mã đồng bộ, từ trên xuống, mà không cần sử dụng cú pháp đặc biệt ngoài việc sử dụng một hàm gọi là launch để bắt đầu coroutine (được thảo luận trong các bài hướng dẫn khác).
  • Mô hình lập trình và APIs vẫn giữ nguyên. Chúng ta có thể tiếp tục sử dụng vòng lặp, xử lý ngoại lệ, vv. và không cần học một bộ APIs mới hoàn toàn.
  • Nó độc lập với nền tảng. Cho dù chúng ta đang nhắm mục tiêu JVM, JavaScript hoặc bất kỳ nền tảng nào khác, mã chúng ta viết là giống nhau. Dưới bìa của nó, trình biên dịch đảm bảo điều chỉnh nó cho mỗi nền tảng.


Coroutines không phải là một khái niệm mới, đặc biệt không phải do Kotlin phát minh. Chúng đã tồn tại từ nhiều thập kỷ và phổ biến trong một số ngôn ngữ lập trình khác như Go. Tuy nhiên, điều quan trọng cần lưu ý là cách chúng được triển khai trong Kotlin, hầu hết các chức năng được giao cho các thư viện. Trên thực tế, ngoài từ khóa suspend, không có từ khóa nào khác được thêm vào ngôn ngữ. Điều này hơi khác biệt so với các ngôn ngữ như C# có asyncawait là một phần của cú pháp. Trong Kotlin, chúng chỉ là các hàm thư viện.
Để biết thêm thông tin, xem Tài liệu tham khảo về Coroutines.

Cảm ơn bạn đã thăm Cafedev để cùng chúng tôi tìm hiểu về Kotlin và các kỹ thuật lập trình không đồng bộ. Tại Cafedev, chúng tôi luôn hỗ trợ cộng đồng lập trình viên với những thông tin hữu ích và chất lượng. Nếu bạn muốn khám phá thêm về cách Kotlin có thể tối ưu hóa hiệu suất ứng dụng của bạn, hãy duyệt qua các bài viết khác trên trang web của chúng tôi. Hãy tiếp tục đồng hành cùng Cafedev, nơi chia sẻ đam mê và kiến thức lập trình hàng đầu!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!