0. 코루틴이란?

  • 코루틴은 비동기 실행을 간단하게 하기위한 concurrency design pattern이다.
    • suspendable computation 기법을 사용한다.
      • 함수의 실행을 특정 지점에서 일시 중단하고 나중에 다시 실행할 수 있는 기법
  • 코루틴(Coroutine)은 코틀린(Kotlin)에만 존재하는 것이 아니다.
    • Python, Go, C#, Js 등에서도 사용하는 개념이다.

1. Main Routine, Sub Routine, Co Routine

Subroutine vs Coroutine

  • 일반적으로 한 함수에서 다른 함수를 실행할 때, 호출되는 함수를 서브루틴이라고 부른다.
fun register() {
	...
	getDeviceToken()
	...
}

fun getDeviceToken() {
	...
}
  • getDeviceToken() 함수는 register() 함수의 서브루틴이다.

 

  • 코루틴은 호출한 함수의 안에서 수행되는 것이 아니라 호출한 함수와 함께 수행되는 루틴이다.

  • coroutine1안에서 coroutine2가 실행되는 것이 아니다.
  • 각 코루틴은 하나의 routine이며 쓰레드를 공유하며 함께 실행되는 것이다.
    • 따라서 병렬적으로 처리하는 것이 아닌 동시적으로 처리하는 기법이다.

Concurrency vs Parallelism

  • CPU의 코어는 1개의 프로세스만 실행할 수 있다.
  • 싱글 코어 CPU에서는 멀티태스킹을 Concurrent하게 처리한다.
    • 실제로 프로세스가 동시에 실행되지 않는다.
    • 시분할을 통해 동시에 실행되는 것 처럼 동작한다.
 

  • 멀티태스킹을 Parallel하게 하려면 코어나 CPU가 여러개 있어야한다.
    • 같은 시간에 여러 프로세스가 실행된다.

2. 동작방식 - H/W

Thread

  • 쓰레드는 같은 프로세스 내에서 Heap, Code, Data를 공유하며, Stack영역을 독립적으로 가진다.
    • Thread단위로 작업하면 context switching 비용이 프로세스에 비해 적어진다.

Coroutine

  • 코루틴의 작업 단위는 Coroutine Object로 JVM의 Heap에 적재된다.
  • 한 쓰레드에서 Coroutine Object만 변경되기 때문에 context switching이 필요없다.
    • 프로그래밍적으로 switching을 할 수 있다.
    • Coroutine을 Lightweight Thread라고도 한다.

3. 사용

코루틴은 크게 3가지로 구성된다.

  • Coroutine Scope
  • Coroutine Context
  • Coroutine Builder

코루틴이 어떤 범위에서 동작하는 지에 대한 Scope를 정하고, 어떤 쓰레드에서 코루틴을 실행할 지 Context를 설정하고, 코루틴 객체를 만드는 Builder로 구성된다.

Scope

  • CoroutineScope
    • 코루틴이 필요할 때 만들고, 필요하지 않을 때 취소할 수 있는 Scope이다.
    • 안드로이드에서는 라이프사이클을 따라간다.
  • GlobalScope
    • Application이 종료될때 까지 유지되는 Scope이다.
    • Singleton으로 만들어져 있다.
    • 메모리 누수가 생기기 쉽다.

Context

Dispatchers

코루틴이 실행이되는 쓰레드를 지정

  • Default
    • 많은 CPU 연산이 필요한 작업을 하기위한 백그라운드 쓰레드
  • IO
    • File, Network, DB IO를 위한 쓰레드
  • Main
    • UI 쓰레드

Builder

launch

  • Job 객체 반환
val job: Job = CoroutineScope(Dispatchers.IO).launch {
 // 비동기함수()
}

async

  • Deferred 객체 반환
    • 람다 안에 마지막 줄이 반환된다.
    • await() 메소드가 호출되면 async가 실행되고 실행을 완료하고 결과를 반환할 때 까지 기다린다.
suspend fun doWorld() {  // this: CoroutineScope
  val deferredInt: Deferred<Int> = CoroutineScope(Dispatchers.IO).async {
		println(1)
		2
	}
  println(3)

	val result: Int = deferredInt.await()
  
  println("$result")
  println(4)
}

// 결과
3
1
2
4

runBlocking

  • 현재 쓰레드를 block하고, 코루틴 함수를 실행한다.

withContext

  • Dispatcher 변환
    • Main 스레드에서 작업하는 중에 DB작업 같은 IO작업이 필요할때 사용할 수 있다.
CoroutineScope(Dispatchers.Main).launch {
	updateUI1()
	updateUI2()
	withContext(Dispatchers.IO) {
		insertUserData()
	}
	updateUI3()
}

Job & Deferred

  • 코틀린에서는 Coroutine Object를 Job이나 Deferred로 정의한다.
    • Deferred는 결과값을 가지는 Job이다.
  • 코루틴의 상태를 가지고 있고 흐름을 관리할 수 있다.
    • start()
      • 중단 없이 실행됨
      • 실행을 기다리지 않는다. 호출한 쓰레드가 종료되면 같이 종료
    • join()
      • Job의 동작이 완료될 때 까지 코루틴 일시 중단
    • cancel()
      • 코루틴을 종료하도록 유도한다. 대기하지 않는다.
    • cancelAndJoin()
      • 코루틴 종료 신호를 보내고 종료할 때 까지 기다린다.
    • cancelChildren()
      • 자식 코루틴 종료
  • Job states cycle
                                          wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+

예외처리

  • CoroutineExceptionHandler를 사용하여 예외처리를 할 수 있다.
    • 코루틴 안에서 예외 발생 시 exception을 handler로 전달하여 처리하는 흐름을 만들 수 있다.
    • launch vs async
val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
    throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)

4. 예시

  • 앱에 로그인 했을 때, Sendbird User를 만들고 디바이스 토큰을 등록하는 상황
    • 유저등록이 완료된 후, 디바이스 토큰을 등록해야 한다.
  • callback
fun initializeSendbird() {
	createUser() {
		registerToken() {
			doSomething() 
		} 
	}
}
  • rx
fun initializeSendbird() {
	Observable
		.just(...)
		.observeOn(MAIN_Thread)
		.subscribeOn(IO_Thread)
		.flatMap { () -> createUser() }
		.flatMap { () -> registerToken() }
		.subscribe( { () -> doSomthing() }, { fail() })
}
  •  coroutine
    • 비동기 작업을 순차적으로 작성할 수 있다.
      • initializeSendbird() 함수가 코루틴이기 때문에, createUser()를 호출하고, 코루틴을 빠져나간다. (다른 스레드에서 실행)
      • createUser()가 완료되면 코루틴으로 들어와서 다음 함수를 호출한다.
suspend fun initializeSendbird() {
    val result = createUser()
    registerToken(result)
    doSomething()
}

 

5. 동작방식 - S/W

  • 코루틴은 어떻게 suspendable하게 동작하는가?
  • 코루틴은 CPS(Conrinuation Passing Style)이라는 형태의 코드로 변환된다.
    • label로 중단점을 나누고, 함수 실행 완료되면, resume을 호출하여 코루틴을 재개한다.
suspend fun myCoroutine(cont: MyContinuation) {
    val userData = fetchUserData()
    val userCache = cacheUserData(userData)
  updateTextView(userCache)
}

⬇️

fun myCoroutine(cont: MyContinuation) {
    when(cont.label) {
        0 -> {
            cont.label = 1
            fetchUserData(cont)
        }
        1 -> {
            val userData = cont.result
            cont.label = 2
            cacheUserData(userData, cont)
        }
        2 -> {
            val userCache = cont.result
            updateTextView(userCache)
        }
    }
}

fun fetchUserData(cont: MyContinuation) {
    val result = "[서버에서 받은 사용자 정보]"
    cont.resumeWith(Result.success(result))
}

출처

[Coroutine] Coroutine(코루틴)과 Subroutine(서브루틴)의 차이 - Coroutine이란 무슨 뜻일까?

Coroutine, Thread 와의 차이와 그 특징

알기쉬운 코루틴 이론::Android Studio에서 Kotlin으로#28

코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩

Coroutine exceptions handling | Kotlin

[Kotlin] Coroutine - 코루틴의 내부 구현

구현하고 싶은 상황

  1. 현재 웹 뷰로 웹 컨텐츠를 보고있는 상황
  2. FCM을 통해 알림이 온다.
  3. 알림을 누르면 웹 뷰 안에서 새로운 웹 페이지로 이동하게 하고싶다.

구현 방법

  1. Intent만들기 + Flag 설정
  2. 웹뷰를 띄우는 Activity에서 onNewIntnet 오버라이드하기

 

val intent = Intent(this, WebViewActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
    putExtra("url", "www.example.com")
}

val pendingIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
    .setContentTitle(title)
    .setContentText(body)
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    .build()

notificationManager.notify(CHAT_NOTIFICATION_ID, notification)

이런 식으로 PendingIntent와 Notification을 만든다.

 

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);

    webView.loadUrl(intent.getExtras().getString("url"));
}

그리고 WebViewActivity 안에서 onNewIntent를 오버라이딩하면된다.

문제 상황

Notification을 클릭하면 Intent에 담긴 Extra의 url을 웹뷰로 띄우는 작업이다.

Intent로 전달하는 url을 바꾸어도 이전 url이 계속 로드되는 현상이 발생했다.

해결

pending intent의 request code때문이었다.

PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)

request code인 2번째 파라미터 값을 변경해주면 된다. 

문제

에러코드

E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #1
    Process: , PID: 25378
    java.lang.RuntimeException: An error occurred while executing doInBackground()
        at android.os.AsyncTask$3.done(AsyncTask.java:354)
        at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:383)
        at java.util.concurrent.FutureTask.setException(FutureTask.java:252)
        at java.util.concurrent.FutureTask.run(FutureTask.java:271)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter activity
        at android.os.AsyncTask$2.call(AsyncTask.java:333)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
        at java.lang.Thread.run(Thread.java:764)

발생코드

class MainActivity extends AppCompatActivity {
    ...
    public void method() {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected void onPreExecute() {
            }

            @Override
            protected Void doInBackground(Void... params) {
            	getParent(); // null
                MainActivity.this // ok
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
            }
        };
    }
}

 

Activity 안의 메소드에서 익명클래스로 AsyncTask를 구현하고 있었고, AsyncTask의 메소드 안에서 getParent() 를 호출하고 있었다.

Activity의 context를 얻고싶어 했던 것같다.

하지만 getParent()는 null로 전달되고 있었다. 

getParent()의 값을 사용하는 메소드의 callee를 kotlin으로 변환하며, Nullable이던 자바의 타입에서 NotNull로 바뀌면서 에러가 생겼다.

해결

코드가 포함되어있는 액티비티의 context를 참조하도록 바꾸었다.

getParent() -> MainActivity.this

의문점

getParent()는 뭐하는 메소드이고, 왜 null이 나오는가?

 

getParent()는 뭐하는 메소드인가?

Activity.java

/** Return the parent activity if this view is an embedded child. */
public final Activity getParent() {
    return mParent;
}
  • mParent를 반환하고 있다.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
final void setParent(Activity parent) {
    mParent = parent;
}

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
        IBinder shareableActivityToken) {
    ...
    
    mParent = parent;
    
    ...
}
  • mParent는 Activity.java의 setParent와 attach에서 할당된다.
  • 이 메소드들은 어디서 호출되는 걸까?
  • ActivityThread의 performLaunchActivity() 에서 attach()를 호출한다.
  • performLaunchActivity()는 ActivityThread의 startActivityNow()와 handleLaunchActivity()에서 호출된다. 

intent할 때 호출되는 콜스택을 보면 handleLaunchActivity()가 호출되고 있다.

startActivityNow()같은 경우는 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 어노테이션이 있다. 
P(AOS9, SDK28)이하에서 접근가능한 메소드인 것이다. (테스트 기기: AOS13)

parent 액티비티의 정보는 ActivityClientRecord 라는 클래스의 parent속성값으로, handleLaunchActivity()의 파라미터를 통해 들어온다. 

하지만 ActivityClientRecord의 Parent를 설정해주는 코드는 startActivityNow() 함수에만 있다. 이는 intent상에서 실행되지 않는 함수이기때문에 parent를 불렀을 때, null이 나왔던 것이다.

그럼 parent는 무엇일까?

getParent()의 설명을 보면 "Return the parent activity if this view is an embedded child."라고 나와있다. 

embedded child라는 것은, 현재 뷰 안에 속해있는 다른 뷰를 말하는 것으로 보인다.
한 액티비티가 다른 액티비티를 호출했을때, Caller를 Parent, Callee를 Child라고 하는 것과는 약간 달라보인다.

ActivityGroup의 설명을 보면 

"A screen that contains and runs multiple embedded activities"라고 되어있는데,

Fragment가 도입되기 전에(API11 에서 추가됨) 한 화면에 뷰를 여러개 띄울 때, 다른 뷰에 속해있는 뷰를 embedded child라고 불렀던 것같다.

 

 

+ Recent posts