코루틴과 동시성 프로그래밍 (1/3)

장똑대·2022년 5월 3일

Do it! 코틀린 프로그래밍 [셋째마당, 코틀린 표준 라이브러리의 활용] 학습

✏️1. 동시성 프로그래밍✏️

  • 동기적(synchronous) 프로그래밍
    • 순서대로 작업을 수행하여 1개의 루틴을 완료한 후 다른 루틴을 실행하는 방식
    • 코드의 복잡도가 낮음
    • 코드의 여러 구간에서 요청된 작업이 마무리가 될 때 까지 멈춰 있는 현상 블로킹(Blocking)이 발생

  • 비동기적(Asynchronous) 프로그래밍
    • 여러 개의 루틴이 선행 작업의 순서나 완료 여부와 상관없이 실행되는 방식
    • RxJava, Reactive와 같은 서드파티(third-party) 라이브러리에서 제공
    • 코틀린에서는 코루틴(Coroutine)을 서드파티가 아닌 기본으로 제공
    • 다중작업을 위해 스레드와 같은 비동기 코드를 작성하면 코드가 복잡해짐

1-1. 블로킹과 넌블로킹

📌 블로킹 동작

  • 입출력 과정(I/O)인 읽기나 쓰기(R/W)과정이 수행될 때 태스크 A의 코드가 더이상 진행되지 않고 내부 메모리 영역에서 해당 작업이 마무리 될 때까지 코드가 멈춤 -> 이런 상황을 코드가 '블로킹'하고 있다고 함
  • 태스크 A가 블로킹하는 동안 운영체제의 스케줄링 정책에 따라 우선순위가 낮은 또 다른 태스크 B가 실행될 수 있음
  • 태스크 A의 실행이 재개되면 우선순위가 낮은 B는 블로킹하고 태스크 A가 종료되면 태스크 B가 재개

📌 넌블로킹 동작

  • 입출력 요청을 하더라도 운영체제에 의해 EAGAIN과 같은 시그널을 태스크 A가 받아서 실행을 재개
  • 태스크 A는 다른 루틴을 수행하다가 내부적으로 입출력 완료 시그널을 받은 후 콜백 루틴(Callback Routine) 등을 호출해 완료된 이후의 일을 처리
  • 태스크 A를 수행하는 도중에 또 다른 태스크 B가 생성될 수 있는데 이때 태스크 A와 B는 비동기적으로 수행될 수 있음
  • A와 B의 실행 시점 운영체제의 스케줄링 기법에 의해 결정되어 프로그래머는 어떤것이 어떻게 수행될 지 알 수 없음
  • 태스크 A와 B는 프로세서 코어 수에 따라 동시에 수행될 수도 있고 2개의 태스크를 자주 교환에 동시에 수행되는 것처럼 보이게 할 수 있음
  • 여러개의 코어가 태스크를 동시에 수행하는 것을 병행 수행(Concurrency)라고 함

1-2. 프로세스와 스레드

📌 프로세스 (Process)

  • 코드, 데이터, 열린 파일의 식별자, 동적 할당 영역, 스택등을 가진 큰 실행 단위
    -> 이러한 정보를 문맥(Context)라고 함
  • 실행되는 메모리, 스택, 열린 파일 등을 모두 포함
    -> 프로세스 간 문맥 교환(Context-Switching)을 할 때 많은 비용이 들어감

📌스레드 (Thread)

  • 프로세스의 코드, 데이터, 열린 파일 등을 고융하는 작은 독립된 실행 단위
  • 자신의 스택만 독립적으로 가지고 나머지는 대부분 스레드끼리 공유
    -> 문맥 교환 비용이 낮아 프로그래밍에서 많이 사용
  • 여러 개의 스레드를 구성하면 코드가 복잡해짐

📌 문맥교환(Context-Switching)

  • 하나의 프로세스나 스레드가 CPU를 사용하고 있는 상태에서 다른 프로세스나 스레드가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(문맥)를 보관하고 새로운 프로세스의 상태를 적재하는 과정
  • 프로세스와 프로세스는 완전히 독립적이기 때문에 프로세스 간의 실행을 전환하려면 문맥을 저장해 두었다가 새로운 프로세스의 문맥을 불러들이는 과정 필요
  • 스레드는 레지스터와 스택만 독립적으로 가지고 있고 대부분의 문맥은 프로세스 안에서 공유
    -> 프로세스 간 전환보다 훨씬 빠르고 운영체제 입장에서 비용이 낮음

📌 스레드 생성하기

// (1) Thread 클래스를 상속받아 구현하기
class SimpleThread: Thread() {
	override fun run() {
    	println(Thread.currentThread())
    }
}

// (2) Runnable 인터페이스로부터 run() 메서드 구현하기
class SimpleRunnable: Runnable {
	override fun run() {
    	println(Thread.currentThread())
    }
}

fun main() {
	SimpleThread().start()
   
   val runnable = SimpleRunnable()
   Thread(runnable).start()
}

-> (1)번에서는 Thread 이외의 클래스를 상속할 수 없음
-> (2)번에서는 Runnable 인터페이스를 구현한 것이므로 다른 클래스를 상속할 수 있음

⬇️ 익명 객체를 사용하기

object: Thread() {
	override fun run() {
    	println(Thread.currentThread())
    }
}.start()
profile
장똑대와 안드로이드

0개의 댓글