스레드(Thread)
어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다.
일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 방식을 멀티 스레드(MultiThread) 라고 한다.
프로세스 vs 스레드
멀티프로세스와 멀티스레드는 양쪽 모두 여러 흐름이 동시에 진행된다는 공통점을 가지고 있다. 하지만 멀티프로세스에서 각 프로세스는 독립적으로 실행되며 각각 별개의 메모리를 차지하고 있는 것과 달리 멀티스레드는 프로세스 내의 메모리를 공유해 사용할 수 있다. 또한 프로세스 간의 전환 속도가 빠르다.
멀티스레드 단점
각각의 스레드 중 어떤 것이 먼저 실행될지 그 순서를 알 수 없다.
예를 들어,
두 스레드가 특정 공유 변수 i 의 값을 1 증가 시키는 명령을 실행 할 때, 다음과 같은 방식으로 수행될 수 있다.
1. 공유되는 변수 i 의 값을 레지스터에 저장
2. 레지스터의 값을 1 증가 시킴
3. 변수 i 에 그 값을 저장
이때 두 스레드가 실행될 때 어떤 스레드가 먼저 실행될지는 보장되지 않으며, 만약 다음과 같은 순서로 실행된다면
스레드 | 동작 | i 의 값 | 스레드 1 의 레지스터 | 스레드 2 의 레지스터 |
스레드 1 | i 의 값을 레지스터에 저장 | 0 | 0 | |
스레드 1 | 레지스터 값을 1 증가 | 0 | 1 | |
스레드 1 | i 에 값 저장 | 1 | 1 | |
스레드 2 | i 의 값을 레지스터에 저장 | 1 | 1 | 1 |
스레드 2 | 레지스터 값을 1 증가 | 1 | 1 | 2 |
스레드 2 | i 에 값 저장 | 2 | 1 | 2 |
최종 결과로 i 는 2가 증가된다. 하지만 다음과 같이 실행된다면
스레드 | 동작 | i 의 값 | 스레드 1 의 레지스터 | 스레드 2 의 레지스터 |
스레드 1 | i 의 값을 레지스터에 저장 | 0 | 0 | |
스레드 2 | i 의 값을 레지스터에 저장 | 0 | 0 | 0 |
스레드 1 | 레지스터 값을 1 증가 | 1 | 1 | 0 |
스레드 2 | 레지스터 값을 1 증가 | 1 | 1 | 1 |
스레드 1 | i 에 값 저장 | 1 | 1 | 1 |
스레드 2 | i 에 값 저장 | 1 | 1 | 1 |
최종 결과로 i 는 1이 증가되고, 이것은 원래 프로그램의 의도(각각의 스레드가 i를 1씩 증가하는 동작)와 다를 수 있다.
또한, 이러한 문제는 스레드의 실행 조건에 따라 결과가 다르게 나오므로, 오류가 발생했을 때 원인을 찾기가 힘들다.
이러한 문제를 경쟁 조건이라고 하며, 문제를 막기 위해 세마포어와 같은 방법을 통해 공유 데이터에 접근하는 스레드의 개수를 한개 이하로 유지하는 방법을 사용할 수 있다.
경쟁 조건 이란
Race Condition, 여러 스레드가 동시에 고유 자원에 접근 할 때, 실행 순서에 따라 결과가 달라지는 문제
세마포어 란
공유 자원에 동시에 접근할 수 있는 스레드 수를 제한하는 동기화 기법
실행될 수 있는 스레드 개수는 개발자가 직접 정할 수 있다.
하지만 이 스레드들이 어떤 자원에 동시에 접근하는지는 다로 제어하지 않는다.
예를 들어,
10개의 스레드를 만들었는데, 그 중 동시에 DB 연결을 가져가는 스레드는 3개까지만 허용하고 싶을 때
동기화로 제어하면 된다.
Semaphore semaphore = new Semaphore(3); // 동시에 3개까지만 허용
Runnable task = () -> {
try {
semaphore.acquire(); // 허락 받기
// 공유 자원 접근 (예: DB)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 사용 후 반납
}
};
그럼에도 멀티스레드를 사용하는 이유
멀티 스레드를 사용하지 않으면,
- 병렬 처리가 불가능해진다. → 느려짐
- UI 프로그램이 멈춰버림(응답 없음)
- 웹 서버에서 사용자 요청을 동시에 처리할 수 없음
- IO 작업 중 낭비가 큼
스레드 종류
사용자 레벨 스레드 (User-Level Thread)
사용자 스레드는 커널 영역의 상위에서 지원되며 일반적으로 사용자 레벨의 라이브러리를 통해 구현되며, 라이브러리는 스레드의 생성 및 스케줄링 등에 관한 관리 기능을 제공한다.
동일한 메모리 영역에서 스레드가 생성 및 관리되므로 속도가 빠른 장점이 있는 반면,
여러 개의 사용자 스레드 중 하나의 스레드가 시스템 호출 등으로 중단되면 나머지 모든 스레드 역시 중단되는 단점이 있다. 이는 커널이 프로세스 내부의 스레드를 인식하지 못하며 해당 프로세스를 대기 상태로 전환시키기 때문이다.
커널 레벨 스레드 (Kernel-Level Thread)
커널 스레드는 운영체제가 지원하는 스레드 기능으로 구현되며, 커널이 스레드의 생성 및 스케줄링 등을 관리한다.
스레드가 시스템 호출 등으로 중단되더라도, 커널은 프로세스 내의 다른 스레드를 중단시키지 않고 계속 실행시켜준다.
다중처리기 환경에서 커널은 여러 개의 스레드를 각각 다른 처리기에 할당할 수 있다. 다만, 사용자 스레드에 비해 생성 및 관리하는 것이 느리다.
스레드 데이터
스레드 기본 데이터
스레드도 프로세스와 마찬가지로 하나의 실행 흐름이므로 실행과 관련된 데이터가 필요하다.
일반적으로 스레드는 자신만의 고유한 스레드 ID, 프로그램 카운터, 레지스터 집합, 스택을 가진다. 코드, 데이터, 파일 등 기타 자원은 프로세스 내의 다른 스레드와 공유한다.
스레드 특정 데이터
위의 기본 데이터 외에도 하나의 스레드에만 연관된 데이터가 필요한 경우가 있는데, 이런 데이터를 스레드 특정 데이터(Thread-Specific Data, 줄여서 TSD)라고 한다.
멀티스레드 프로그래밍 환경에서 모든 스레드는 프로세스의 데이터를 공유하고 있지만, 특별한 경우에는 개별 스레드만의 자료 공간이 필요하다.
예를 들어,
여러 개의 트랜잭션을 스레드로 처리할 경우, 각각의 트랜잭션 ID를 기억하고 있어야 하는데, 이때 TSD가 필요하다. TSD는 여러 스레드 라이브러리들이 지원하는 기능 중의 하나이다.
스레드를 만드는 주요 환경들
- 병렬 작업 처리 (CPU 집약적인 작업)
- 이미지 처리, 데이터 분석, 계산 등의 CPU를 많이 사용하는 작업
- 비동기 작업 처리(I/O 작업 등)
- 파일 읽기/쓰기, 데이터베이스 쿼리, 네트워크 요청 등 I/O 작업
- UI 응답성 유지 (UI 프로그램에서 백그라운드 작업 처리)
- 사용자 인터페이스(UI)에서 시간이 많이 걸리는 작업을 수행할 때, UI가 멈추지 않도록 백그라운드에서 스레드를 실행
- 웹 서버에서 동시 처리(HTTP 요청 처리)
- 웹 서버가 여러 클라이언트의 요청을 동시에 처리할 때
예시
public class FileDownloadTask implements Runnable {
@Override
public void run() {
System.out.println("파일 다운로드 시작");
// 비동기 파일 다운로드 작업
try {
Thread.sleep(3000); // 다운로드 시뮬레이션
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("파일 다운로드 완료");
}
public static void main(String[] args) {
Thread thread = new Thread(new FileDownloadTask());
thread.start();
}
}
대부분의 경우에는 Spring이 스레드 관리를 대신하기 때문에 Spring 환경에서는 거의 스레드를 직접 선언할 일이 거의 없다.
하지만 병렬 처리가 성능에 큰 영향을 주거나 비동기 로직을 커스터마이징 할 때, 외부 API 요청을 병렬로 보낼 때, 동시성 상황을 시뮬레이션 할 때, 배치 처리, 크롤링, 마이크로 서비스 간 통신 등에서는 사용하고 있다.
⌗ 동시성 상황 시뮬레이션 ⇢ CountDownLatch와 ExecutorService를 사용해서 동시에 실행되게 만드는 테스트 코드
스레드 관리 방법
직접 생성
Thread 클래스를 상속하거나 Runnable 인터페이스를 구현해서 스레드를 직접 만든다.
스레드 풀
ExecutorService 를 사용하여 스레드를 관리하는 방식. 효율적이고, 자원 관리를 용이하게 해준다.
ExecutorService executorService = Executors.newFixedThreadPool(4); // 4개의 스레드 풀
executorService.submit(() -> {
// 작업 실행
});
프로세스 관리의 변화
멀티스레드 환경이 확산됨에 따라 전통적인 프로세스 관리 방식에도 변화가 필요해졌다. 예를 들어, fork 또는 exec와 같은 시스템 호출시에 어떻게 처리할 것인가 하는 문제가 대두된 것이다.
fork 문제
어떤 프로세스 내의 스레드가 fork를 호출하면 모든 스레드를 가진 프로세스를 생성할 것인지, 아니면 fork를 요청한 스레드만 가진 프로세스를 생성할 것인지 하는 문제이다. 유닉스에서는 각각 2가지 버전의 fork를 지원하고 있다.
exec 문제
fork를 통해 모든 스레드를 복제하고 난 후, exec를 수행한다면 모든 스레드들이 초기화된다. 그렇다면 교체될 스레드를 복제하는 작업은 필요가 없기 때문에 애초에 fork를 요청한 스레드만을 복제했어야 한다. 한편, fork를 한 후에 exec를 수행하지 않는다면 모든 스레드를 복제할 필요가 있는 경우도 있다
동기화 (Synchronization)
여러 스레드가 동시에 공유 자원에 접근할 때, 데이터의 일관성과 안정성을 유지하기 위해 접근을 제어하는 것
멀티스레드 환경에서는 두 개 이상의 스레드가 동시에 같은 데이터(공유 자원)를 수정할 수 있다.
이럴 경우, 예측하지 못한 결과가 발생할 수 있다.
따라서 한 번에 한 스레드만 접근하게 막도록 제어가 필요하다. 이를 동기화 라고 한다.
싱글 스레드 vs 멀티 스레드
싱글 스레드 | 멀티 스레드 | |
장점 | 동기화 필요 ✕, 코드 단순, 안정적 | 병렬 처리 가능, 속도 빠름 |
단점 | 병렬 처리 불가, 느린 작업이 전체 흐름을 멈춤 | 코드 복잡, 관리 필요 |
예시 | 사용자 100명이 동시 접속 → 1명씩 차례대로 처리 99명 대기 |
사용자 100명 동시 접속 → 동시에 처리 단, 공유 자원만 조심 |
참고 문서
위키백과 - 스레드(컴퓨팅)
스레드 (컴퓨팅) - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 두 개의 스레드를 실행하고 있는 하나의 프로세스. 스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으
ko.wikipedia.org
'개발 지식 > 기본지식' 카테고리의 다른 글
메모리 관리란 (2) | 2025.05.15 |
---|---|
터미널 기본 명령어 / 실습 (0) | 2025.05.14 |
프로세스 관리 (0) | 2025.05.08 |
운영체제(OS)의 일반적인 작동 원리 (0) | 2025.05.02 |
터미널이란 (0) | 2025.05.02 |