시작하며

본 포스팅은 글또 9기 활동 중 Udemy 로부터 강의 쿠폰을 지원받아 작성되었습니다.

이번 포스팅은 ‘글또 9기’ 활동의 일부로, Udemy에서 지원해주신 쿠폰으로 수강한 【한글자막】 Java 멀티스레딩, 병행성 및 성능 최적화 강의에 대한 후기 글이다. 멀티스레드의 필수(기초)이론부터 심화주제까지 폭넓게 다루고 있고, 멀티스레딩과 병행성에 대한 이론적인 학습은 물론 관련 코드를 포함한 실습과정이 잘 구성되어 있었다.

데이터 엔지니어링에 관심을 가질수록 그 기반이 되는 백엔드 엔지니어링에도 관심이 생긴다. 회사 업무인 마이데이터 시스템 아키텍처에서도 멀티스레딩을 통한 병행성은 성능 최적화의 핵심이 된다. 사실 멀티스레딩은 취준생 시절에 이론적인 공부만 얕게 해봤지 제대로 된 강의로 학습해 본 적은 없었는데, 이번 기회로 멀티스레드 아키텍처의 기초부터 성능 최적화, ReentrantLock, Lock-Free 알고리즘, 비동기-논블록킹 IO가상 스레드까지 공부할 수 있었다. 추천하는 이유는 다음과 같다.

  1. 강사님(Michael Pogrebinsky) 설명이 굉장히 깔끔하다.
    • Thread, Runnable과 같은 정말 기본 내용으로 시작하여 심화된 개념까지 깔끔하게 정리하여 전달해준다.
    • 불필요한 내용으로 혼란을 주지 않고 핵심만 짚어준다. 수강생이 이해하기 편하도록 흐름을 배려해 구성했다는 것이 느껴졌다.
  2. 강의 구성이 개념을 익히기에 적합하다.
    • 이론을 코드로 옮겨 실습하는 과정과 섹션별 코딩 실습은 중간에 점검하고 넘어가기 좋은 구성이다.
    • 개념을 전체적으로 정리한 뒤 그대로 옮겨진 코드 실습, 적절한 예제(코딩 연습)로 개념을 재점검한다.
  3. 실무와 밀접하게 연관된 내용을 배운다.
    • 코드를 그대로 적용할 수 있는 것은 아니지만, 다루는 개념들이 실무에 직접적으로 연관되어 있어 의심스러웠던 부분들이 강의를 듣고 훨씬 깔끔하게 정리되었다.

배우는 내용

강의에서 배우게 되는 내용은 다음과 같다.

  1. 운영 체제의 기본 내용 및 멀티스레딩과 병행성이 필요한 이유
  2. 멀티스레딩의 기본 - Java에서 스레드를 생성하는 방법 및 스레드 간에 소통하는 방법
  3. 멀티스레드 병렬 실행 애플리케이션의 성능 관련 고려 사항 및 설계 패턴 (지연 시간 또는 처리량 최적화)
  4. Java에서 스레드 간 데이터를 공유하는 방법, 발생할 수 있는 함정과 솔루션 및 모범 사례
  5. 반응성과 성능을 향상시킬 수 있는 락이 걸리지 않은 고급 알고리즘 및 데이터 구조

강의 목차로 정리하면 다음과 같다.

  1. 개요
  2. 스레딩 기초 - 스레드 생성
  3. 스레딩 기초 - 스레드 조정
  4. 성능 최적화
  5. 스레드 간 데이터 공유
  6. 병행성 문제와 솔루션
  7. 락킹 심화
  8. 스레드 간 통신
  9. Lock-Free 알고리즘, 데이터 구조 및 기술
  10. 고성능 IO를 위한 스레딩 모델
  11. 가상 스레드와 고성능 IO
  12. 마무리

사전 점검 - 멀티스레드 기초 이론

강의 처음은 개요를 통해 멀티스레딩의 기본 이론을 다루고 시작한다. 아래 내용은 바로 코드 실습부터 들어가고 싶은 분들을 위해 첫 개요 섹션을 빠르게 훑어볼 수 있도록 정리한 것이다.

멀티스레드가 필요한 이유

  1. responsiveness(응답성)
    • 개별 task들을 각각의 thread가 처리함으로써 응답성을 향상시킬 수 있다.
    • 이런 multitasking은 Concurrency라고 하며, 한 개의 코어로도 구현할 수 있다.
  2. performance(성능)
    • 단일 thread로 구현하는 것보다 훨씬 빠르게 정해진 기간 동안 일을 해결할 수 있다.
    • 동일한 이유로 하드웨어 수나 서비스를 유지하는 공수가 줄어든다.

프로세스, 스레드 동작 방식

  1. 컴퓨터 전원을 켜면 운영체제가 disk에서 메모리로 로드된다. (이때 다른 프로그램들은 파일 형태로 디스크에 있다.)
  2. 사용자가 프로그램을 실행시키면 해당 프로그램을 디스크에서 가져와 인스턴스 형태로 메모리에 생성한다.
    • 이때의 인스턴스를 ‘프로세스(Process)’ 혹은 ‘어플리케이션 컨텍스트(Context of an Application)‘라고 한다. 따라서 각각의 프로세스는 완전히 구분되어 있다.
    • 프로세스의 메타데이터는 다음과 같다.
      • PID : 어플리케이션이 읽고 쓰기 위해 여는 파일
      • Code : CPU에서 실행되는 프로그램 명령어
      • Data(Heap) : 어플리케이션 실행에 필요한 모든 데이터가 들어있음
      • Main Thread : 적어도 하나의 메인 스레드가 있음 (멀티 스레드 환경에서는 Stack, Instruction Pointer 덩어리가 여러 개 생성되고 나머지는 공유한다.)
        • Stack : 지역변수가 생성되고 함수가 실행되는 영역
        • Instruction Pointer : 스레드가 수행할 다음 영역을 가리키는 포인터

컨텍스트 스위치(Context Switch)

  1. 컨텍스트 스위치의 정의
    • 일반적으로 프로세스는 다른 프로세스들과 독립적으로 움직인다.
    • 코어 수보다 프로세스가 더 많고, 각 프로세스는 스레드 하나를 기본으로 가진다.
    • 스레드 하나를 수행하고 멈춘 뒤 스케줄링에 따라 다른 스레드를 수행하는 것을 Context Switch라 한다.
  2. 컨텍스트 스위치의 주의점
    • 동시에 많은 스레드를 다룰 때는 효율이 떨어질 수밖에 없다. 이는 병행성의 대가이다.
      • CPU에서 실행되는 각 스레드는 레지스터, 캐시, 메모리의 커널 리소스를 사용하는데, Context Switch 단계에서 이를 전부 저장해두고 다른 스레드의 데이터를 CPU와 메모리에 복원해야 하기 때문이다.
    • 너무 많은 스레드를 다루면 Thrashing이 발생한다. Thrashing은 운영체제가 실제 작업보다 Context Switch 작업에 더 많은 시간을 할애하게 되는 것이다.
    • 한 프로세스 내의 스레드는 자원을 공유하므로, 동일 프로세스 내의 컨텍스트 스위치가 다른 프로세스 간 스레드 컨텍스트 스위치보다 효율적이다.

운영체제의 스레드 스케줄링 방법

  1. First Come, First Serve : 먼저 도착한 스레드를 먼저 실행 — 다른 thread의 기아 문제를 야기한다.
  2. Shortest Job First : 실행시간이 짧은 스레드를 먼저 실행 — 실행시간이 긴 스레드는 영영 실행이 안 될 수 있다.
  3. (실제) Epochs : 운영체제가 Epoch에 맞추어 시간을 적당한 크기로 나누고 각 스레드의 타임 슬라이스를 종류별로 에폭에 할당한다. 단, 모든 스레드가 각 에폭에서 완료되는 것은 아니다.
  4. (실제) Dynamic Priority : Dynamic-priority = Static-priority(개발자가 미리 정해둔 우선순위) + bonus(각 Epoch마다 OS가 세팅, 예: 실시간 스레드에 더 점수를 부여하고 기아현상도 고려)

Thread를 사용하는 경우, Process를 사용하는 경우

  1. Thread를 사용하는 multi-thread architecture
    • 자원을 많이 공유하는 경우 유리하다.
    • Thread는 생성과 파괴가 훨씬 빠르며, 같은 프로세스 내의 컨텍스트 스위치는 프로세스 간 스위치보다 빠르다.
  2. Process를 사용하는 multi-process architecture
    • 보안성과 안정성이 중요하다면 독립된 프로세스로 수행하는 것이 나을 수 있다. (멀티스레드 앱은 스레드 하나가 앱 전체를 다운시킬 수 있음)
    • 서로 관련 없는 작업을 하나의 프로세스로 통합하는 것 역시 의미가 없다.

정리하며

Udemy에는 참 좋은 강의들이 많은 것 같다. 이번 강의도 꼼꼼하게 정리하여 멀티스레딩 관련 포스팅을 연재해보려 한다. 위 강의는 JVM, 멀티스레딩, 성능 최적화에 관심은 많으나 시간이 부족한 분들, 이론과 실무를 병행하여 빠르게 학습하고 싶은 분들께 추천한다. 👋

참고문헌