Một số lượng lớn các coroutines, mặc dù nhẹ, vẫn có thể là một vấn đề trong các ứng dụng đòi hỏi
Tôi muốn xóa tan lầm tưởng về "quá nhiều điều tra" là một vấn đề bằng cách định lượng chi phí thực tế của chúng.
Đầu tiên, chúng ta nên gỡ các coroutine bản thân từ bối cảnh coroutine mà nó được đính kèm. Đây là cách bạn chỉ tạo một quy trình đăng ký với chi phí tối thiểu:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
Giá trị của biểu thức này là Job
giữ một quy trình đăng quang bị đình chỉ. Để giữ lại phần tiếp theo, chúng tôi đã thêm nó vào danh sách trong phạm vi rộng hơn.
Tôi đã đánh giá tiêu chuẩn cho mã này và kết luận rằng nó phân bổ 140 byte và mất 100 nano giây để hoàn thành. Vì vậy, đó là mức độ nhẹ của một quy trình đăng ký.
Để có thể tái tạo, đây là mã tôi đã sử dụng:
fun measureMemoryOfLaunch() {
val continuations = ContinuationList()
val jobs = (1..10_000).mapTo(JobList()) {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
}
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
class JobList : ArrayList<Job>()
class ContinuationList : ArrayList<Continuation<Unit>>()
Mã này bắt đầu một loạt các coroutines và sau đó ngủ để bạn có thời gian phân tích đống bằng một công cụ giám sát như VisualVM. Tôi đã tạo các lớp chuyên biệt JobList
và ContinuationList
vì điều này giúp phân tích kết xuất đống dễ dàng hơn.
Để có được một câu chuyện hoàn chỉnh hơn, tôi đã sử dụng đoạn mã dưới đây để đo lường chi phí withContext()
và async-await
:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis
const val JOBS_PER_BATCH = 100_000
var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()
fun main(args: Array<String>) {
try {
measure("just launch", justLaunch)
measure("launch and withContext", launchAndWithContext)
measure("launch and async", launchAndAsync)
println("Black hole value: $blackHoleCount")
} finally {
threadPool.shutdown()
}
}
fun measure(name: String, block: (Int) -> Job) {
print("Measuring $name, warmup ")
(1..1_000_000).forEach { block(it).cancel() }
println("done.")
System.gc()
System.gc()
val tookOnAverage = (1..20).map { _ ->
System.gc()
System.gc()
var jobs: List<Job> = emptyList()
measureTimeMillis {
jobs = (1..JOBS_PER_BATCH).map(block)
}.also { _ ->
blackHoleCount += jobs.onEach { it.cancel() }.count()
}
}.average()
println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}
fun measureMemory(name:String, block: (Int) -> Job) {
println(name)
val jobs = (1..JOBS_PER_BATCH).map(block)
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
val justLaunch: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {}
}
}
val launchAndWithContext: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
withContext(ThreadPool) {
suspendCoroutine<Unit> {}
}
}
}
val launchAndAsync: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
async(ThreadPool) {
suspendCoroutine<Unit> {}
}.await()
}
}
Đây là đầu ra điển hình mà tôi nhận được từ đoạn mã trên:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
Có, async-await
mất khoảng thời gian dài gấp đôi withContext
, nhưng nó vẫn chỉ là một micro giây. Bạn sẽ phải khởi chạy chúng trong một vòng lặp chặt chẽ, gần như không làm gì ngoài việc đó trở thành "vấn đề" trong ứng dụng của bạn.
Sử dụng, measureMemory()
tôi thấy chi phí bộ nhớ sau cho mỗi cuộc gọi:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
Chi phí của async-await
chính xác cao hơn 140 byte so với withContext
, con số chúng tôi nhận được là trọng lượng bộ nhớ của một chương trình đăng quang. Đây chỉ là một phần nhỏ của chi phí hoàn chỉnh cho việc thiết lập CommonPool
bối cảnh.
Nếu hiệu suất / tác động bộ nhớ là tiêu chí duy nhất để quyết định giữa withContext
và async-await
, thì kết luận sẽ là không có sự khác biệt liên quan giữa chúng trong 99% trường hợp sử dụng thực tế.
Lý do thực sự là withContext()
một API đơn giản hơn và trực tiếp hơn, đặc biệt là về mặt xử lý ngoại lệ:
- Một ngoại lệ không được xử lý bên trong
async { ... }
sẽ khiến công việc chính của nó bị hủy. Điều này xảy ra bất kể cách bạn xử lý các ngoại lệ từ kết hợp await()
. Nếu bạn chưa chuẩn bị coroutineScope
cho nó, nó có thể làm hỏng toàn bộ ứng dụng của bạn.
- Một ngoại lệ không được xử lý bên trong
withContext { ... }
chỉ đơn giản là bị ném bởi withContext
cuộc gọi, bạn xử lý nó giống như bất kỳ ngoại lệ nào khác.
withContext
cũng sẽ được tối ưu hóa, tận dụng thực tế là bạn đang tạm dừng quy trình đăng ký của cha mẹ và chờ đợi đứa trẻ, nhưng đó chỉ là một phần thưởng thêm.
async-await
nên dành riêng cho những trường hợp bạn thực sự muốn đồng thời, để bạn khởi chạy một số quy trình trong nền và chỉ sau đó chờ đợi chúng. Nói ngắn gọn:
async-await-async-await
- đừng làm vậy, sử dụng withContext-withContext
async-async-await-await
- đó là cách sử dụng nó.
withContext
, một quy trình đăng ký mới luôn được tạo bất kể. Đây là những gì tôi có thể thấy từ mã nguồn.