Unit 4: Internet (1)

quokka·2021년 11월 21일
0

Android Basics in Kotlin

목록 보기
18/25
post-thumbnail

멀티스레딩과 동시 실행

하나의 스레드로도 여러 작업을 할 수 있지만 앱이 커짐에 따라 동시 실행으로 여러 코드를 병렬적으로 실행해 리소스를 효율적으로 사용하는 것을 고려해야 한다. 하지만 스레드를 사용한 멀티스레딩은 다음과 같은 문제가 있다.

많은 리소스가 필요한 thread

스레드를 생성하고 전환하고 관리하는 데는 시스템의 리소스가 필요하다. 다른 스레드로 전환하는 컨텍스트 스위칭은 비교적 리소스가 많이드는 작업이다.

실행 중인 앱에는 여러 스레드가 있지만 각 앱에는 메인 스레드가 하나 있고 특히 이 메인 스레드는 앱의 UI를 담당한다. 이 스레드를 메인 스레드 또는 UI 스레드라고도 한다. (UI 스레드와 메인 스레드가 다른 경우도 있다.)

메인 스레드는 UI를 담당하기 때문에, 앱이 자연스럽게 실행되려면 메인 스레드가 잘 동작하는게 중요하다.

예측할 수 없는 동작 & Race conditions (경쟁 상태)

프로세서가 여러 스레드의 명령어 집합 간에 전환할 때 스레드가 실행되는 정확한 시간과 스레드가 일시중지되는 시점은 개발자가 제어할 수 없다. 즉 스레드를 직접 사용할 때 순서를 예측할 수 없다.

여러 스레드가 동시에 메모리의 동일한 값에 액세스할 때 경합 상태가 발생할 수 있다.

성능 문제, 경합 상태, 재현하기 어려운 버그는 스레드 직접 사용을 권장하지 않는 이유! 대신 동시 실행 코드 작성에 도움이 되는 코루틴이라는 Kotlin의 기능에 관해 알아보자.

Coroutine

코루틴은 멀티태스킹, 동시성 프로그래밍, 비동기 처리를 지원한다. 코루틴은 스레드를 중단하지 않으면서 비동기적으로 실행할 수 있다.
코루틴의 주요 기능 중 하나는 상태를 저장하여 중단했다가 재개할 수 있다는 것이다.

  • Job: 취소 가능한 작업 단위
  • CoroutineScope: 새 코루틴 생성에 사용되는 launch() 및 async()와 같은 함수는 CoroutineScope를 확장한다.
  • Dispatcher: 코루틴이 사용할 스레드를 결정한다. 메인 dispatcher는 항상 메인 스레드위에서 코루틴을 실행하지만, Default나 IO 또는 Unconfined 같은 dispatcher는 다른 스레드를 사용한다.

GlobalScope는 앱이 실행되는 동안 내부의 코루틴이 실행되도록 한다. 실제 어플에서 코루틴을 사용할 때는 다른 scope를 사용하기 때문에 권장되는 방식이 아니다.

launch()는 취소 가능한 Job 객체로 래핑된 enclosed code에서 코루틴을 생성한다. 반환 값이 코루틴의 범위 밖에서 필요하지 않을 때 사용한다.

fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

runBlocking

새 코루틴을 시작하고 완료될 때까지 현재 스레드를 차단한다.

runBlocking은 다음과 같이 사용되는데, 주로 메인 기능과 테스트 사이에서 blocking code와 non-blocking code를 연결하는데 사용된다.

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }

suspend fun getValue(): Double {
    println("entering getValue() at ${time()}")
    delay(3000)
    println("leaving getValue() at ${time()}")
    return Math.random()
}

fun main() {
    runBlocking {
        val num1 = getValue()
        val num2 = getValue()
        println("result of num1 + num2 is ${num1 + num2}")
    }
}

/* 출력
entering getValue() at 09:04:36.568176
leaving getValue() at 09:04:39.574646
entering getValue() at 09:04:39.575694
leaving getValue() at 09:04:42.576174
result of num1 + num2 is 0.8632321377131991
*/

getValue()는 entering time과 delay(3000) 후 leaving time을 출력한다.

main()을 다음과 같이 바꾸면, 출력 결과가 달라진다.

fun main() {
    runBlocking {
        val num1 = async { getValue() }
        val num2 = async { getValue() }
        println("result of num1 + num2 is ${num1.await() + num2.await()}")
    }
}

/* 출력
entering getValue() at 09:06:50.336893
entering getValue() at 09:06:50.340555
leaving getValue() at 09:06:53.340053
leaving getValue() at 09:06:53.343047
result of num1 + num2 is 1.1458268364123785
*/

async()는 다음과 같이 정의된다.

Fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

launch()와 유사한 async() 함수는 Deferred 유형의 값을 반환한다.
Deferred

  • 미래 값 참조를 보유할 수 있는 취소 가능한 Job
  • 즉시 값을 반환하는 것처럼 함수를 계속 호출할 수 있다.
  • 비동기 작업이 언제 반환될지 확실히 알 수 없기 때문에 자리표시자 역할만 한다.
  • 나중에 이 객체에 값이 반환된다는 것을 보장한다.
  • 현재 코드 줄이 Deferred의 출력을 기다리도록 하려면 코드 줄에서 await()를 호출하면 된다.

suspend

suspend 함수를 호출하려면 그 함수도 suspend 함수여야 한다. getValue()는 suspend 함수인 delay()를 호출하기 때문에 suspend이다.

그렇다면 getValue()를 호출하는 main()은 왜 suspend가 아닐까? 사실 getValue()runBlocking()으로 전달된 suspend 함수에서 호출되는 것이다. 즉 사실상 main()에서 호출하는 것도, runBlocking()에서 호출하는 것도 아니기 때문에 main()은 suspend가 아니다.

Thread를 Coroutine으로

// Thread
fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}

/*
Thread[Thread-1,5,main] - Doing Task 1 Thread[Thread-0,5,main] - Doing Task 1 
Thread[Thread-2,5,main] - Doing Task 1 Thread[Thread-1,5,main] - Doing Task 2 
Thread[Thread-0,5,main] - Doing Task 2 Thread[Thread-2,5,main] - Doing Task 2 
Thread[Thread-1,5,main] - Ending Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending Thread[Thread-0,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-1,5,main] - Starting
*/
// Coroutine
import kotlinx.coroutines.*

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       GlobalScope.launch {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               delay(5000)
           }
       }
   }
}

/*
Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main] has started 
Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main] - Starting 
Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] has started 
Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] - Starting 
Thread[DefaultDispatcher-worker-2 @coroutine#3,5,main] has started 
Thread[DefaultDispatcher-worker-2 @coroutine#3,5,main] - Starting
*/

0개의 댓글