Chủ đề vs Coroutines trong Kotlin

1. Giới thiệu

Trong hướng dẫn nhanh này, chúng ta sẽ tạo và thực thi các luồng trong Kotlin.

Sau đó, chúng ta sẽ thảo luận về cách tránh nó hoàn toàn, có lợi cho Kotlin Coroutines.

2. Tạo chủ đề

Tạo một luồng trong Kotlin cũng tương tự như trong Java.

Chúng tôi có thể mở rộng lớp Thread (mặc dù nó không được khuyến khích do Kotlin không hỗ trợ đa kế thừa):

class SimpleThread: Thread() { public override fun run() { println("${Thread.currentThread()} has run.") } }

Hoặc chúng ta có thể triển khai giao diện Runnable :

class SimpleRunnable: Runnable { public override fun run() { println("${Thread.currentThread()} has run.") } }

Và theo cách tương tự như chúng ta làm trong Java, chúng ta có thể thực thi nó bằng cách gọi phương thức start () :

val thread = SimpleThread() thread.start() val threadWithRunnable = Thread(SimpleRunnable()) threadWithRunnable.start()

Ngoài ra, giống như Java 8, Kotlin hỗ trợ SAM Conversions, do đó chúng ta có thể tận dụng nó và chuyển một lambda:

val thread = Thread { println("${Thread.currentThread()} has run.") } thread.start()

2.2. Kotlin thread () Hàm

Một cách khác là xem xét hàm thread () mà Kotlin cung cấp:

fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread

Với chức năng này, một luồng có thể được khởi tạo và thực thi đơn giản bằng cách:

thread(start = true) { println("${Thread.currentThread()} has run.") }

Hàm chấp nhận năm tham số:

  • bắt đầu - Để chạy ngay lập tức chuỗi
  • isDaemon - Để tạo chuỗi dưới dạng một chuỗi daemon
  • contextClassLoader - Một trình tải lớp để sử dụng để tải các lớp và tài nguyên
  • name - Để đặt tên của chuỗi
  • ưu tiên - Để đặt mức độ ưu tiên của chuỗi

3. Kotlin Coroutines

Thật hấp dẫn khi nghĩ rằng việc tạo ra nhiều luồng hơn có thể giúp chúng ta thực thi nhiều tác vụ cùng lúc. Thật không may, điều đó không phải lúc nào cũng đúng.

Tạo quá nhiều luồng thực sự có thể làm cho một ứng dụng hoạt động kém trong một số tình huống; luồng là các đối tượng áp đặt chi phí trong quá trình cấp phát đối tượng và thu gom rác.

Để khắc phục những vấn đề này, Kotlin đã giới thiệu một cách mới để viết mã không đồng bộ, không chặn; chương trình Coroutine.

Tương tự như các luồng, các coroutines có thể chạy đồng thời, đợi và giao tiếp với nhau với sự khác biệt là việc tạo ra chúng rẻ hơn so với các luồng.

3.1. Bối cảnh quy trình

Trước khi trình bày các trình xây dựng quy trình đăng ký mà Kotlin cung cấp sẵn, chúng ta phải thảo luận về Bối cảnh quy trình đăng ký.

Coroutines luôn thực thi trong một số ngữ cảnh là một tập hợp các phần tử khác nhau.

Các yếu tố chính là:

  • Công việc - mô hình hóa quy trình công việc có thể hủy bỏ với nhiều trạng thái và vòng đời đạt đến đỉnh điểm khi hoàn thành
  • Dispatcher - xác định luồng hoặc luồng mà chương trình điều chỉnh tương ứng sử dụng để thực thi nó. Với bộ điều phối, chúng ta có thể giới hạn việc thực thi quy trình đăng ký cho một chuỗi cụ thể, gửi nó đến một nhóm luồng hoặc để nó chạy không bị giới hạn

Chúng ta sẽ xem cách chỉ định ngữ cảnh trong khi chúng ta mô tả các quy trình trong các giai đoạn tiếp theo.

3.2. phóng

Hàm khởi chạy là một trình tạo chương trình đăng quang bắt đầu một chương trình đăng quang mới mà không chặn chuỗi hiện tại và trả về một tham chiếu đến chương trình đăng quang dưới dạng đối tượng Công việc :

runBlocking { val job = launch(Dispatchers.Default) { println("${Thread.currentThread()} has run.") } }

Nó có hai tham số tùy chọn:

  • context - Bối cảnh mà chương trình đăng quang được thực thi, nếu không được xác định, nó sẽ kế thừa ngữ cảnh từ CoroutineScope mà nó đang được khởi chạy từ
  • start - Các tùy chọn bắt đầu cho quy trình đăng quang. Theo mặc định, quy trình đăng ký ngay lập tức được lên lịch thực thi

Lưu ý rằng mã trên được thực thi trong một nhóm chủ đề nền được chia sẻ vì chúng tôi đã sử dụng Dispatchers.Default khởi chạy nó trong GlobalScope.

Ngoài ra, chúng ta có thể sử dụng GlobalScope.launch sử dụng cùng một trình điều phối:

val job = GlobalScope.launch { println("${Thread.currentThread()} has run.") }

Khi chúng tôi sử dụng Dispatchers.Default hoặc GlobalScope.launch, chúng tôi tạo một quy trình điều tra cấp cao nhất. Mặc dù trọng lượng nhẹ nhưng nó vẫn tiêu tốn một số tài nguyên bộ nhớ khi chạy.

Thay vì khởi chạy các coroutines trong GlobalScope, giống như chúng ta thường làm với các luồng (các luồng luôn mang tính toàn cục), chúng ta có thể khởi chạy các coroutines trong phạm vi hoạt động cụ thể mà chúng ta đang thực hiện:

runBlocking { val job = launch { println("${Thread.currentThread()} has run.") } }

Trong trường hợp này, chúng ta bắt đầu chương trình đăng quang mới bên trong trình tạo chương trình đăng quang runBlocking (mà chúng tôi sẽ mô tả sau) mà không chỉ định ngữ cảnh. Do đó, coroutine sẽ kế thừa ngữ cảnh của runBlocking .

3.3. không đồng bộ

Một chức năng mà Kotlin cung cấp để tạo ra một coroutine là async .

Hàm async tạo một quy trình đăng quang mới và trả về kết quả trong tương lai dưới dạng một thể hiện của Deferred:

val deferred = async { [email protected] "${Thread.currentThread()} has run." }

deferred is a non-blocking cancellable future which describes an object that acts as a proxy for a result that is initially unknown.

Like launch, we can specify a context in which to execute the coroutine as well as a start option:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) { println("${Thread.currentThread()} has run.") }

In this case, we've launched the coroutine using the Dispatchers.Unconfined which starts coroutines in the caller thread but only until the first suspension point.

Note that Dispatchers.Unconfined is a good fit when a coroutine does not consume CPU time nor updates any shared data.

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) { println("${Thread.currentThread()} has run.") }

Dispatchers.IO is recommended when we need to do intensive I/O operations.

3.4. runBlocking

We had an earlier look at runBlocking, but now let's talk about it in more depth.

runBlocking is a function that runs a new coroutine and blocks the current thread until its completion.

By way of example in the previous snippet, we launched the coroutine but we never waited for the result.

In order to wait for the result, we have to call the await() suspend method:

// async code goes here runBlocking { val result = deferred.await() println(result) }

await() is what’s called a suspend function. Suspend functions are only allowed to be called from a coroutine or another suspend function. For this reason, we have enclosed it in a runBlocking invocation.

Chúng tôi sử dụng runBlocking trong các chức năng chính và trong các thử nghiệm để chúng tôi có thể liên kết mã chặn với mã chặn khác được viết theo kiểu tạm ngưng.

Theo cách tương tự như chúng ta đã làm trong các trình xây dựng quy trình đăng quang khác, chúng ta có thể đặt bối cảnh thực thi:

runBlocking(newSingleThreadContext("dedicatedThread")) { val result = deferred.await() println(result) }

Lưu ý rằng chúng ta có thể tạo một chuỗi mới mà chúng ta có thể thực thi chương trình đăng quang. Tuy nhiên, một luồng chuyên dụng là một tài nguyên đắt tiền. Và, khi không cần thiết nữa, chúng ta nên giải phóng nó hoặc tốt hơn là sử dụng lại nó trong toàn bộ ứng dụng.

4. Kết luận

Trong hướng dẫn này, chúng tôi đã học cách thực thi mã không đồng bộ, không chặn bằng cách tạo một chuỗi.

Để thay thế cho chuỗi, chúng tôi cũng đã thấy cách tiếp cận của Kotlin để sử dụng coroutines rất đơn giản và thanh lịch.

Như thường lệ, tất cả các mẫu mã hiển thị trong hướng dẫn này đều có sẵn trên Github.