[Q/A] Kotlin, suspend의 사용 사례

질문:

suspend fun a(){

}

위 함수는 suspsend함수니깐 async(a).await 이런식으로 launch 구문 안에서만 사용할 수 있는거 맞나요?

그리고 위의 구문을 줄이기 위해서 함수를 선언할 때 다음과 같이 선언하는게 맞는지 궁금합니다.

fun a =async(){

//  샬라샬라

}

그리고 위의 a는 항상 launch(CommonPool) 안에서 사용하면 되는거 맞나요??

만약에 그러면 비동기 처리를 할 무엇인가 생기게 된다면 순차적으로 비동기로직을 처리하기 위해서는 다음과 같이 처리하는건가요?

launch(UI){

   a.await()

  //UI처리

   b.await()

  // UI처리

}

이렇게 구문처리를 하게되면 쓰레드를 2개 사용?? launch랑 a or b를 사용하는거 같은데 성능상의 이슈는 없을까요??

답변: 

질문 1: 'suspend 함수를 async(a).await 이런식으로 launch 구문 안에서만 사용할 수 있는거 맞나요?' 

 먼저 suspend 함수는 다음과 같이 suspend 키워드를 사용해 정의하죠.

suspend fun doingSomething(name: String): String {
    // do something
    val result = name
    return result  // 처리된 반환값 
}

suspend 를 붙임으로서 코루틴으로 언제든 지연되었다가 재개 될 수 있는 함수가 정의 됩니다. 

이 함수는 코루틴 처리 블럭인 launch이나 async 에서 호출되거나 또다른 suspend 함수에서 호출 될 수 있습니다.  실행을 위해 main() 블록에 다음과 같이 쓸 수 있죠.

fun main(args: Array<String>) {
    val job = launch(CommonPool) {
        val a = doingSomething("Kildong")
        println(a)
    }
   // ....
  // 어디선가 job을 join() 시킨다.
}

launch(<문맥 및 옵션>) { ...suspend함수 사용... }  형태로 사용할 수 있죠. main() 할일이 없어 그대로 종료 되버리면 doingSomething가 뭔가 하기전에 프로그램이 끝날 수 있으니 join이나 main을 좀 지연시킬 필요가 있습니다. 

async를 사용 하려면 launch와 함께 사용합니다. 

fun main(args: Array<String>) {
    val ret = async {
        val a = doingSomething("Kildong")
        a // 마지막 식이 반환된다.
    }
    launch {
        println(ret.await())
    }
//...
}

launch 블록에서 ret 결과를 받기 위해 await() 호출해 결과가 반환 되기까지 기다립니다. 

질문2: '이것을 줄이기 위해서...'

먼저 suspend를 줄이기 위해서 다음과 같이 람다식 함수 자체를 할당 시킬 수 있습니다. 

fun doingSimple(name: String) = async {
    // do something
    val result = name
    result  // 처리된 반환값, 마지막은 반환되는 식이므로 return 필요 없음
}

그 다음 main() 블록에서 다음과 같이 수정됩니다.

fun main(args: Array<String>) {
    launch {
        val a = doingSimple("Kildong").await()
        println(a)
    }
    // ...
   Thread.sleep(200)  // main을 약간 지연하기 위해
}

조금 더 줄여보기 위해 main 블록을 runBlocking으로 지정합니다. 

fun main(args: Array<String>)  = runBlocking<Unit> {
    val a = doingSimple("Kildong").await()
    println(a)
   //...
}

runBlocking을 사용하면 main() 자체가 코루틴 블록이 되므로 launch를 사용하지 않아도 됩니다.  이 모든건 람다식 함수 표현으로 이루어져 있기 때문에 얼마든지 여러가지 스타일의 코드를 만들어 낼 수 있을 것입니다. 

질문3:  'launch(CommonPool) 안에서 사용하면 되는거 맞나요??'

​​​​​​​네. 기본 문맥은 CommonPool이므로 사용자가 정하지 않는 이상 launch {...} 만 사용하면 디폴트인 CommonPool이 됩니다. 

CommonPool은 일종의 공유풀 개념으로 스레드를 생성하는 오버헤드 없이 미리 만들어놓은 요소를 사용하기 때문에 스레드를 일일이 생성하는 버전 보다 빠르겠죠?

UI는 안드로이드에서 그래픽 요소를 다루기 위한 전용 스레드를 말합니다. 일반 스레드에서는 UI를 다룰 수 없죠. 따라서, UI로 문맥을 지정하면 UI요소에 접근이 허용됩니다.

 

질문4: '비동기 처리를 할 무엇인가 생기게 된다면 순차적으로 비동기로직을 처리하기 위해서는 다음과 같이 처리하는건가요?'

async와 함께 사용한다면 ​​​​​​​작업을 실행하는 시점은 OS에 의해 결정됩니다. 순차 처리가 아니라 병렬로 처리되는 것이죠. 만일, 순차적 처리가 필요하면 async를 사용하지 않고 launch {...} 에 작업을 나열합니다. 

    launch(CommonPool) {
        val one = work1()
        val two = work2()
        // ...
    }

병렬 처리인 비동기 방식으로 하려면, 

  val one = async(CommonPool) { work1() }
  val two = async(CommonPool) { work2() }

  launch(CommonPool) {
      // 조건에 따라 
      one.await() ...
      two.await() ...
  }

work1() 혹은 work2()가 언제 실행 완료될지 모르니 조건에 따라 await()에서 멈춰 있다가 결과를 받아오도록 하면 됩니다. 

질문5: '쓰레드를 2개 사용?? launch랑 a or b를 사용하는거 같은데 성능상의 이슈는 없을까요??'

​​​​​​​스레드는 run()을 구현하는 것이고 스레드를 생성하면 오버헤드가 있습니다. launch는 단순히 스레드를 사용하는게 아니라 아까 말했던 공유풀과 같은 개념으로 사용되고, 문맥 교환이라는 오버헤드도 없기 때문에 성능상 스레드처리보다 코루틴이 훨씬 좋습니다. 2개로는 확인하기 힘들고 예를 들어 작업이 10만개가 넘는다면 차이가 나겠죠. 코루틴은 10만개의 작업도 공유풀을 쓰므로 문제 없지만, 스레드는 10만개 새로운 문맥을 생성하다가 OutOfMemoryException을 만날 확률이 높습니다. 

 

 

youngdeok의 이미지

Language

Get in touch with us

"어떤 것을 완전히 알려거든 그것을 다른 이에게 가르쳐라."
- Tryon Edwards -