ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동기/비동기 프로그래밍, 쓰레드, 코루틴
    카테고리 없음 2024. 3. 15. 17:07

    1. 동기/비동기 프로그래밍

    프로그래밍에 있어 동기 프로그래밍과 비동기 프로그래밍 뜻이 뭘까? 동기 프로그래밍은 순서대로 하나의 작업씩 수행하는 행위를 의미한다. 앞선 작업이 끝나지 않는다면 뒷 작업은 실행이 되지 않아 동시성이 보장되지 않는다. 즉, 요청 보내고 결과값을 받을때까지 작업을 멈추고 한가지씩 작업을 처리한다. 이와 달리, 비동기 프로그래밍은 동시성이 보장되어 요청을 보내고 결과값을 받기 전까지 다른 일을 수행할 수 있어 다양한 일을 한번에 처리할 수 있다. 프로그래밍에서는 비동기 프로그래밍 개념이 중요하다.

     

    2. 쓰레드

    프로그램은 하나의 메인 쓰레드인 메인 함수가 기본적으로 존재한다. 지금까지는 메인 쓰레드 위에서 동작을 실행해서 동시처리가 불가능했는데, 따로 자식 쓰레드를 생성하면 동시 처리가 가능해진다.

     

    프로그램이 메모리에 올라가서 실행될때 프로세스 1개가 생성된다. 이때 쓰레드는 프로세스 안에 있는 더 작은 작업의 단위를 의미한다. 쓰레드는 생성되어 수행할때 각 독립된 메모리 영역인 스택(stack)을 가진다. 

    프로세스 구조

     

    쓰레드를 사용하기 위해서는 외부 종속성을 추가해야한다. 해당 아래의 코드를  Gradle 파일 중 app 모듈의dependency 부분에 추가하면 된다.

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta") // 쓰레드를 위한 외부 종속성 추가

     

    해당 코드에는 총 3개의 쓰레드가 있다(main 함수의 메인 쓰레드와 자식 쓰레드 2개). 

    fun main() {
        thread {
            for(i in 1..10) {
                println("현재 숫자 : ${i}")
                runBlocking {
                    launch {
                        delay(1000)
                    }
                }
            }
        }
        thread {
            for(i in 100..110) {
                println("현재 숫자 : ${i}")
                runBlocking {
                    launch {
                        delay(1000)
                    }
                }
            }
        }
    }

    해당 위의 코드에서 쓰레드의 구조는 다음과 같이 쓸 수 있다. thread → runBlocking → launch → delay 순이다.

    thread {
        runBlocking {
            launch {
                delay(1000) //1초
            }
        }
    }

     

     

    3. 코루틴

    코루틴은 쓰레드보다 더욱 가볍게 사용할 수 있는데, 구글은 코루틴 사용을 권장한다. 정리표는 아래와 같고, 안드로이드에서는 Dispatcher 간의 변환을 하는 작업을 고려해야 할 수 있다. 

    코루틴 빌더 launch async
    결과값  결과값 X 결과값 O
    객체  Job 객체로 코루틴 관리 X
    객체의 함수 join : 현재의 코루틴이 종료되길 기다리는 함수
    cancel : 현재의 코루틴을 즉시 종료하는 함수
    X
    특징 - 빌더 내부에 delay 가 있음 - 매개변수로 dispatcher 가 필요함
    (꼭 안써줘도 됨)
    - 빌더 내부에 delay 가 있음
    - 결과값을 반환할 때 await 함수랑 같이 써야한다
    코루틴 스코프의 범위 GlobalScope CoroutineScope
    특징 앱이 실행된 이후에 계속 수행되어야 할 때 사용 필요할때만 생성하고 사용후에 정리가 필요함 → 사용 후 cancel 함수 호출해야 함
    매개변수 여부 X 매개변수로 Dispatcher 가 필요함
    Dispacher(코루틴을 실행할 쓰레드) 설명
    Dispatchers.Main UI와 상호작용하기 위한 메인쓰레드
    Dispatchers.IO 네트워크나 디스크 I/O 작업에 최적화된 쓰레드
    Dispatchers.Default CPU에 최적화된 쓰레드

     

     

    해당 아래 코드에서는 1초뒤에 "코루틴 영역입니다!" 를 출력해야 하지만, main 쓰레드가 먼저 종료되어 출력되지 않는다. 안드로이드는 항상 앱이 켜있는 상태이지만, 우리가 실습하는 Android studio는 실행 후 종료되는 JVM 환경이기 때문이다.

    launch → delay 순으로 나오는 것을 볼 수 있다.

    fun main(args:Array<String>) {
        println("메인쓰레드 시작")
        //코루틴 영역
        var job = GlobalScope.launch {
            delay(1000)
            println("코루틴 영역입니다!")
        }
        println("메인쓰레드 종료")
    }

     

    이때, 코루틴 내용이 출력되도록 하려면, launch 코루틴 빌더의 Job 객체의 join 함수를 이용하면 된다. 해당 함수는 현재의 코루틴이 종료되기를 기다리는 함수이다. 이때, 해당 코드는 runBlocking 으로 감싸줬다.  이때, 코루틴 스코프.코루틴빌더 형태로 쓰는 것을 볼 수 있다!

    fun main(args:Array<String>) {
        println("메인쓰레드 시작")
        //코루틴 영역
        var job = GlobalScope.launch {
            delay(1000)
            println("코루틴 영역입니다!")
        }
        runBlocking { // 새로 추가한 코드
            job.join()
        }
        println("메인쓰레드 종료")
    }

     

    다른 코루틴 스코프도 있다. 이때, CoroutineScope 의 경우에는 매개변수로 쓰레드인 dispatcher 를 지정해줘야 한다.

    또한, 사용후 정리가 필요한데 이때, cancel 함수를 호출하면 된다. 

    fun main(args:Array<String>) {
        println("메인쓰레드 시작")
        //코루틴 영역
        var job = CoroutineScope(Dispatchers.Default).launch {
            delay(3000)
            println("코루틴 영역입니다!")
        }
        runBlocking {
            job.join()
        }
        println("메인쓰레드 종료")
        job.cancel()
    }

     

     

    여러개의 코루틴을 사용하여 코루틴의 결과값을 리턴받는 코드 예시도 있다. 이때는, await라는 함수를 사용해야한다.

    fun main(args:Array<String>) {
        println("메인 쓰레드 시작!")
        var job = CoroutineScope(Dispatchers.Default).launch {
    
            // 코루틴 1
            var fileDownloadCoroutine = async(Dispatchers.IO) {
                delay(5000)
                "파일 다운로드 완료"
            }
    
            // 코루틴 2
            var databaseConnectCoroutine = async(Dispatchers.IO) {
                delay(10000)
                "데이터베이스 연결 완료"
            }
    
            println("${fileDownloadCoroutine.await()}")
            println("${databaseConnectCoroutine.await()}")
    
        }
    
        runBlocking {
            job.join()
        }
        println("메인 쓰레드 종료!")
        job.cancel()
    
    }

     

    근데, 아래와 같이 실행해도 동일한 결과가 나오는데 무슨 차이인지 잘 모르겠다.. (튜터님 질문 답변 듣고 수정하기!)

    // 동일한 결과를 나타내는 다른 코드
    var fileDownloadCoroutine = async(Dispatchers.IO) {
                delay(5000)
                println("파일 다운로드 완료")
            }
    
            // 코루틴 2
            var databaseConnectCoroutine = async(Dispatchers.IO) {
                delay(10000)
                println("데이터베이스 연결 완료")
            }
    
    //        println("${fileDownloadCoroutine.await()}")
    //        println("${databaseConnectCoroutine.await()}")

     

    4. 쓰레드&코루틴 정리

    코루틴은 쓰레드보다 CPU 자원을 절약하기 때문에 Light-Weight Thread 라고 한다. 구글에서는 코루틴 사용을 적극 권장한다.

    쓰레드 코루틴(=Light-Weight Thread)
    Context Switching No-Context Switching
    → 소스 코드를 통해 Switching 시점을 마음대로 정함
    운영체제 커널의 개입 존재 운영체제(OS)의 관여 없음
    Blocking 방식 Suspend 방식(Task 가 바뀔 때 기존 Task는 suspend 됨)
    - Task 가 바뀌면 기존 Thread 는 블로킹 상태가 됨
    - 하나의 Thread 당 하나의 Task 수행 
    - Task 가 바뀌어도 동일한 Thread 에서 진행
    - 하나의 Thread 가 여러 Task object 를 동시 수행 가능
    작업 단위: Thread
    스케쥴링 : 어떤 Thread를 먼저 실행할지 결정하는 행
    작업 단위 : Object
    Coroutine Object는 JVM Heap 에 저장됨

    쓰레드 작동방식
    코루틴 작동방식

     

Designed by Tistory.