Structured Concurrency trong Kotlin
1. Giới thiệu
Structured Concurrency đảm bảo rằng coroutines được tổ chức theo cấu trúc phân cấp. Khi một coroutine scope kết thúc, tất cả child coroutines cũng kết thúc. Điều này ngăn chặn memory leaks và đảm bảo proper cleanup.
2. Coroutine Hierarchy
import kotlinx.coroutines.*
fun main() = runBlocking { // Parent scope
println("Parent started")
launch { // Child 1
println("Child 1 started")
delay(1000)
println("Child 1 finished")
}
launch { // Child 2
println("Child 2 started")
delay(500)
println("Child 2 finished")
}
println("Parent waiting for children...")
// Tự động đợi tất cả children hoàn thành
}
// Output:
// Parent started
// Parent waiting for children...
// Child 1 started
// Child 2 started
// Child 2 finished
// Child 1 finished3. coroutineScope vs runBlocking
import kotlinx.coroutines.*
suspend fun doWork() = coroutineScope {
// coroutineScope: suspending, không block thread
launch {
delay(100)
println("Task 1 done")
}
launch {
delay(200)
println("Task 2 done")
}
println("coroutineScope started")
}
fun main() = runBlocking {
// runBlocking: blocks current thread
println("Before doWork")
doWork()
println("After doWork - all children completed")
}
// Output:
// Before doWork
// coroutineScope started
// Task 1 done
// Task 2 done
// After doWork - all children completedSo sánh
| Feature | runBlocking | coroutineScope |
|---|---|---|
| Blocks thread | Yes | No |
| Suspending | No | Yes |
| Use case | Main function, tests | Inside suspend functions |
4. CoroutineScope và lifecycles
import kotlinx.coroutines.*
class UserService {
// Tạo scope với lifecycle riêng
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun fetchUser(userId: String) {
scope.launch {
val user = fetchFromNetwork(userId)
processUser(user)
}
}
fun cleanup() {
// Cancel tất cả coroutines khi không cần nữa
scope.cancel()
}
private suspend fun fetchFromNetwork(id: String): String {
delay(1000)
return "User $id"
}
private fun processUser(user: String) {
println("Processing: $user")
}
}Trong ViewModel (Android)
class MyViewModel : ViewModel() {
// viewModelScope tự động cancel khi ViewModel bị destroy
fun loadData() {
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
}
}
}5. Job Hierarchy
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob = coroutineContext[Job]
println("Parent job: $parentJob")
val childJob = launch {
println("Child job: ${coroutineContext[Job]}")
println("Child's parent: ${coroutineContext[Job]?.parent}")
delay(1000)
}
println("Is child of parent: ${childJob.parent == parentJob}")
// Is child of parent: true
}6. Cancellation Propagation
Parent cancels children
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child 1 cancelled")
}
}
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child 2 cancelled")
}
}
}
delay(100)
job.cancel() // Cancels parent -> all children cancelled
job.join()
println("Parent and children cancelled")
}
// Output:
// Child 1 cancelled
// Child 2 cancelled
// Parent and children cancelledChild failure cancels parent (default)
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
launch {
delay(100)
throw RuntimeException("Child failed!")
}
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Sibling cancelled due to child failure")
}
}
}
} catch (e: RuntimeException) {
println("Caught: ${e.message}")
}
}
// Output:
// Sibling cancelled due to child failure
// Caught: Child failed!7. SupervisorJob - Isolate Failures
SupervisorJob ngăn child failure lan truyền:
import kotlinx.coroutines.*
fun main() = runBlocking {
// supervisorScope: child failures don't affect siblings
supervisorScope {
val child1 = launch {
println("Child 1 started")
delay(100)
throw RuntimeException("Child 1 failed")
}
val child2 = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child 2 completed normally or cancelled")
}
}
child1.join()
println("Child 1 finished with: ${child1.isCancelled}")
delay(100)
child2.cancel()
}
println("Supervisor scope completed")
}
// Output:
// Child 1 started
// Child 1 finished with: true (failed)
// Child 2 completed normally or cancelled
// Supervisor scope completedSo sánh coroutineScope vs supervisorScope
import kotlinx.coroutines.*
suspend fun regularScope() = coroutineScope {
// Nếu 1 child fail -> tất cả siblings bị cancel
launch { /* ... */ }
launch { throw Exception() } // Cancels sibling
}
suspend fun supervisorScopeExample() = supervisorScope {
// Child fail KHÔNG ảnh hưởng siblings
launch {
delay(1000)
println("This still runs!")
}
launch { throw Exception() } // Sibling continues
}8. GlobalScope - Avoid!
import kotlinx.coroutines.*
// ❌ GlobalScope - không có structured concurrency
fun badExample() {
GlobalScope.launch {
// Coroutine này tồn tại độc lập
// Không bị cancel khi component bị destroy
// Memory leak potential!
while (true) {
println("Still running...")
delay(1000)
}
}
}
// ✅ Use scoped coroutines
class MyComponent {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun start() {
scope.launch {
while (isActive) {
println("Working...")
delay(1000)
}
}
}
fun stop() {
scope.cancel() // Properly cleanup
}
}9. Coroutine Context Inheritance
import kotlinx.coroutines.*
fun main() = runBlocking(CoroutineName("Parent")) {
println("Parent: ${coroutineContext[CoroutineName]}")
launch {
// Inherits CoroutineName from parent
println("Child inherits: ${coroutineContext[CoroutineName]}")
}
launch(CoroutineName("CustomChild")) {
// Override với context riêng
println("Child overrides: ${coroutineContext[CoroutineName]}")
}
launch(Dispatchers.IO) {
// Inherit CoroutineName, override Dispatcher
println("IO child: ${coroutineContext[CoroutineName]}, ${coroutineContext[ContinuationInterceptor]}")
}
}
// Output:
// Parent: CoroutineName(Parent)
// Child inherits: CoroutineName(Parent)
// Child overrides: CoroutineName(CustomChild)
// IO child: CoroutineName(Parent), Dispatchers.IO10. Practical Patterns
Parallel decomposition
import kotlinx.coroutines.*
suspend fun loadUserData(userId: String): UserData = coroutineScope {
// Run in parallel
val userDeferred = async { fetchUser(userId) }
val postsDeferred = async { fetchPosts(userId) }
val friendsDeferred = async { fetchFriends(userId) }
// Wait for all
UserData(
user = userDeferred.await(),
posts = postsDeferred.await(),
friends = friendsDeferred.await()
)
}
data class UserData(val user: String, val posts: List<String>, val friends: List<String>)
suspend fun fetchUser(id: String): String {
delay(1000)
return "User $id"
}
suspend fun fetchPosts(id: String): List<String> {
delay(800)
return listOf("Post 1", "Post 2")
}
suspend fun fetchFriends(id: String): List<String> {
delay(600)
return listOf("Friend 1", "Friend 2")
}Timeout với structured concurrency
import kotlinx.coroutines.*
suspend fun fetchWithTimeout(): String? {
return withTimeoutOrNull(1000) {
// All child coroutines are cancelled if timeout
val data = async { fetchData() }
val extra = async { fetchExtraInfo() }
"${data.await()} + ${extra.await()}"
}
}
suspend fun fetchData(): String {
delay(500)
return "Data"
}
suspend fun fetchExtraInfo(): String {
delay(300)
return "Extra"
}
fun main() = runBlocking {
println(fetchWithTimeout()) // "Data + Extra"
}Retry with scope
import kotlinx.coroutines.*
suspend fun <T> retryWithScope(
times: Int,
delayMs: Long = 1000,
block: suspend CoroutineScope.() -> T
): T {
repeat(times - 1) { attempt ->
try {
return coroutineScope { block() }
} catch (e: Exception) {
println("Attempt ${attempt + 1} failed: ${e.message}")
delay(delayMs)
}
}
// Last attempt
return coroutineScope { block() }
}
fun main() = runBlocking {
var attempts = 0
val result = retryWithScope(3) {
attempts++
if (attempts < 3) throw RuntimeException("Failed attempt $attempts")
"Success on attempt $attempts"
}
println(result) // Success on attempt 3
}📝 Tóm tắt
| Concept | Description |
|---|---|
| Structured Concurrency | Coroutines có hierarchy, parent đợi children |
coroutineScope | Suspending scope, đợi tất cả children |
supervisorScope | Child failures không affect siblings |
Job | Lifecycle handle cho coroutine |
SupervisorJob | Job không propagate child failures |
| Context inheritance | Children inherit parent’s context |
Best practices:
- Avoid
GlobalScope- use scoped coroutines - Dùng
supervisorScopekhi children independent - Dùng
coroutineScopekhi cần “all or nothing” - Luôn cancel scope khi component bị destroy
- Prefer
viewModelScope,lifecycleScopetrong Android
Last updated on