-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1부 코틀린 코루틴 이해하기 #1
Comments
[1장] 코틀린 코루틴을 배워야 하는 이유리액티브 프로그래밍
책에 나온 것과 같이 비동기 연산을 하기 위해 스레드 전환 방식을 사용하기엔 컨텍스트 스위칭 비용이 많이 들고, 스레드가 실행된 상태에서 멈출 수 있는 방법이 없어 메모리 누수로도 이어질 수 있다. 그러자니 콜백을 사용하기엔 읽기도 어렵고, 콜백 지옥에 빠지기 쉽다. 이러한 상황에서 리액티브 프로그래밍을 사용할 경우 쉽게 해결할 수 있다. 이 방식을 사용하면 데이터 스트림 내에서 일어나는 모든 연산을 시작, 처리, 관찰할 수 있다. fun onCreate() {
disposables += getNewsFromApi()
.subscribeOn(Shedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map { news ->
news.sortedByDescending { it.publishedAt }
}
.subscribe { sortedNews ->
view.showNews(sortedNews)
}
} 우선,
이렇게 리액티브 프로그래밍을 했을 때의 장점은 메모리 누수도 없으며, 취소도 가능하고, 스레드를 적절하게 사용할 수 있다. 하지만 이를 사용하기 위한 함수들을 배워야하고, 기존 코드를 코틀린 코루틴코루틴을 사용하면 기존 코드를 메인 스레드를 블로킹 없이 사용할 수 있다. 또한, 코루틴을 중단시켜도, 스레드는 블로킹되지 않으며, 다른 코루틴을 실행하는 등의 작업도 가능하다. fun onCreate() {
viewModelScope.launch {
val news = getNewsFromApi()
val sortedNews = news
.sortedByDescending { it.publishedAt }
view.showNews(sortedNews)
}
} 위 코드와 같이 리액티브 라이브러리를 사용한 것보다 훨씬 간편하게 사용이 가능하다. 만약 3개의 엔드포인트를 호출해야할 경우 다음과 같이 작성할 수 있다. fun showNews() {
viewModelScope.launch {
val config = async { getConfigFromApi() }
val news = async { getNewsFromApi(config.await()) }
val user = async { getUesrFromApi() }
view.showNews(user.await(), news.await())
}
}
백엔드의 코루틴백엔드에서의 코루틴 사용은 단순히 suspend fun getArticle(
key: String,
lang: Language
): ArticleJson? {
return articleRepository.getArticle(articleKy, lang)
?.let { toArticleJson(it) }
} suspend fun getAllArticles(
userId: String?,
lang: Language
): List<ArticleJson> = coroutineScope {
val user = async { userRepo.findUserById(userId) }
val articles = articleRepo.getArticles(lang)
articles
.filter { hasAccess(user.await(), it) }
.map { toArticleJson(it) }
} 이렇게 코루틴을 사용하는 가장 중요한 이유는 스레드를 사용하는 비용이 크기 때문이다. 수백만 명의 사용자들이 앱을 사용할 때, DB 혹은 다른 서비스로부터 응답을 기다릴 때마다 블로킹이 이뤄진다면, 메모리나 프로세스 사용에 큰 비용이 들 것이다. 대용량 처리백엔드에서 10만 건의 데이터를 저장한다고 가정해보자. @PostConstruct
private fun dataInit() {
for (i in 0 until 100_000) {
val result = random()
val mul = result * 10
repo.save(Member(null, mul.toInt().toString(), mul.toInt()))
}
} 10만 건의 데이터를 처리하는데 걸리는 시간은 대략 50초가 걸린다.
그렇다면 코루틴을 사용하면 어떨까? @PostConstruct
private fun dataInit() {
log.info("data init start")
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
val jobs = mutableListOf<Deferred<Member>>()
for (i in 0 until 100_000) {
val result = random()
val mul = result * 10
val job = async {
repo.save(Member(null, mul.toInt().toString(), mul.toInt()))
}
jobs.add(job)
}
jobs.awaitAll()
log.info("repo.findAll().size = ${repo.findAll().size}")
}
} 코루틴을 사용할 경우 대략 15초가 나온다. 모든 코루틴을 시작하는 비용은 스레드 생성해 처리하는 것에 비해 비교도 안 될 정도로 저렴하고, 사람이 인지할 수 없는 정도이다. [2장] 시퀀스 빌더코틀린의 시퀀스는
이러한 특징 때문에 값을 순차적으로 계산해 필요할 때 반환하는 빌더를 정의하는 것이 좋다. val seq = sequence {
yield(1)
yield(2)
yield(3)
}
fun main() {
for(num in seq) print(num)
}
코루틴을 사용하지 않을 경우 스레드를 이용할 수 있지만, 그럴 경우 유지하고 관리하는 비용이 크게 든다. 하지만 코루틴은 중단이 가능하고, 이전 지점으로 다시 돌아가 다음 값을 생성할 수 있다. [3장] 중단은 어떻게 작동할까?중단중단(suspend) 함수는 코틀린 코루틴의 핵심이다. 중단이 가능하다는 건 코틀린 코루틴의 다른 모든 개념의 기초가 되는 필수적인 요소이다. 코루틴을 중단한다는 것은 게임에서 저장을 하고 체크 포인트에서부터 시작하는 것처럼 실행을 중간에 멈추는 것을 의미한다. 코루틴은 중단되었을 때 스레드의 경우 저장이 불가능하고 멈추는 것만 가능하지만 코루틴의 경우 멈추는 것도 가능하고, 어디까지 진행했는지 저장도 할 수 있는 것이다. 이 점이 코루틴의 강력한 도구이다. 재개재개란, 게임을 할 때 '이어서 하기'를 누르면 저장했던 체크 포인트에서 시작하는 느낌이다. 작업을 재개하려면 코루틴이 필요하다. suspend fun main() {
println("Before")
println("After")
} 위 코드의 경우 두 줄 모두 출력이 된다. 그렇다면 중간에 중단 함수를 넣으면 어떻게 될까? suspend fun main() {
println("Before")
suspendCoroutine<Unit> { }
println("After")
} 이 경우에는 suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continuation.resume(Unit)
}
println("After")
} 이렇게 suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
println("Suspended")
Thread.sleep(1000L)
continuation.resume(Unit)
println("Resume")
}
println("After")
}
이렇게 스레드를 재우는 방법 보다는 private val executor = Executors.newSingleThreadScheduledExecutor() {
Thread(it, "scheduler").apply { isDaemon = true }
}
suspend fun delay(timeMillis: Long): Unit =
suspendCoroutine { cont ->
executor.schedule({
cont.resume(Unit)
}, timeMillis, TimeUnit.MILLISECONDS)
}
suspend fun main() {
println("Before")
delay(1000)
println("After")
} 이전 코드가 하나의 스레드를 블로킹한다. 하지만 위 코드의 값으로 재개하기API를 호출해 네트워크 응답을 기다리는 것처럼 특정 데이터를 기다리기 위해 중단되는 상황은 자주 발생한다. 스레드는 특정 데이터가 필요한 지점까지 비즈니스 로직을 수행하고, 이후 네트워크 라이브러리를 통해 데이터를 요청한다. 코루틴이 없다면 스레드는 응답을 기다릴 수 밖에 없다. 하지만 코루틴을 사용하면 중단함과 동시에 데이터를 받고 난 뒤, private suspend fun apiTest() {
println("BEFORE")
val user = requestUser()
println(user)
println("AFTER")
}
private suspend fun requestUser() = suspendCoroutine<User> { cont ->
requestUser {
cont.resume(it)
}
}
private fun requestUser(block: (User) -> Unit) {
block(createUser())
} 함수가 아닌 코루틴을 중단시킨다.중단 함수는 코루틴이 아니고, 단지 코루틴을 중단할 수 있는 함수라고 할 수 있다. 아래 코드는 변수에 var continuation: Continuation<Unit>? = null
suspend fun main() {
println("BEFORE")
suspendAndSetContinuation()
continuation?.resume(Unit)
println("AFTER")
}
suspend fun suspendAndSetContinuation() {
suspendCoroutine<Unit> { cont ->
continuation = cont
}
} 위 코드는 의도와 달리 var continuation: Continuation<Unit>? = null
suspend fun main() = coroutineScope {
println("BEFORE")
launch {
delay(1000)
continuation?.resume(Unit)
}
suspendAndSetContinuation()
println("AFTER")
}
suspend fun suspendAndSetContinuation() {
suspendCoroutine<Unit> { cont ->
continuation = cont
}
} 하지만 위 코드는 메모리 누수가 발생한다. 그 이유는 [4장] 코루틴의 실제 구현
코루틴의 동작 과정 중 중요한 점은 다음과 같다.
|
코루틴의 동작 과정
Kotlin 컴파일러가 suspend 키워드를 만나면 어떻게 동작할까?1. CPS (continuation-passing style)
suspend fun postItem(item: Item): Post {
val token = requestToken()
val post = createPost(token, item)
return processPost(post)
}
↓
fun postItem(item: Item, cont: Continuation<Post>) {
val token = requestToken()
val post = createPost(token, item)
cont.resume(processPost(post))
} 2. State machine - Suspension Points를 기준으로 코드 블록이 구분
fun postItem(item: Item, cont: Continuation<Post>) {
// LABEL 0
val token = requestToken()
// LABEL 1
val post = createPost(token, item)
// LABEL 2
cont.resume(processPost(post))
} fun postItem(item: Item, cont: Continuation<Post>) {
switch (label) {
case 0:
val token = requestToken()
case 1:
val post = createPost(token, item)
case 2:
processPost(post)
}
} 3. State machine - label과 각 suspend fun 내부 변수들이 관리
fun postItem(item: Item, cont: Continuation<Post>) {
val sm = cont as? ThisSM ?: object : ThisSM {
fun resume(…) {
postItem(null, this)
}
}
switch (sm.label) {
case 0:
throwOnFailure(sm.result)
sm.label = 1
requestToken(sm)
case 1:
throwOnFailure(sm.result)
sm.label = 2
createPost(token, item, sm)
…
}
} resumeWith는 어떻게 동작할까? (BaseContinuationImpl)BaseContinuationImpl 클래스를 통해 resumeWith 의 동작 방식을 알아보자. internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
...
} resumeWith
|
3월 10일 오후 2시
The text was updated successfully, but these errors were encountered: