스레드와 코루틴

김태영·2024년 6월 11일
0

TIL

목록 보기
26/70
post-thumbnail

오늘 공부한 것

- 알고리즘 카펫 풀이
- 코틀린 문법 종합반 5주차 복습
- 프로세스, 스레드, 코루틴 학습

개요

나는 부끄럽지만 CS 지식이 없다. 이전에 스터디도 해보고, 책도 읽어봤지만 머리에 남아있는 건 뭐가 있지..?ㅠ 사실 지금은 코틀린을 이용한 안드로이드 앱 개발에 대해 배우고 있어서 CS를 공부할 시기가 아니긴 한데, 자주 나오는 단어들을 이해 없이 넘어가다 보면 나중에 다칠 것 같아 알아보았다. 자세한 개념들은 적당히 넘겨가면서 코틀린의 스레드와 코루틴을 이해할 수 있을 정도로만 공부해보았다.

메모리의 구조

메모리란 무엇일까. 보통 RAM(Random Access Memory)이라고 부르며, 단기적으로 데이터를 저장하고, 접근할 수 있는 공간을 제공한다. 더 자세히 타고 들어가면 뭐 주기억장치는 ROM과 RAM으로 나뉘고, ROM은 비휘발성인데 RAM은 휘발성이고… 등등이 있겠지만, 오늘은 CS를 공부하는 것이 아니니까! 정말 간단하게만 정리하겠다.

메모리는 단기적으로 데이터를 저장하기 때문에 프로그램 파일 같은 걸 저장하는 용도가 아니다. 그저 CPU가 작업할 수 있도록 하는 공간을 제공하는 것이다. 여기서 말하는 CPU의 작업으로는 프로그램 실행이나 인터넷 검색 등의 흔히 우리가 “컴퓨터를 사용한다”고 말하는 작업들을 말한다. 이 RAM의 용량이 커야 인터넷 창 여러 개 띄워 놓고 안드로이드 에뮬레이터 여러 개를 띄워 놔도 렉이 안걸린다.

프로그램을 실행하면, 혹은 객체를 생성하면 메모리에 올라가게 되고, 이걸 메모리에 “로드(load)”된다고 말한다. 프로그램을 냅다 메모리에 올리는 게 아니라, 실행에 필요한 부분, 정적인 변수 부분, 함수를 호출할 부분 등으로 나눠 각 부분마다 메모리 공간을 할당하게 된다. 나누는 이유는 CPU가 작은 메모리에 더 빨리 접근할 수 있기 때문이라고 한다.

그럼 드디어 메모리가 어떤 식으로 구성되어 있는지 알아보자.

메모리는 위 그림과 같이 크게 4가지 영역으로 나눌 수 있다. 하나하나 알아보자.

코드 영역 (텍스트 영역)

  • 작성한 소스코드가 저장되는 영역으로, 기계어 형태(0과 1)로 저장된다.
  • 실행 파일을 구성하는 명령어들이 올라간다. ex. 함수, 제어문, 상수
  • CPU는 저장된 명령어들을 하나씩 가져가서 실행한다.

데이터 영역

  • 전역 변수와 정적 변수가 저장된다.
  • 프로그램의 시작과 함께 (main 함수 이전에) 할당되고, 프로그램이 종료되면 소멸한다.

힙 영역

  • 런타임 시 크기가 결정된다.
  • 이 영역에 메모리를 할당하는 것을 동적 할당이라 한다.
  • 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.
    • 사용 후에 메모리 해제를 해주지 않으면 메모리 누수 현상이 발생한다. 꼭 해제를 해주자.
    • 메모리 누수란 필요하지 않은 메모리를 계속 점유하고 있는 현상이다.
    • JVM 위에서 돌아가는 Kotlin은 JVM의 가비지 컬렉터가 자동으로 해제해준다.
  • 메모리의 낮은 주소에서 높은 주소의 방향으로 할당되는 선입선출(FIFO) 구조이다.
  • 참조형 데이터 타입이 저장된다.
    • 단, 힙 영역에 있는 객체를 가리키는 변수는 스택에 적재한다.

스택 영역

  • 컴파일 시 크기가 결정된다.
  • 프로그램이 자동으로 사용하는 임시 메모리 영역이다.
  • 함수 호출 시 메모리 할당, 함수 호출 완료 시 메모리는 해제된다.
  • 메모리의 높은 주소에서 낮은 주소의 방향으로 할당되는 후입선출(LIFO) 구조이다.
  • push로 데이터 저장, pop으로 데이터를 꺼낸다.
  • 함수의 호출과 관계되는 지역 변수와 매개변수가 저장된다.

Stack Overflow와 Heap Overflow

그림을 다시 보면 힙 영역과 스택 영역은 각각 위에서, 아래에서 부터 차곡차곡 쌓이는 구조이다. 이 둘은 사실 같은 공간을 공유하는데, 각 영역이 상대 영역을 침범하는 경우 Heap Overflow, Stack Overflow라고 한다. 스택 오버플로우를 마냥 지식인 사이트로만 알았었는데, 힙 영역을 침범했던 것이었구나..

JVM 메모리 구조

그냥 메모리 구조만 알아서는 안된다. 사실 JVM 메모리 구조만 알아도 되긴 하다.. 아무튼 코틀린은 JVM 위에서 돌아간다. 그리고 JVM은 자바 바이트 코드를 기계가 실행할 수 있는 형태로 변경해준다. JVM은 운영체제로부터 메모리를 할당받고, 코틀린의 클래스들은 그림에 나온 JAVA 컴파일러 대신 코틀린 컴파일러를 통해 바이트 코드로 변경되어 JVM의 메모리 영역인 Runtime Data Area에 적재된다.

메모리 구조와 비슷하니 간단하게만 알아보자.

Method 영역

  • JVM이 시작될 때 생성된다.
  • 클래스, 인터페이스, 메소드, 필드, static 변수 등의 바이트 코드를 보관한다.
  • 프로그램을 종료할 때까지 명시적으로 null을 선언할 경우 가비지 콜렉팅의 대상이 된다.

Heap 영역

  • 런타임 시 동적으로 할당하여 사용한다.
  • 인스턴스와 배열등의 참조형이 저장된다. (메소드 영역에 로드된 클래스만 생성이 가조하다.)
    • 힙의 참조 주소는 스택이 갖고 있고, 객체를 통해 힙 영역에 있는 인스턴스를 다룰 수 있다.
  • 객체가 더 이상 쓰이지 않거나, 명시적으로 null을 선언한 경우 가비지 콜렉팅의 대상이 된다.

코틀린은 모든 자료형이 참조형인데?

이 전에 자료형에 대해 잠깐 정리했을 때, 코틀린은 모든 자료형이 참조형 자료형이라고 했다. 그런데 참조형 자료형은 메모리 해제가 필요한 힙 영역에 저장된다. 모든 변수를 참조형으로 다루면 메모리 누수가 쉽게 발생하는 것 아닌가? 라고 생각해서 찾아보았다.

코틀린의 자료형은 컴파일 시 Java의 기본형에 맞는 자료형이 있다면 그것에 맞게 변환해준다고 한다. 예를 들어 Int면 Java의 int형으로 변환되는 것이다! 따라서 걱정할 필요는 없다~

Stack 영역

  • 정수형, 실수형 등의 기본형이 저장된다.
  • 임시적으로 사용되는 변수나 정보들이 저장된다.
  • 후입선출(LIFO) 구조이다.

PC register

  • 현재 실행 중인 JVM 주소를 가진다.
  • CPU 명령어를 수행한다.

Native Method Stack

  • 자바 외의 언어로 작성된 네이티브 코드를 위한 영역이다.

프로세스 Process

스레드라는 녀석을 알려면 또 프로세스를 알아야 한다. 프로세스는 쉽게 말해 실행 중인 프로그램의 단위이다. 보통 작업 관리자의 프로세스 탭에 있는 프로그램 각각이 프로세스라고 할 수 있다.

위에 작성한 메모리의 구조에서, 프로그램은 메모리에 로드되어야 실행된다고 했다. 또 메모리에 로드되는 것은 메모리 공간을 할당 받는 것으로도 이해할 수 있다. 프로그램 하나 당 위에서 말했던 메모리 구조 하나씩 할당된다.

그런데 메모리 공간을 할당 받았다고 해서 프로그램을 실행할 수 있는 게 아니다. 그저 저장하는 것 뿐.. CPU가 해당 메모리에 접근해서 코드를 실행시켜야 프로그램이 실행된다.

메모리 공간과 (메모리에 접근하고 코드를 실행시키는) CPU의 시간 등을 CPU 자원이라고 하고, 프로그램은 이런 CPU 자원을 할당 받아 프로세스가 된다.

프로그램과 프로세스

프로그램은 코드 덩어리고 프로세스는 실제로 실행되는 프로그램이라는 것까지는 이해했다. 그렇다면 크롬을 여러 개 실행하게 되면 어떻게 될까? 작업 관리자에는 크롬 프로세스가 여러 개 쌓이게 된다. 즉, 프로그램 하나는 여러 개의 프로세스를 가질 수 있는 것이다.

스레드 Thread

드디어 스레드에 대해 정리할 수 있다. 하나의 프로세스는 하나의 (스택, 힙, 코드, 데이터로 구성된) 메모리 공간을 할당받는다. 따라서 한 번에 하나의 작업 만을 수행할 수 있다.

그런데 우리는 하나의 크롬을 실행시키더라도, 파일을 다운로드 받으면서 유튜브를 보면서 웹 서핑도 할 수 있다. 바로 스레드 덕분에 가능한 일이다.

스레드는 한 프로세스 내에서 동시에 진행될 수 있는 작업의 단위이다. 스레드를 단어 그대로 “실”로 표현해 하나의 실행 흐름, 또는 실행 가닥이라고 표현하는 경우도 있다. (사실 내가 그렇게 배웠음)

프로그램과 프로세스의 관계처럼, 하나의 프로세스는 여러 개의 스레드를 가질 수 있다. 이를 멀티 스레드라고 한다.

메인 스레드?

프로세스를 생성하면 기본적으로 메인 스레드 하나가 생성된다. 이외의 스레드는 개발자가 직접 만들어주어야 하는 것이다.. (가질 수 있다고 했지 자동은 아니라고 했다.) 다시 정리해보면 프로세스는 메인 스레드를 포함해 적어도 하나의 스레드를 갖고 있고, 여러 개 존재하는 것을 멀티 스레드라고 한다.

멀티 스레드

프로세스는 하나의 독립된 메모리 공간을 할당받기 때문에 다른 프로세스의 메모리에 직접 접근은 불가능하다. 그러나 스레드는 프로세스의 자원을 공유해서 사용하기 때문에 다른 스레드의 정보에 접근할 수 있다.

정확히 말하자면 스레드는 프로세스의 자원 중 메모리의 스택 영역을 할당 받아 복사해 각각의 독립된 스택 영역을 갖고 있게 된다. 이 외에 코드, 데이터, 힙 영역은 서로 공유해서 사용한다.

코틀린의 스레드

코틀린의 스레드도 JVM 위에서 동작한다는 것만 빼면 동일하다. 단, JVM의 Heap 영역과 Method 영역은 공유하고, 나머지 Stack Area, PC Register, Native Method Stack은 개별로 생성한다.

코루틴 Coroutine

정말 멀리 돌아온 느낌이다. 단어를 보면 Co + Routine의 형태이다. 즉, 협력하여 작업을 실행하자라는 의미가 있는 코루틴은 프로세스에서의 스레드처럼 동시 처리를 위해 사용되며, 스레드보다도 더 작은 작업 단위이다. 스레드 하나 당 여러 개의 코루틴을 가질 수 있다. 구글에서는 이 코루틴의 사용을 적!극! 권장한다고 한다.

하나의 스레드 안에서 여러 개의 코루틴 객체를 만들어 각각의 작업에 객체를 할당하는 방식으로 동작한다. 코틀린의 경우, 코루틴 객체이기 때문에 JVM Heap에 적재된다고 한다. 그렇다면, 스레드에 관계 없이 실행된다는 것으로 이해하면 되지 않을까? 실제로, 특정 스레드에 속하지 않고 한 스레드에서 중단된 루틴이 다른 스레드에 의해 재실행될 수 있다고 한다.

동시성 Concurrency

사람의 뇌는 한 번에 한 가지 일 밖에 처리하지 못한다. 멀티 태스킹을 잘 하는 사람은 사실 동시에 여러가지를 하는 것이 아니라, 빠르게 집중하는 곳을 바로바로 바꿔 동시에 하는 것처럼 보이게 되는 것이다.

CPU도 그렇다. 사실 CPU의 “코어”가 그렇다. 코어 하나 당 하나의 작업 만을 처리할 수 있기 때문에 여러 작업을 조금씩 나눠 번갈아가면서 실행하게 되고, 이걸 시분할 기법이라고 한다. 이 때, 실행하는 작업을 바꾸는 것을 Context Switching 이라고 한다.

💡 코어가 여러 개라면?

각 코어에 프로세스와 스레드를 돌려 여러 작업을 정말 동시에 수행할 수 있다. 이는 병렬성(Parallelism) 프로그래밍이라고 한다. 멀티 코어 프로세서에서 가능한 일이라고 한다.

동시성과 병렬성 Concurrency vs. Parallelism

동시성Concurrency병렬성Parallelism
동시에 실행되는 것처럼 보이는 것실제로 동시에 실행되는 것
논리적인 개념물리적인 개념
싱글 코어, 멀티 코어에서 가능멀티 코어에서만 가능

블로킹과 논블로킹 Blocking, Non-blocking

스레드와 코루틴의 동시성을 보장하기 위한 방식을 알아보기 전에, 블로킹과 논블로킹에 대해 대충 알아보고 넘어가자. 참고할 것은 Block이 차단 혹은 대기라는 뜻을 가진다는 것이고, 제어권은 자신의 코드를 실행할 권리로 이해하면 되겠다.

  • 블로킹
    • A가 B를 호출하면 제어권을 A가 B에게로 넘겨준다.
    • A는 B의 작업이 끝날 때까지 대기(Block)한다.
    • B의 작업이 끝나면 A에게 다시 제어권을 돌려준다.
  • 논블로킹
    • A가 B를 호출해도 계속 A가 제어권을 갖고 있는다. B는 실행만 시킨다.
    • A는 B의 작업 완료 여부와 관계 없이 자기 코드를 계속 실행한다.

스레드의 동시성 보장

운영체제 커널에 의한 컨텍스트 스위칭을 통해 동시성을 보장한다.

  • Thread A가 Task 1을 수행하는 동안 Task 2의 결과가 필요하다면, Thread B를 호출한다.
  • 이 때, Thread A는 블로킹 되고 Thread B로 컨텍스트 스위칭이 일어나고 Task 2를 수행한다.
  • Task 2가 완료되면 Thread A로 다시 바꾸고, 결과값을 Task 1에 반환한다.
  • 이 때, Task 3과 Task 4는 각각 동시에 실행된다.

코루틴의 동시성 보장

소스 코드를 통한 Programmer Switching으로 동시성을 보장한다. 코드를 통해 스위칭 시점을 마음대로 정할 수 있다.

  • Object 1이 Object 2의 결과를 기다릴 때 Object 1은 Suspend(일시 중지) 상태가 된다.
  • 같은 스레드에서 Object 2를 수행할 수 있기 때문에 컨텍스트 스위칭이 일어나지 않는다.
  • Thread C처럼 하나의 스레드에서 여러 Object를 (시분할 기법으로) 동시에 수행할 수 있다.
  • 그러나 Thread A와 동시에 수행되어야 하기 때문에 이 때는 컨텍스트 스위칭이 필요하다.
    • 따라서 단일 스레드에서 여러 코루틴을 관리하는 것을 권장한다고 한다.
  • 다수의 코루틴을 실행할 수 있고, 컨텍스트 스위칭이 일어나지 않는 특징 때문에 Light-Weight Thread라고 말한다.
    • 스레드 보다 CPU 자원을 절약한다.

스레드 vs. 코루틴

스레드와 코루틴은 각자의 방법으로 동시성을 보장하기 위한 기술이다. 코루틴은 스레드를 대체하기 위한 수단이 아닌, 스레드를 더 효율적으로 쓰기 위한 수단이라고 한다. 차이점을 정리해보자.

스레드코루틴
작업의 단위ThreadCoroutine Object
동시성 보장 수단Context SwitchingProgrammer Switching
다른 작업의 결과를 기다릴 때BlockingSuspend

마치며

사실 관련된 강의는 어제 들었지만, 추가로 알아야 할 게 많은 것 같아 오늘 알아보면서 정리했다. 정리하다 보니 개념에 대해 어느 정도 이해는 한 것 같기도 한데 이게 또 코드를 작성하려고 하면 헷갈릴 것 같다.

그래도 개념만 알아둔다면 비동기 처리는 쉽게 잘 만들어 놓은 라이브러리 갖다가 쓰지 않을까 하는 행복 회로를 돌리는 중이다. 아니라면 유감인거지 뭐~

profile
화이팅

0개의 댓글