Coroutine에 대해

최희창·2022년 6월 2일

Coroutine이란?

  • 동시성 프로그래밍
  • 쓰레드와 비슷하지만, 하나의 쓰레드 내에서 여러 개의 코루틴이 실행되는 개념으로 비동기 프로그래밍에 권장되는 동시 실행 설계 패턴입니다.

특징

  • 단일 스레드 내에서 여러 개의 코루틴을 실행할 수 있기 대문에, 많은 양의 동시 작업을 처리할 수 있으면서 메모리 절약의 장점이 있습니다.
  • 기존의 스레드는 Context-Switching(CPU가 스레드를 점유하면서 실행, 종료를 반복하여 메모리 소모)이 발생하기 때문에 많은 양의 스레드를 갖기가 어렵지만, 코루틴은 스레드가 아닌 루틴을 일시 중단(suspend)하는 방식이라 Context-Switching에 비용이 들지 않는다.
  • 즉, 코루틴은 스레드의 간소화된 버젼이라고 할 수 있습니다.
  • 코루틴은 스레드 위에서 실행되는데 여러가지 코루틴이 존재한다고 할 때 코루틴 1,2,3이 있다고 할 때 코루틴1을 실행하던 중 2가 실행되도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라 기존 스레드를 유지하며 기존 스레드에서 코루틴2를 실행하게 됩니다.
  • 이후 코루틴1을 다시 실행할 때 저장해둔 코루틴1 상태를 불러와 다시 스레드에서 코루틴1을 실행하게 됩니다.
  • 즉, 스레드의 멈춤없이 루틴을 돌릴 수 있게 되며 이는 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것 보다 훨씬 적은 자원 소모를 하게 됩니다.
  • 스레드 관련 이벤트 또는 결과 처리를 위한 콜백 작성이 필요없고 순차적으로 코드를 작성하면 된다.

배경

  • 코루틴은 코틀린에 최근 추가된 것이지만 여러 형태로 다른 언어들에 이미 존재했던 것이다.
  • CSP(Communicating Sequential Process)라는 모델에 기반한다.

의존성 추가

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")

코루틴의 주요개념

  • CoroutineScope
  • CoroutineContext (Job, Dispatchers)
  • CoroutineBuilder (launch, async)
  • susfend function

코루틴 스코프(Coroutine Scope)

  • 코루틴이 실행되는 범위
  • 코루틴을 실행하고 싶은 Lifecycle에 따라 원하는 Scope를 생성하여 코루틴이 실행될 작업 범위를 지정할 수 있습니다.
  • 사용자 지정 CoroutineScope : CoroutineScope(CoroutineContext)
// 메인 쓰레드에서 실행될 사용자 정의 Scope
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
    // 메인 쓰레드 작업
}

// 백그라운드에서 실행될 사용자 정의 Scope
CoroutineScope(Dispatchers.IO).launch {
    // 백그라운드 작업
}
  • GlobalScope : 앱이 실행될 때부터 종료될 때까지 실행
// 앱의 라이프사이클동안 실행될 Scope
GlobalScope.launch {
    // 백그라운드로 전환하여 작업
    launch(Dispatchers.IO) {

    }

    // 메인쓰레드로 전환하여 작업
    launch(Dispatchers.Main) {

    }
}
  • ViewModelScope : ViewModel 대상, ViewModel이 제거되면 코루틴 작업이 자동으로 취소됩니다.
class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // ViewModel이 제거되면 코루틴도 자동으로 취소됩니다.
        }
    }
}
  • LifecycleScope : Lifecycle 객체 대상(Activity, Fragment, Service...), Lifecycle이 끝날 때 코루틴 작업이 자동으로 취소됩니다.
class MyActivity : AppCompatActivity() {
    init {
        lifecycleScope.launch {
            // Lifecycle이 끝날 때 코루틴 작업이 자동으로 취소됩니다.
        }
   }
}

CoroutineContext

  • 코루틴 작업을 어느 쓰레드에서 실행할 것인지에 대한 동작을 정의하고 제어하는 요소입니다.
  • 주요 요소로는 Job과 Dispatchers가 있습니다.
  • Job : 코루틴을 고유하게 식별하고, 코루틴을 제어합니다.
val job = CoroutineScope(Dispatchers.IO).launch {
    // 비동기 작업
}
job.join()      // 작업이 완료되기까지 대기
job.cancel()    // 작업 취소
val job1 = Job()
CoroutineScope(job1 + Dispatchers.Main).launch {
    // 메인 쓰레드 작업

    launch(Dispatchers.IO) {
        // 비동기 작업
    }

    withContext(Dispatchers.Default) {
        // 비동기 작업
    }
}

val job2 = CoroutineScope(Dispatchers.IO).launch {
    // 비동기 작업
}

job1.cancel() // job1이 연결된 코루틴 작업 취소

Dispatchers

  • 코루틴이 실행될 스레드
  • Dispatchers.Default : 안드로이드 기본 스레드풀 사용. CPU를 많이 쓰는 작업에 최적화(데이터 정렬, 복잡한 연산 등)
  • Dispatchers.IO : 이미지 다운로드, 파일 입출력 등 입출력에 최적화 되어있는 디스패쳐(네트워크, 디스크, DB 작업에 적합)
  • Dispatchers.Main : 안드로이드 기본 스레드에서 코루틴 실행. UI와 상호작용에 최적화
  • Dispatchers.Unconfined : 호출한 컨텍스트를 기본으로 사용하는데 중단 후 다시 실행될 때 컨텍스트가 바뀌면 바뀐 컨텍스트를 따라가는 특이한 디스패쳐
  • 디스패처는 코루틴을 적당한 스레드에 할당하며, 코루틴 실행 도중 일시 정지 or 실행 재개를 담당한다.

CoroutineBuilder

  • 위에서 설정한 CoroutineScope와 CoroutineContext를 통해 비로소, 코루틴을 실행시켜주는 함수입니다.
  • 주요 요소로는 launch와 async가 있습니다.
  • launch : Job 객체이며, 결과값을 반환하지 않습니다. 실행 후 결과값이 필요 없는 모든 작업은 launch를 사용하여 실행할 수 있습니다.
CoroutineScope(Dispatchers.Main).launch {
    // 결과값이 필요없는 작업
}
  • async : Deferred 객체이며, 결과값을 반환합니다. await() 함수를 사용하며, 코루틴 작업의 최종 결과값을 반환합니다.
val deferred = CoroutineScope(Dispatchers.Main).async {
    // 결과값
    "Hello Coroutine!"
}

val message = deferred.await() // await()함수로 결과값 반환
println(message)
  • withContext : async와 동일하게 결과값을 반환하며, async와의 차이점은 await()을 호출할 필요가 없다는 것입니다.
init {
    viewModelScope.launch {       // Dispatchers.Main
        val user = getUserInfo()  // Dispatchers.Main
    }
}

suspend fun getUserInfo(): User =                   // Dispatchers.Main
        withContext(Dispatchers.IO) {               // Dispatchers.IO
            val response = apiService.getUserInfo() // Dispatchers.IO
            if (response.isSuccessful) {            // Dispatchers.IO
                return@withContext response.body()  // Dispatchers.IO
            } else {                                // Dispatchers.IO
                return@withContext null             // Dispatchers.IO
            }                                       // Dispatchers.IO
        }                                           // Dispatchers.Main

cancel

  • 코루틴의 동작을 멈추는 상태관리 메서드로 하나의 스코프 안에 여러 코루틴이 존재하는 경우 하위 코루틴 또한 모두 멈춥니다.
val job = CoroutineScope(Dispatchers.Default).launch {
	val job1 = launch {
    	for (i in 0..10){
        	delay(500)
            Log.d("코루틴","$i")
            }
        }
     }

-> 아래 코드에서 job을 캔슬하게 되면 안에 있던 job1도 중단됩니다.

delay

  • delay는 코루틴에서 정의된 suspend function이다. 즉 코루틴이나 다른 suspend 함수 안에서만 수행될 수 있다.
  • 괄호 안의 ms 만큼 실행을 멈춘다. Thread.sleep(1000)와 거의 비슷하게 느껴질 것이다. 하지만 위에서 설명한 것 처럼 코루틴은 스레드 안에서 돌아가는 하나의 Job이며 그 스레드 안에 여러 개의 코루틴이 실행되고 있을 수 있다.
  • 따라서 delay는 코루틴 하나만 멈추게 되지만, Thread.sleep은 해당 스레드 안에 있는 코루틴을 다 멈추게 된다.

join

  • 코루틴 내부에 여러 launch 블록이 있는 경우 모두 새로운 코루틴으로 분기되어 동시 실행되기 때문에 순서를 정할 수 없습니다. 순서를 정해야 한다면 join()을 사용해서 순차적으로 실행되도록 코드를 짤 수 있습니다.
	CoroutineScope(Dispatchers.Default).launch {
    	launch {
        	for (i in 0..5) {
            	delay(500)
               }
         }.join()
         
         launch {
        	for (i in 6..10) {
            	delay(500)
               }
         }
     }
  }

async로 결과값 처리

  • async로 코루틴 스코프의 결과를 받아서 쓸 수 있습니다. 특히 연산시간이 오래걸리는 2개의 네트워크 작업의 경우를 예를 들면 2개의 작업이 모두 완료되고 나서 이를 처리하려면 await()을 사용할 수 있습니다. 이때는 async 작업이 모두 완료되고 나서야 await()호출줄 코드가 실행됩니다.
CoroutineScope(Dispatchers.Default).async {
    	val deferred1 = async {
        		delay(500)
                350
             }
         
         val deferred2 = async {
         		delay(1000)
                200
         }
         Log.d("coroutine","${deferred1.await() + deferred2.await()}")
     }
  }

suspend 함수

  • suspend 키워드는 코루틴 안에서 사용되면 suspend 함수가 호출될 경우 이전까지의 코드의 실행이 멈추며 suspend 함수가 처리가 완료 된 후 멈춰 있던 원래 스코프의 다음 코드가 실행됩니다.
	suspend fun subRoutine() {
    	for(i in 0..10) {
        	Log.d("subRoutine","$i")
            }
        }
        
     CoroutineScope(Dispatchers.Main).launch {
     	//선처리 코드
     	subroutine()
     	//후처리 코드
     }
  • suspend 키워드를 사용했기 때문에 코루틴스코프 안에서 자동으로 백그라운드 스레드 처럼 동작하게 됩니다.
  • 가장 큰 특징이라고 한 이유는 suspend 키워드를 붙인 함수가 실행되면서 호출한 쪽의 코드를 잠시 멈추게 되지만 스레드의 중단이 없습니다.
  • 코루틴이 실행되다가 일시 정지하는 경우 코틀린 런타임은 해당 코루틴이 실행되던 스레드에 다른 코루틴을 할당하여 실행되게 한다. 그리고 다시 이전 코루틴이 재개할 때 사용 가능한 스레드를 코틀린 런타임이 할당해준다. 이런 것은 다 효율적인 스레드 활용을 위한 것인데, 이런 매커니즘에 맞게 실행되는 함수가 바로 suspend 함수입니다.
  • 만약 스레드에서 해당 코드를 사용했다면 선처리 코드가 동작하는 스레드를 멈춰야지 서브루틴 호출이 가능한데 코루틴은 해당 부모 루틴 상태를 저장. 서브 루틴 실행. 부모 루틴 복원하는식으로 동작하여 스레드 영향을 주지 않게 됩니다.

withContext

  • 기존에는 다른 코루틴에 보내진 작업의 결과를 수신하려면 다음과 같이 코드를 만들어야 했다.
suspend fun main() {
	val deferred: Deferred<String> = CoroutineScope(Dispatchers.IO).async {
    	"Async Result"
        }
        
    val result = deferred.await()
    println(result)

-> Deferred로 결과값을 감싼 다음 await() 메서드를 통해 해당 값이 수신될 때까지 기다려야 한다.

  • withContext를 이용하면 두 가지 특성으로 작업을 간단하게 만들 수 있다.
  1. withContext 블록의 마지막 줄의 값이 반환 값이 된다.
  2. withContext가 끝나기 전까지 해당 코루틴은 일시정지된다.
suspend fun main() {
	val result: String = withContext(Dispatchers.IO) {
    	"Async Result"
    }
    
    println(result)
}
  • 순서
  1. withContext 블록은 IO Thread의 작업이 끝나기 전까지 Main Thread에서 수행되는 코루틴을 일시중단되도록 만든다.
  2. IO Thread의 작업이 끝나면 "Async Result"가 반환되며 이는 result에 세팅된다.
  3. result가 프린트된다.

(참고 블로그 : https://greedy0110.tistory.com/102)
https://0391kjy.tistory.com/49

profile
heec.choi

0개의 댓글