[자바 무료 강의] CPU와 스레드 - 코드라떼
Lesson List button
코스자바로 배우는 프로그래밍
hamburger button
강의CPU와 스레드최종수정일 2021-09-08
아이콘약 10분

노트 강의

스레드를 조금 더 이해하기 위해 스레드와 CPU의 관계를 가볍게 살펴보는 강의입니다. 너무 깊은 내용을 다루게 되면 우리가 진행하고자 하는 범위에서 벗어나므로 스레드를 배우기 위한 선제 지식 범위까지만 다룹니다.

*혹시나 좀 더 깊은 내용을 공부하려면 컴퓨터 구조와 운영체제의 CPU 스케줄링, 페이징에 대해 공부해야 합니다.


목차


  1. CPU
  2. Core와 Process의 관계
  3. 스레드의 종류
  4. Context Switching Cost




CPU


컴퓨터에는 사람의 뇌와 같은 가장 중요한 하드웨어가 있는데요. CPU(Central Processing Unit)라고 불리는 하드웨어는 컴퓨터의 뇌와 같은 역할을 하고 있습니다.

CPU는 컴퓨터의 연산을 실행하고 제어하는 가장 중요한 일을 맡고 있으며, 컴퓨터의 작동 과정이 CPU의 제어를 받기 때문에 CPU가 없는 컴퓨터는 생각도 할 수 없습니다. 일반적으로 연산이라는 것은 수학에만 관련되어 보일 수 있지만 명령어들을 실행하는 것도 큰 범위의 연산입니다.

우리가 사용하는 일반적인 컴퓨터는 CPU가 하나만 존재하지만 특수 목적에 의한 컴퓨터는 여러 개의 CPU가 존재할 수 있습니다.

CPU가 여러 개가 존재하는 경우는 멀티 프로세서라고 부르며, 다수의 프로세서가 협력적으로 일을 처리하는 것을 멀티 프로세싱이라고 합니다.


Core


core

Core는 핵이라고 할 수 있으며 CPU에는 하나 이상의 Core가 존재하는데요. Core가 실질적인 뇌라고 볼 수 있습니다. Core가 여러 개이면 연산할 수 있는 뇌가 여러 개라고 봐도 무방합니다.

Core가 한 개인 경우를 Single-Core라고 부르며 두 개인 경우는 Dual-Core, 네 개인 경우는 Quad-Core, 여섯 개인 경우는 Hexa-Core, 여덟 개인 경우는 Octa-core라고 부릅니다. 물론 그 이상의 Core도 있습니다.


Clock

Core가 동작하려면 지속해서 0 또는 1이라는 전기적 진동을 생성해야 하는데 이를 클럭(Clock)이라고 부르며 진동수를 헤르츠(Hz)라는 단위로 표기합니다.

1 헤르츠는 초당 1번의 디지털 신호 생성을 말하며, 클럭이 높을 수록 일반적으로는 더 빠른 연산 속도를 낼 수 있습니다. 다만 주의할 점은 클럭이 높다고 전체적인 연산 속도가 빠르다고 할 수는 없습니다.

예시로 두 개의 CPU가 있고 Single-Core라고 가정합니다.

  1. Core 2.8Ghz (1초당 28억)
  2. Core 3.6Ghz (1초당 36억)

일반적으로 2번 Core가 더 빠른 연산 속도를 낼 수 있습니다.
같은 CPU 설계 아키텍처라고 가정한다면 맞습니다.

다만 이런 경우는 어떨까요?

  1. Core 2.8Ghz (1초당 28억)
    (1초당 28억번의 포크레인의 삽질)
  2. Core 3.6Ghz (1초당 36억)
    (1초당 36억번의 인간의 삽질)

아무리 인간이 더 빠른 속도로 삽을 이용하여 흙을 퍼낸다 하더라도, 포크레인에 비교할 수가 없습니다. 그렇기 때문에 클럭이 높다고 절대적으로 시간 대비 연산량이 많지는 않습니다.




Core와 Process의 관계


프로세스라는 것은 이전 강의에서 프로그램의 실행 상태라고 얘기했었는데요. 윈도우라면 작업관리자, 유닉스/리눅스 환경이면 top 명령어를 통해 프로그램과 프로세스의 상태를 확인할 수 있습니다.

운영체제는 프로그램(프로세스)이 실행되면 한정된 자원인 CPU 및 그 외 기타 자원들을 사용할 수 있도록 할당하고 관리합니다.

Core와 프로세스의 관계를 인간에 빗대어 설명하면 조금 더 쉽게 이해됩니다.

core2

뇌가 하나인 사람이 존재합니다. 일반적으로 사람은 뇌가 하나입니다. 그리고 우리는 뇌가 하나인 A라는 사람에게 세 개의 일을 맡기기로 합니다.

A라는 사람은 맡은 작업들을 어떻게 효율적으로 처리할 수 있을까요?

core3

첫 번째는 하나씩 처리하는 방법입니다.

TaskA를 처리후, TaskB를 처리하고 TaskC를 처리하는 순차적 처리 방법을 사용하면 됩니다.

모든 작업을 끝내기까지 TaskA는 7시간, TaskB는 2시간, TaskC는 4시간의 작업시간이 소모되어야 합니다.

TaskB가 끝나는 시간은
TaskA(7시간) + TaskB(2시간) = 9시간
9시간이라는 작업시간이 지나야지 TaskB가 끝납니다.

모든 작업을 끝내려면 총 13시간이 걸립니다.

현실 세계에서도 차례차례 하나씩 일을 끝내기도 합니다.

core4

두 번째는 멀티플레이를 하면 됩니다.

TaskA를 조금 끝내고, TaskB를 조금 끝내고, TaskC를 조금 끝내는 방식으로 어느 정도 균등한 시간을 투자하여 여러 번 처리하면 됩니다.

동일한 조건으로 모든 작업을 끝내기까지 TaskA는 7시간, TaskB는 2시간, TaskC는 4시간의 작업시간이 소모되어야 합니다.

다만 첫 번째 방법과 다르게 번갈아 가면서 작업하므로,
TaskA(1시간) -> TaskB(1시간) -> TaskC(1시간) -> TaskA(1시간) -> TaskB(1시간)

TaskB가 끝나는 시간은
TaskA(2시간) + TaskC(1시간) + TaskB(2시간) = 5시간
5시간이라는 작업시간이 지나야지 TaskB가 끝납니다.

모든 작업을 끝내려면 총 13시간이 걸리는 것은 동일합니다.

현실 세계에서 일처리를 하기 위해 멀티플레이를 많이 합니다.

실제로 우리가 어떤 중요한 일을 할 때 그 일만 집중적으로 할 수 있는 경우는 적습니다.

주 업무를 하고 있다가, 누군가가 갑자기 새로운 일거리를 던져주거나 또는 시간이 되어 밥을 먹으러 가야 한다던가 또는 생리 욕구 때문에 화장실을 가야 한다던가 할 수 있습니다.


문제점

그러나 첫 번째 방법이나 두 번째 방법이나 각각의 장단점이 있으며 한계점이 있습니다.


순차적 처리의 문제점

첫 번째 방식에는 두 가지 문제가 있습니다.

  • 한 작업이 끝날 때까지 다른 작업을 하지 못한다는 것
  • 어떤 작업을 먼저 해야 하는지에 대한 기준의 불명확

사람으로 비유할 경우 화장실 가는 것도 하나의 작업이라고 본다면 Task A 작업을 맡은 경우, 화장실도 가지 말아야 합니다.

즉, TaskA를 끝낼 때까지는 다른 작업은 할 수 없습니다.

또한 중요한 작업을 먼저 해야 할지, 빨리 끝나는 작업을 먼저 해야 할지 기준을 정해야 합니다.

만약에 기준을 정하더라도, 어떤 작업이 더 빨리 끝날지, 어떤 작업이 더 중요한지 구분하는 문제도 발생합니다.

사람마다 다르겠지만 중요한 작업을 먼저 하는 경우도 있고, 빨리 끝나는 작업을 먼저 하는 경우도 있습니다.

여러분들은 어떤 작업부터 먼저 하시나요?

어떤 것이 더 효율적인지에 대한 문제는 쉽게 풀 수가 없습니다.


멀티플레이의 문제점

두 번째 방식에도 문제가 있습니다.

모든 작업에 균등하게 시간을 투자하여 번갈아가며 처리하는 방식은

  • 중요한 작업을 빠르게 처리할 수 없음
  • 작업을 넘나들며 번갈아 가면서 처리하기 때문에 집중력이 저하되며 몰입할 수 없음

사람으로 비유하면 중요하고 빨리 끝내야 하는 일이 다른 일을 하느라 늦게 처리될 수 있으며, 일을 번갈아 가면서 한다는 것은 이전에 했던 일을 기억해야 한다는 점과 집중력이 저하되므로 몰입이라는 장점을 놓치게 됩니다.

그리고 균등하게 시간을 투자한다는 점도 얼마만큼의 균등하게 시간을 투자해야 하는지에 대한 기준을 잡기가 쉽지 않습니다.

사실 일을 처리하면서 Task의 개수가 정해져있다면 그나마 낫습니다.

하지만 현실 세계는 더욱더 가혹합니다.

core5

일하는 도중 또 다른 새로운 일이 추가됩니다. 사람의 머리는 더욱더 복잡해집니다. 새로운 작업이 추가되는 순간 다시 우선순위도 정해야 하며 일이 얼마나 빨리 끝날지 파악해야 합니다.

현실 세계에서는 우리가 하는 작업이 얼마나 빨리 끝날지 파악하기 쉽지 않습니다. 이미 해본 작업이라면 어느 정도 걸릴지 견적이 나오겠지만, 새로운 작업이라면 견적내기가 쉽지 않습니다.

이와 같은 문제는 컴퓨터의 세계에서도 동일합니다.


컴퓨터 세계의 문제도 동일하다

core6

컴퓨터의 뇌를 Core로 생각하고, 작업을 Process라고 한다면, Core를 어떻게 사용해야 할지 고민할 수밖에 없습니다. 그래서 컴퓨터 과학자들은 이 문제를 해결하기 위해 여러 가지 방법을 연구했습니다.

실제로 컴퓨터에는 Core보다 훨씬 많은 Process가 실행되고 있으며, 운영체제가 CPU를 이용하여 Process가 문제없이 실행되도록 관리하고 있습니다.

운영체제가 CPU를 관리하는 것을 CPU 스케줄링이라고 하며 운영체제마다 CPU를 관리하는 기법이 조금씩 다릅니다.

이러한 스케줄링은 기법은 크게 두 가지 방향이 있으며

  • Preemptive Scheduling
  • Non-preemptive Scheduling

그리고 각 스케줄링 기법에 따라 다양한 방식이 있습니다.


Non-Preemptive Scheduling
  • FCFS(First-Come First-Service)
  • SJF(Shortest Job First)
  • HRN(Hightest Responseratio Next)
  • Deadline
  • Priority


Preemptive Scheduling
  • Priority
  • SRT(Shortest Remaining Time)
  • Round Robin
  • Multi level Queue
  • Multi level Feedback Queue

어떤 것이 있으며 각각 어떤 방식인지 몰라도 지금은 넘어가셔도 됩니다. 현재 이렇게 많은 방식들이 존재한다는 것만 알면 됩니다.




스레드의 종류


Thread는 작업의 실행 흐름 단위라고 했었습니다.
이러한 Thread에는 크게 두 가지로 나뉩니다.

  • 하드웨어 스레드
  • 소프트웨어 스레드


하드웨어 스레드

하드웨어 스레드는 CPU Core 내에 thread를 뜻하며, 일반적으론 1 Core 당 1 Thread로 구성되어있으나 CPU의 아키텍처가 발전되기도 했고, 비싼 자원을 더 효율적으로 사용하기 위해 SMT 기술을 이용한 가상 스레드라는 개념이 추가되어 1 Core에 2개의 Thread로 구성되기도 합니다.

물론 그 이상으로 발전할 수도 있습니다.


소프트웨어 스레드

소프트웨어 스레드에는 크게 커널 레벨 스레드와 사용자 레벨 스레드가 존재하는데요. 운영체제에는 가장 핵심인 커널이라는 것이 존재하며, 다양한 하드웨어들을 적절하게 관리합니다.

커널이 관리하는 것들 중에서 CPU를 스케줄링에 따라 Core의 하드웨어 스레드를 논리적으로 관리하는 스레드를 커널 레벨 스레드라고 합니다.


Kernel Level Thread(Native Thread)

core7

커널 레벨 스레드는 하드웨어 스레드를 이용하여 커널이 스레드의 생성 및 스케쥴링을 관리합니다. 그리고 Process는 운영체제로부터 하나 이상의 커널 스레드를 할당받으며, 할당받은 스레드를 이용하여 Process를 실행하고 운영합니다.


User Level Thread

core8

사용자 레벨 스레드는 커널 레벨 스레드를 이용하여 논리적으로 생성하고 관리하는 스레드를 사용자 레벨 스레드라고 합니다. 커널 스레드와 사용자 레벨 스레드가 1:1 관계일 수도 있고, 1:N 관계일 수도 있고 그림처럼 M:N 관계일 수도 있습니다.

간략적으로 봤는데도 다양한 용어와 개념들로 인해 굉장히 복잡합니다. 지금은 느낌만 알면 됩니다. 당장에 더 자세히 알고 싶다면 인터넷에 검색해도 되나 완전히 이해하기에는 지금은 쉽지 않으실 겁니다. 용어에 익숙해지고 여유가 생긴다면 그때 추가적인 지식을 쌓아도 됩니다.

그럼 이제 자바의 Thread에 대해서 알아봅시다.


Java Thread

core9
Java8 기준으로 Java의 Thread는 사용자 레벨 스레드를 사용하지 않습니다.

(이전에는 Green Thread라는 사용자 레벨 스레드를 지원하긴 했습니다)

Java Virtual Machine을 통해 운영체제로부터 시스템 콜을 호출하여 Kernel Thread를 할당받습니다.

Windows 운영체제에서는 Windows Thread를 할당 받고, BSD 계열에서는 POSIX Thread, 리눅스 계열에서도 POSIX Thread를 할당 받습니다.

즉 자바에서 Thread 인스턴스를 생성 후, thread.start() 메서드를 호출하면 JVM은 운영체제에게 커널 레벨 스레드를 요청하며, 자바의 스레드와 커널 레벨 스레드와 1:1 매핑됩니다.

그리고 커널 레벨 스레드는 윈도우, 유닉스/리눅스, Mac 각각 설정, 컴퓨터 환경에 따라서 최대 몇 개의 스레드를 생성할 수 있는지 다릅니다.

public static void main(String[] args) { … Thread thread = new Thread(runnable); // start() 메서드가 호출되면 그제서야 Kernel Thread를 요청한다. thread.start(); }
copy

자바의 Thread를 얘기하기 위해 CPU와 Core와 CPU 스케줄링과, Process와 Hardware Thread, Kernel Level Thread, User Level Thread가 어떤 관계인지 굉장히 간략하게 살펴봤고 적은 수의 Core로 수많은 Task를 실행해야 하는 어려움도 살펴봤습니다.

일단 커다란 그림을 머릿속에 넣었으면 세부적인 내용은 앞으로 하나씩 채워나가면 됩니다.




Thread를 많이 사용한다고 좋은 것은 아니다


우리는 이전 강의 “스레드는 종잡을 수 없다”에서 Thread를 이용하여 0에서 10억까지의 수를 더했을 때 값은 얼마인지 구하는 법에 대해 예시를 확인해 봤습니다.

예제 코드
public class Person implements Runnable { public long sum = 0; private final long from; private final long to; public Person(long from, long to) { this.from = from; this.to = to; } public void run() { for (long i = from; i <= to; i++) { if (0 == i % 2) { sum += i; } } } }
copy
public static void main(String[] args) {     try {         long startTime = System.currentTimeMillis();         Person person1 = new Person(0, 500000000);         Thread threadA = new Thread(person1);         Person person2 = new Person(500000001, 1000000000);         Thread threadB = new Thread(person2);         threadA.start();         threadB.start();         threadA.join();         threadB.join();         System.out.println(person1.sum + person2.sum);         long endTime = System.currentTimeMillis();         System.out.println(endTime – startTime);     } catch (InterruptedException e) {} }
copy

Thread를 이용하면 연산이 항상 빠를 것이라고 생각할 수 있지만, 특정 상황에서는 오히려 Single-Thread를 이용하는 것보다 더 느릴 수 있습니다.

우리는 조금 전 Core와 커널 스레드에 대해서 살펴보았으며, Java의 스레드는 커널 스레드를 사용한다고 했었고,
Core라는 자원은 유한하나 Task의 수는 많을 수 있다고 했으며 Task를 처리하는 방법으로 사람으로 따지면 멀티플레이 방법을 얘기했었습니다.

멀티플레이의 단점은 중요하고 빨리 끝내야 하는 일이 다른 일을 하느라 늦게 처리될 수 있으며, 일을 번갈아 가면서 한다는 것은 이전에 했던 일을 기억해야 한다는 점과 집중력이 저하된다고 했었습니다.

이러한 문제는 컴퓨터도 동일합니다. 운영체제에서도 한정된 Core를 이용하여 여러 Kernel Thread를 관리하는 방식은 인간의 멀티플레이와 유사한 부분이 있습니다.

core10

Core의 개수가 하나, 일하고 있는 Kernel Thread A, B로 두 개라고 가정합니다.

  1. Kernel Thread A의 작업을 실행하다가 중간에 Kernel Thread B로 넘어갑니다.
  2. 넘어가기 전에 Kernel Thread A의 작업 내용을 어디서부터 다시 시작해야 하는지 저장하고 Kernel Thread B로 넘어갑니다.
  3. Kernel Thread B의 작업을 실행하다가 Kernerl Thread B의 작업 내용을 어디서부터 다시 시작해야 하는지 저장하고 Kernel Thread A로 넘어갑니다.
  4. Kernel Thread A의 이전에 작업했던 내용을 읽은 후 시작해야 할 부분부터 다시 실행합니다.
  5. 이 과정을 작업이 끝날 때까지 반복합니다.

이렇게 Kernel Thread를 옮겨가며 작업하는 것을 Context Switching이라고 하며 들어가는 비용을 Context Switching Cost라고 합니다.

Thread를 옮겨가는 것만으로 저장, 읽기 연산이 계속 반복되며 들어가는 비용입니다.
그리고 생각보다 이 비용은 굉장히 비쌉니다.

만약에 Single-Core 컴퓨팅 환경에서 Single-Thread 연산이 아닌, Multi-Thread 연산을 하게 되면, Single-Thread 보다 더 큰 비용이 들어갑니다.

그리고 해당 예제 코드에서 발생하는 추가적인 비용은 다음과 같습니다.

  1. Thread 인스턴스 2개 생성 비용
  2. thread.start() 메서드 시점에 Kernel Thread를 생성하는 비용
  3. 연산시 Context Switching Cost

그러므로 간단한 연산은 Single-Thread 환경에서 연산하는 것이 훨씬 낫습니다.

도전자 질문
작성된 질문이 없습니다
이용약관|개인정보취급방침
알유티씨클래스|대표, 개인정보보호책임자 : 이병록
이메일 : cs@codelatte.io
사업자등록번호 : 824-06-01921
통신판매업신고 : 2021-성남분당C-0740
주소 : 경기도 성남시 분당구 대왕판교로645번길 12, 9층 24호
Lesson List button
코스자바로 배우는 프로그래밍
hamburger button
강의CPU와 스레드최종수정일 2021-09-08
아이콘약 10분

노트 강의

스레드를 조금 더 이해하기 위해 스레드와 CPU의 관계를 가볍게 살펴보는 강의입니다. 너무 깊은 내용을 다루게 되면 우리가 진행하고자 하는 범위에서 벗어나므로 스레드를 배우기 위한 선제 지식 범위까지만 다룹니다.

*혹시나 좀 더 깊은 내용을 공부하려면 컴퓨터 구조와 운영체제의 CPU 스케줄링, 페이징에 대해 공부해야 합니다.


목차


  1. CPU
  2. Core와 Process의 관계
  3. 스레드의 종류
  4. Context Switching Cost




CPU


컴퓨터에는 사람의 뇌와 같은 가장 중요한 하드웨어가 있는데요. CPU(Central Processing Unit)라고 불리는 하드웨어는 컴퓨터의 뇌와 같은 역할을 하고 있습니다.

CPU는 컴퓨터의 연산을 실행하고 제어하는 가장 중요한 일을 맡고 있으며, 컴퓨터의 작동 과정이 CPU의 제어를 받기 때문에 CPU가 없는 컴퓨터는 생각도 할 수 없습니다. 일반적으로 연산이라는 것은 수학에만 관련되어 보일 수 있지만 명령어들을 실행하는 것도 큰 범위의 연산입니다.

우리가 사용하는 일반적인 컴퓨터는 CPU가 하나만 존재하지만 특수 목적에 의한 컴퓨터는 여러 개의 CPU가 존재할 수 있습니다.

CPU가 여러 개가 존재하는 경우는 멀티 프로세서라고 부르며, 다수의 프로세서가 협력적으로 일을 처리하는 것을 멀티 프로세싱이라고 합니다.


Core


core

Core는 핵이라고 할 수 있으며 CPU에는 하나 이상의 Core가 존재하는데요. Core가 실질적인 뇌라고 볼 수 있습니다. Core가 여러 개이면 연산할 수 있는 뇌가 여러 개라고 봐도 무방합니다.

Core가 한 개인 경우를 Single-Core라고 부르며 두 개인 경우는 Dual-Core, 네 개인 경우는 Quad-Core, 여섯 개인 경우는 Hexa-Core, 여덟 개인 경우는 Octa-core라고 부릅니다. 물론 그 이상의 Core도 있습니다.


Clock

Core가 동작하려면 지속해서 0 또는 1이라는 전기적 진동을 생성해야 하는데 이를 클럭(Clock)이라고 부르며 진동수를 헤르츠(Hz)라는 단위로 표기합니다.

1 헤르츠는 초당 1번의 디지털 신호 생성을 말하며, 클럭이 높을 수록 일반적으로는 더 빠른 연산 속도를 낼 수 있습니다. 다만 주의할 점은 클럭이 높다고 전체적인 연산 속도가 빠르다고 할 수는 없습니다.

예시로 두 개의 CPU가 있고 Single-Core라고 가정합니다.

  1. Core 2.8Ghz (1초당 28억)
  2. Core 3.6Ghz (1초당 36억)

일반적으로 2번 Core가 더 빠른 연산 속도를 낼 수 있습니다.
같은 CPU 설계 아키텍처라고 가정한다면 맞습니다.

다만 이런 경우는 어떨까요?

  1. Core 2.8Ghz (1초당 28억)
    (1초당 28억번의 포크레인의 삽질)
  2. Core 3.6Ghz (1초당 36억)
    (1초당 36억번의 인간의 삽질)

아무리 인간이 더 빠른 속도로 삽을 이용하여 흙을 퍼낸다 하더라도, 포크레인에 비교할 수가 없습니다. 그렇기 때문에 클럭이 높다고 절대적으로 시간 대비 연산량이 많지는 않습니다.




Core와 Process의 관계


프로세스라는 것은 이전 강의에서 프로그램의 실행 상태라고 얘기했었는데요. 윈도우라면 작업관리자, 유닉스/리눅스 환경이면 top 명령어를 통해 프로그램과 프로세스의 상태를 확인할 수 있습니다.

운영체제는 프로그램(프로세스)이 실행되면 한정된 자원인 CPU 및 그 외 기타 자원들을 사용할 수 있도록 할당하고 관리합니다.

Core와 프로세스의 관계를 인간에 빗대어 설명하면 조금 더 쉽게 이해됩니다.

core2

뇌가 하나인 사람이 존재합니다. 일반적으로 사람은 뇌가 하나입니다. 그리고 우리는 뇌가 하나인 A라는 사람에게 세 개의 일을 맡기기로 합니다.

A라는 사람은 맡은 작업들을 어떻게 효율적으로 처리할 수 있을까요?

core3

첫 번째는 하나씩 처리하는 방법입니다.

TaskA를 처리후, TaskB를 처리하고 TaskC를 처리하는 순차적 처리 방법을 사용하면 됩니다.

모든 작업을 끝내기까지 TaskA는 7시간, TaskB는 2시간, TaskC는 4시간의 작업시간이 소모되어야 합니다.

TaskB가 끝나는 시간은
TaskA(7시간) + TaskB(2시간) = 9시간
9시간이라는 작업시간이 지나야지 TaskB가 끝납니다.

모든 작업을 끝내려면 총 13시간이 걸립니다.

현실 세계에서도 차례차례 하나씩 일을 끝내기도 합니다.

core4

두 번째는 멀티플레이를 하면 됩니다.

TaskA를 조금 끝내고, TaskB를 조금 끝내고, TaskC를 조금 끝내는 방식으로 어느 정도 균등한 시간을 투자하여 여러 번 처리하면 됩니다.

동일한 조건으로 모든 작업을 끝내기까지 TaskA는 7시간, TaskB는 2시간, TaskC는 4시간의 작업시간이 소모되어야 합니다.

다만 첫 번째 방법과 다르게 번갈아 가면서 작업하므로,
TaskA(1시간) -> TaskB(1시간) -> TaskC(1시간) -> TaskA(1시간) -> TaskB(1시간)

TaskB가 끝나는 시간은
TaskA(2시간) + TaskC(1시간) + TaskB(2시간) = 5시간
5시간이라는 작업시간이 지나야지 TaskB가 끝납니다.

모든 작업을 끝내려면 총 13시간이 걸리는 것은 동일합니다.

현실 세계에서 일처리를 하기 위해 멀티플레이를 많이 합니다.

실제로 우리가 어떤 중요한 일을 할 때 그 일만 집중적으로 할 수 있는 경우는 적습니다.

주 업무를 하고 있다가, 누군가가 갑자기 새로운 일거리를 던져주거나 또는 시간이 되어 밥을 먹으러 가야 한다던가 또는 생리 욕구 때문에 화장실을 가야 한다던가 할 수 있습니다.


문제점

그러나 첫 번째 방법이나 두 번째 방법이나 각각의 장단점이 있으며 한계점이 있습니다.


순차적 처리의 문제점

첫 번째 방식에는 두 가지 문제가 있습니다.

  • 한 작업이 끝날 때까지 다른 작업을 하지 못한다는 것
  • 어떤 작업을 먼저 해야 하는지에 대한 기준의 불명확

사람으로 비유할 경우 화장실 가는 것도 하나의 작업이라고 본다면 Task A 작업을 맡은 경우, 화장실도 가지 말아야 합니다.

즉, TaskA를 끝낼 때까지는 다른 작업은 할 수 없습니다.

또한 중요한 작업을 먼저 해야 할지, 빨리 끝나는 작업을 먼저 해야 할지 기준을 정해야 합니다.

만약에 기준을 정하더라도, 어떤 작업이 더 빨리 끝날지, 어떤 작업이 더 중요한지 구분하는 문제도 발생합니다.

사람마다 다르겠지만 중요한 작업을 먼저 하는 경우도 있고, 빨리 끝나는 작업을 먼저 하는 경우도 있습니다.

여러분들은 어떤 작업부터 먼저 하시나요?

어떤 것이 더 효율적인지에 대한 문제는 쉽게 풀 수가 없습니다.


멀티플레이의 문제점

두 번째 방식에도 문제가 있습니다.

모든 작업에 균등하게 시간을 투자하여 번갈아가며 처리하는 방식은

  • 중요한 작업을 빠르게 처리할 수 없음
  • 작업을 넘나들며 번갈아 가면서 처리하기 때문에 집중력이 저하되며 몰입할 수 없음

사람으로 비유하면 중요하고 빨리 끝내야 하는 일이 다른 일을 하느라 늦게 처리될 수 있으며, 일을 번갈아 가면서 한다는 것은 이전에 했던 일을 기억해야 한다는 점과 집중력이 저하되므로 몰입이라는 장점을 놓치게 됩니다.

그리고 균등하게 시간을 투자한다는 점도 얼마만큼의 균등하게 시간을 투자해야 하는지에 대한 기준을 잡기가 쉽지 않습니다.

사실 일을 처리하면서 Task의 개수가 정해져있다면 그나마 낫습니다.

하지만 현실 세계는 더욱더 가혹합니다.

core5

일하는 도중 또 다른 새로운 일이 추가됩니다. 사람의 머리는 더욱더 복잡해집니다. 새로운 작업이 추가되는 순간 다시 우선순위도 정해야 하며 일이 얼마나 빨리 끝날지 파악해야 합니다.

현실 세계에서는 우리가 하는 작업이 얼마나 빨리 끝날지 파악하기 쉽지 않습니다. 이미 해본 작업이라면 어느 정도 걸릴지 견적이 나오겠지만, 새로운 작업이라면 견적내기가 쉽지 않습니다.

이와 같은 문제는 컴퓨터의 세계에서도 동일합니다.


컴퓨터 세계의 문제도 동일하다

core6

컴퓨터의 뇌를 Core로 생각하고, 작업을 Process라고 한다면, Core를 어떻게 사용해야 할지 고민할 수밖에 없습니다. 그래서 컴퓨터 과학자들은 이 문제를 해결하기 위해 여러 가지 방법을 연구했습니다.

실제로 컴퓨터에는 Core보다 훨씬 많은 Process가 실행되고 있으며, 운영체제가 CPU를 이용하여 Process가 문제없이 실행되도록 관리하고 있습니다.

운영체제가 CPU를 관리하는 것을 CPU 스케줄링이라고 하며 운영체제마다 CPU를 관리하는 기법이 조금씩 다릅니다.

이러한 스케줄링은 기법은 크게 두 가지 방향이 있으며

  • Preemptive Scheduling
  • Non-preemptive Scheduling

그리고 각 스케줄링 기법에 따라 다양한 방식이 있습니다.


Non-Preemptive Scheduling
  • FCFS(First-Come First-Service)
  • SJF(Shortest Job First)
  • HRN(Hightest Responseratio Next)
  • Deadline
  • Priority


Preemptive Scheduling
  • Priority
  • SRT(Shortest Remaining Time)
  • Round Robin
  • Multi level Queue
  • Multi level Feedback Queue

어떤 것이 있으며 각각 어떤 방식인지 몰라도 지금은 넘어가셔도 됩니다. 현재 이렇게 많은 방식들이 존재한다는 것만 알면 됩니다.




스레드의 종류


Thread는 작업의 실행 흐름 단위라고 했었습니다.
이러한 Thread에는 크게 두 가지로 나뉩니다.

  • 하드웨어 스레드
  • 소프트웨어 스레드


하드웨어 스레드

하드웨어 스레드는 CPU Core 내에 thread를 뜻하며, 일반적으론 1 Core 당 1 Thread로 구성되어있으나 CPU의 아키텍처가 발전되기도 했고, 비싼 자원을 더 효율적으로 사용하기 위해 SMT 기술을 이용한 가상 스레드라는 개념이 추가되어 1 Core에 2개의 Thread로 구성되기도 합니다.

물론 그 이상으로 발전할 수도 있습니다.


소프트웨어 스레드

소프트웨어 스레드에는 크게 커널 레벨 스레드와 사용자 레벨 스레드가 존재하는데요. 운영체제에는 가장 핵심인 커널이라는 것이 존재하며, 다양한 하드웨어들을 적절하게 관리합니다.

커널이 관리하는 것들 중에서 CPU를 스케줄링에 따라 Core의 하드웨어 스레드를 논리적으로 관리하는 스레드를 커널 레벨 스레드라고 합니다.


Kernel Level Thread(Native Thread)

core7

커널 레벨 스레드는 하드웨어 스레드를 이용하여 커널이 스레드의 생성 및 스케쥴링을 관리합니다. 그리고 Process는 운영체제로부터 하나 이상의 커널 스레드를 할당받으며, 할당받은 스레드를 이용하여 Process를 실행하고 운영합니다.


User Level Thread

core8

사용자 레벨 스레드는 커널 레벨 스레드를 이용하여 논리적으로 생성하고 관리하는 스레드를 사용자 레벨 스레드라고 합니다. 커널 스레드와 사용자 레벨 스레드가 1:1 관계일 수도 있고, 1:N 관계일 수도 있고 그림처럼 M:N 관계일 수도 있습니다.

간략적으로 봤는데도 다양한 용어와 개념들로 인해 굉장히 복잡합니다. 지금은 느낌만 알면 됩니다. 당장에 더 자세히 알고 싶다면 인터넷에 검색해도 되나 완전히 이해하기에는 지금은 쉽지 않으실 겁니다. 용어에 익숙해지고 여유가 생긴다면 그때 추가적인 지식을 쌓아도 됩니다.

그럼 이제 자바의 Thread에 대해서 알아봅시다.


Java Thread

core9
Java8 기준으로 Java의 Thread는 사용자 레벨 스레드를 사용하지 않습니다.

(이전에는 Green Thread라는 사용자 레벨 스레드를 지원하긴 했습니다)

Java Virtual Machine을 통해 운영체제로부터 시스템 콜을 호출하여 Kernel Thread를 할당받습니다.

Windows 운영체제에서는 Windows Thread를 할당 받고, BSD 계열에서는 POSIX Thread, 리눅스 계열에서도 POSIX Thread를 할당 받습니다.

즉 자바에서 Thread 인스턴스를 생성 후, thread.start() 메서드를 호출하면 JVM은 운영체제에게 커널 레벨 스레드를 요청하며, 자바의 스레드와 커널 레벨 스레드와 1:1 매핑됩니다.

그리고 커널 레벨 스레드는 윈도우, 유닉스/리눅스, Mac 각각 설정, 컴퓨터 환경에 따라서 최대 몇 개의 스레드를 생성할 수 있는지 다릅니다.

public static void main(String[] args) { … Thread thread = new Thread(runnable); // start() 메서드가 호출되면 그제서야 Kernel Thread를 요청한다. thread.start(); }
copy

자바의 Thread를 얘기하기 위해 CPU와 Core와 CPU 스케줄링과, Process와 Hardware Thread, Kernel Level Thread, User Level Thread가 어떤 관계인지 굉장히 간략하게 살펴봤고 적은 수의 Core로 수많은 Task를 실행해야 하는 어려움도 살펴봤습니다.

일단 커다란 그림을 머릿속에 넣었으면 세부적인 내용은 앞으로 하나씩 채워나가면 됩니다.




Thread를 많이 사용한다고 좋은 것은 아니다


우리는 이전 강의 “스레드는 종잡을 수 없다”에서 Thread를 이용하여 0에서 10억까지의 수를 더했을 때 값은 얼마인지 구하는 법에 대해 예시를 확인해 봤습니다.

예제 코드
public class Person implements Runnable { public long sum = 0; private final long from; private final long to; public Person(long from, long to) { this.from = from; this.to = to; } public void run() { for (long i = from; i <= to; i++) { if (0 == i % 2) { sum += i; } } } }
copy
public static void main(String[] args) {     try {         long startTime = System.currentTimeMillis();         Person person1 = new Person(0, 500000000);         Thread threadA = new Thread(person1);         Person person2 = new Person(500000001, 1000000000);         Thread threadB = new Thread(person2);         threadA.start();         threadB.start();         threadA.join();         threadB.join();         System.out.println(person1.sum + person2.sum);         long endTime = System.currentTimeMillis();         System.out.println(endTime – startTime);     } catch (InterruptedException e) {} }
copy

Thread를 이용하면 연산이 항상 빠를 것이라고 생각할 수 있지만, 특정 상황에서는 오히려 Single-Thread를 이용하는 것보다 더 느릴 수 있습니다.

우리는 조금 전 Core와 커널 스레드에 대해서 살펴보았으며, Java의 스레드는 커널 스레드를 사용한다고 했었고,
Core라는 자원은 유한하나 Task의 수는 많을 수 있다고 했으며 Task를 처리하는 방법으로 사람으로 따지면 멀티플레이 방법을 얘기했었습니다.

멀티플레이의 단점은 중요하고 빨리 끝내야 하는 일이 다른 일을 하느라 늦게 처리될 수 있으며, 일을 번갈아 가면서 한다는 것은 이전에 했던 일을 기억해야 한다는 점과 집중력이 저하된다고 했었습니다.

이러한 문제는 컴퓨터도 동일합니다. 운영체제에서도 한정된 Core를 이용하여 여러 Kernel Thread를 관리하는 방식은 인간의 멀티플레이와 유사한 부분이 있습니다.

core10

Core의 개수가 하나, 일하고 있는 Kernel Thread A, B로 두 개라고 가정합니다.

  1. Kernel Thread A의 작업을 실행하다가 중간에 Kernel Thread B로 넘어갑니다.
  2. 넘어가기 전에 Kernel Thread A의 작업 내용을 어디서부터 다시 시작해야 하는지 저장하고 Kernel Thread B로 넘어갑니다.
  3. Kernel Thread B의 작업을 실행하다가 Kernerl Thread B의 작업 내용을 어디서부터 다시 시작해야 하는지 저장하고 Kernel Thread A로 넘어갑니다.
  4. Kernel Thread A의 이전에 작업했던 내용을 읽은 후 시작해야 할 부분부터 다시 실행합니다.
  5. 이 과정을 작업이 끝날 때까지 반복합니다.

이렇게 Kernel Thread를 옮겨가며 작업하는 것을 Context Switching이라고 하며 들어가는 비용을 Context Switching Cost라고 합니다.

Thread를 옮겨가는 것만으로 저장, 읽기 연산이 계속 반복되며 들어가는 비용입니다.
그리고 생각보다 이 비용은 굉장히 비쌉니다.

만약에 Single-Core 컴퓨팅 환경에서 Single-Thread 연산이 아닌, Multi-Thread 연산을 하게 되면, Single-Thread 보다 더 큰 비용이 들어갑니다.

그리고 해당 예제 코드에서 발생하는 추가적인 비용은 다음과 같습니다.

  1. Thread 인스턴스 2개 생성 비용
  2. thread.start() 메서드 시점에 Kernel Thread를 생성하는 비용
  3. 연산시 Context Switching Cost

그러므로 간단한 연산은 Single-Thread 환경에서 연산하는 것이 훨씬 낫습니다.

도전자 질문
작성된 질문이 없습니다
이용약관|개인정보취급방침
알유티씨클래스|대표, 개인정보보호책임자 : 이병록
이메일 : cs@codelatte.io|운영시간 09:00 - 18:00(평일)
사업자등록번호 : 824-06-01921|통신판매업신고 : 2021-성남분당C-0740
주소 : 경기도 성남시 분당구 대왕판교로645번길 12, 9층 24호(경기창조혁신센터)