핀수로그
  • [Android] Thread와 Coroutine
    2023년 10월 05일 22시 40분 08초에 업로드 된 글입니다.
    작성자: 핀수
    728x90
    반응형
    val list = mutableListOf("포도", "메론", "딸기", "사과", "바나나")
    
    CoroutineScope(Dispatchers.Main).launch {
        list.forEach { item ->
            if (item == "딸기") {
                list.remove(item)
                Log.d(TAG, "1. ${list}")
            }
        }
    }
    
    CoroutineScope(Dispatchers.IO).launch {
        list.forEach { item ->
            if (item == "바나나") {
                list.remove(item)
                list.add("자두")
                Log.d(TAG, "2. ${list}")
            }
        }
    }
    /*
    2023-04-03 21:19:20.540 2. [포도, 메론, 딸기, 사과, 자두]
    2023-04-03 21:19:20.586 1. [포도, 메론, 사과, 자두]
    */

    Caused by: java.util.ConcurrentModificationException 발생

    근데 Main 이라고 코루틴 스코프를 지정해주지 않으면 2번 뒤에 바로 죽어버린다.
    스코프를 지정하면 위의 결과를 뱉고 죽어버린다.
     

    CoroutineScope(Dispatchers.Main).launch {
        Log.d(TAG, "1. CoroutineScope(Dispatchers.Main): ${Thread.currentThread()}")
    }
    
    CoroutineScope(Dispatchers.IO).launch {
        Log.d(TAG, "2. CoroutineScope(Dispatchers.IO): ${Thread.currentThread()}")
    }
    
    Thread(Runnable { Log.d(TAG, "3. Thread: ${Thread.currentThread()}") }).start()
    
    Log.d(TAG, "4. None: ${Thread.currentThread()}")
    
    Thread(Runnable { Log.d(TAG, "5. Thread: ${Thread.currentThread()}") }).start()
    
    CoroutineScope(Dispatchers.Default).launch {
        Log.d(TAG, "6. CoroutineScope(Dispatchers.Default): ${Thread.currentThread()}")
    }
    
    CoroutineScope(Dispatchers.Main).launch {
        Log.d(TAG, "7. CoroutineScope(Dispatchers.Main): ${Thread.currentThread()}")
    }
    
    Log.d(TAG, "8. None: ${Thread.currentThread()}")

    Thread.currentThread()
    - 스레드 이름, 우선순위, 스레드 그룹을 반환하는 문자열 표현으로 변환되는 Thread 인스턴스 반환
     

    여러 스레드 만들기 및 실행

    이전 예의 정보 줄을 출력하는 스레드 3개를 생성 repeat(3)

    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()
       }
    }

    결과

    스케줄러는 각 스레드에서 일정 시간을 제공하고 스레드는 그 시간 내에 완료되거나 다른 시간을 받을 때까지 정지됨

    • 스레드는 프로세서가 어떻게 한 번에 여러 작업을 처리하는 것처럼 보이는지에 관한 추상화
    • 프로세서가 여러 스레드의 명령어 집합 간에 전환할 때 실행되는 정확한 시간과 스레드가 일시 중지되는 시점은 개발자가 제어할 수 없음
    • 스레드를 직접 사용할 때 항상 예측 가능한 출력을 기대할 수는 없음
    • 여러 스레드로 작업할 때는 경합 상태도 발생할 수 있음
      • 여러 스레드가 동시에 메모리의 동일한 값에 액세스하려고 할 때 발생
      • 경합 상태로 인해 무작위로 보이는 버그를 재현하기 어려울 수도 있고 이로 인해 예상치 못한 앱의 비정상 종료를 유발할 수 있음

    Kotlin의 코루틴

    백그라운드 작업을 위한 스레드를 직접 만들고 사용하는 것은 Android에서 이루어지지만
    Kotlin은 동시 실행을 더 유연하고 쉽게 관리할 수 있는 코루틴도 제공함
     
    코루틴은 멀티태스킹을 지원하지만 단순히 스레드로 작업하는 것보다 다른 수준의 추상화를 제공
    코루틴의 주요 기능 중 하나는 상태를 저장하여 중단했다가 재개할 수 있다는 것
    코루틴은 실행되거나 실행되지 않을 수 있음
     
    연속으로 표시되는 상태를 통해 코드 일부가 제어권을 넘겨주거나 재개되기 전에 다른 코루틴이 작업을 완료할 때까지 기다려야 하는 시기를 나타낼 수 있음 → 협력적인 멀티태스킹
     
    코틀린의 코루틴 구현은 멀티태스킹을 지원하는 여러 기능을 추가함
    연속 외에도 코루틴을 만드는 것에는 CoroutineScope 내에서 수명주기가 있는 취소 가능한 작업 단위인 Job 의 작업이 포함됨
    CoroutineScope 는 하위 요소와 그 하위 요소에 취소 및 기타 규칙을 반복적으로 적용하는 컨텍스트
    Dispatcher 는 코루틴이 실행에 사용할 자원 스레드를 관리하므로 개발자가 새 스레드를 사용할 시기와 위치를 파악하지 않아도 됨
     

    • Job
      • 취소 가능한 작업 단위 (launch() 함수로 만든 작업 단위)
    • CoroutineScope
      • launch()async() 와 같은 새 코루틴을 만드는 데 사용되는 함수는 CoroutineScope 를 확장
    • Dispatcher
      • 코루틴이 사용할 스레드를 결정
      • Main 디스패처는 항상 기본 스레드에서 코루틴을 실행하지만 DefaultIO, Unconfied 와 같은 디스패처는 다른 스레드를 사용함
      • 새 스레드를 초기화 하는 데 드는 성능 비용이 발생하지 않도록 함

     
    예제

    import kotlinx.coroutines.*
    
    fun main() {
        repeat(3) {
            GlobalScope.launch {
                println("Hi from ${Thread.currentThread()}")
            }
        }
    }
    • GlobalScope
      • 앱이 실행되는 한 내부의 코루틴이 실행되도록 허용
      • 기본 스레드에 관해 언급했던 이유로 예 코드 외부에서는 권장되지 않음
    • launch()
      • 취소 가능한 Job 객체에 래핑된 닫힌 코드에서 코루틴을 만듬
      • 반환 값이 코루틴의 범위 밖에서 필요하지 않을 때 사용
    fun CoroutineScope.launch {
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    }

    launch() 의 메소드 시그니처

    • 개발자가 실행을 위해 전달한 코드 블럭은 suspend 키워드로 표시됨
      • suspend: 코드 또는 함수 블록이 일시정지 되거나 재개될 수 있음을 나타냄
      • 함수가 suspend 함수를 호출하지 않으면 그 자체가 suspend 함수가 아니어도 됨

    runCatching

    새 코루틴을 시작하고 완료될 때까지 현재 스레드 차단
    주로 기본 함수와 테스트에서 차단 코드와 비차단 코드 사이를 연결하는 데 사용
    일반적인 안드로이드 코드에서는 자주 사용하지 않음
     

    async()

    Fun CoroutineScope.async() {
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    }: Deferred<T>
    • Deferred 유형의 값 반환
      • Deferred: 미래 값 참조를 보유할 수 있는 취소 가능한 Job (다른 언어에서는 Promise나 Future라고도 함)
      • 즉시 값을 반환하는 것처럼 함수를 계속 호출할 수 있음
      • 나중에 이 객체에 값이 반환된다고 보장
    • 현재 코드 줄이 Deferred 의 출력을 기다리도록 하려면 코드 줄에서 await() 를 호출하면 됨 → 원시 값이 반환 됨

    공부하며 작성된 글이라 잘못된 정보가 있을 수 있습니다.
    말씀해주시면 수정하겠습니다. 감사합니다.

    References

    아래 글을 참고하여 작성 되었습니다.

    코루틴 소개  |  Android 개발자  |  Android Developers

    코루틴 소개

    developer.android.com

     
     
     

    728x90
    반응형
    댓글