내가 하고 싶은 것은 다음과 같다.
- sequential (동기 순차 실행)과 parallel (비동기 병렬 실행)이 존재
- sequential과 parallel은 중첩될 수 있음
- sequential {…} 안의 sequential과 parallel은 순차실행됨.
- parallel {…} 안의 sequential과 parallel은 병렬실행됨.
- await 같은 명시적인 join 표시 없이 스코프가 끝나면 알아서 join
대충 클로드에게 물어봤더니 잘 짜줌.
원래는 parallel도 별도의 래퍼 없이 하고 싶었는데 생각해보니 비동기면 그게 불가능하니 어쨋든 래퍼는 필요함
AsyncResult<T>
앞서 말한 parallel에서 사용할 결과 래퍼이다.
data class AsyncResult<T>(internal var raw: Any? = UNSET) {
internal object UNSET // *1
@Suppress("UNCHECKED_CAST")
operator fun <T> AsyncResult<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
return raw as T
}
internal fun set(v: T) {
raw = v
}
}
*1: object로 생성한 객체는 클래스 안에 있어도 자동으로 static (companion object)가 된다고 한다.
AsyncScope.kt
저걸 이용해서 Kotlin DSL을 만든다.
@DslMarker
annotation class AsyncDsl
@AsyncDsl
object SequentialScope {
suspend fun <R> sequential(block: suspend SequentialScope.() -> R): R = SequentialScope.block()
suspend fun <R> parallel(block: suspend ParallelScope.() -> R): R = coroutineScope {
ParallelScope(this).block()
}
suspend fun <R> task(block: suspend () -> R): R = block()
}
@AsyncDsl
class ParallelScope(private val scope: CoroutineScope) {
fun <R> sequential(block: suspend SequentialScope.() -> R): AsyncResult<R> {
val result = AsyncResult<R>()
scope.launch {
result.set(SequentialScope.block())
}
return result
}
fun <R> parallel(block: suspend ParallelScope.() -> R): AsyncResult<R> {
val result = AsyncResult<R>()
scope.launch {
result.set(coroutineScope {
ParallelScope(this).block()
})
}
return result
}
fun <R> task(block: suspend () -> R): AsyncResult<R> {
val result = AsyncResult<R>()
scope.launch {
result.set(block())
}
return result
}
}
suspend fun <R> sequential(block: suspend SequentialScope.() -> R): R = SequentialScope.block()
suspend fun <R> parallel(block: suspend ParallelScope.() -> R): R = coroutineScope {
ParallelScope(this).block()
}
테스트
class AsyncScopeTests {
private val logger = Logger.getLogger(AsyncScopeTests::class.qualifiedName)
data class Config(val userId: UUID, val postId: UUID)
data class User(val userId: UUID, val name: String)
data class Post(val postId: UUID, val title: String)
private suspend fun randomDelay(maxMillis: Long = 1000) {
val randomDelay = Random.nextLong(maxMillis)
delay(randomDelay.milliseconds)
}
private suspend fun <R> withLogging(title: String, block: suspend () -> R): R {
logger.info("$title started at ${LocalDateTime.now()}")
val result = block()
logger.info("$title finished at ${LocalDateTime.now()}")
return result
}
private suspend fun loadConfig(): Config = withLogging("loadConfig") {
randomDelay(100)
Config(userId = UUID.randomUUID(), postId = UUID.randomUUID())
}
private suspend fun fetchUser(config: Config): User = withLogging("fetchUser") {
randomDelay(100)
User(config.userId, "John Doe")
}
private suspend fun fetchPost(config: Config): Post = withLogging("fetchPost") {
randomDelay(500)
Post(config.postId, "A Brief History of Time")
}
@Test
fun testAsyncScope(): Unit = runBlocking {
val (user, post) = sequential {
val config = task { loadConfig() }
val (user, post) = parallel {
val user = task { fetchUser(config) }
val post = task { fetchPost(config) }
user to post
}
user to post
}
println(
"""
User: $user
Post: $post
""".trimIndent()
)
}
}

코드상으로는 랜덤이지만 고정으로 딜레이를 줬다 (100,100,500)
loadConfig과 fetchUser,fetchPost는 순차이므로 loadConfig이 끝난 다음에 fetch*가 실행되었고
fetchUser와 fetchPost는 병렬호출이므로 똑같이 시작했다가 fetchUser는 100ms 대기하니 로그에는 100ms정도 흘렀고, fetchPost는 500ms대기하니 0.5초만큼 늘었다.
하지만 sequential은 동기호출이니 fetchPost가 다 끝나고야 최종 정보가 나오고, UNSET으로 나오는 경우는 없어진다.
생각해보니 이것도 좀 문제가 있네;
답글 남기기