스레드(Thread)의 개념
- 스레드는 CPU 사용의 기본 단위이며, 실행 컨텍스트(프로그램 카운터, 레지스터 집합, 스택)를 가진다.
- 동일한 프로세스에 속한 스레드들은 코드 영역, 데이터 영역, 열린 파일 등 대부분의 자원을 공유한다.
다중 스레드(Multithreading)의 장점
응답성(Responsiveness) | 하나의 스레드가 블로킹되더라도 다른 스레드가 계속 실행되어 사용자 응답성을 유지할 수 있다. |
자원 공유(Resource Sharing) | 스레드 간 데이터 공유가 용이하며, 같은 프로세스 자원을 공유하므로 통신 비용이 적다. |
경제성(Economy) | 프로세스 생성보다 스레드 생성/전환이 비용이 적게 든다. (문맥 교환 시 공유 메모리 사용 가능) |
확장성(Scalability) | 멀티코어 시스템에서 병렬 실행을 통해 성능을 확장할 수 있다. |
병행성과 병렬성
병행성(Concurrency) | 여러 스레드가 교대로 진행되는 것 (단일 CPU에서 가능) |
병렬성(Parallelism) | 여러 스레드가 실제로 동시에 실행되는 것 (멀티코어 필요) |
병행성 샘플 코드
단일 스레드 풀(1개)에서 작업을 교대로 진행 (병렬 아님)
포인트: 스레드가 1개뿐인 ExecutorService에서 여러 작업을 제출 → 실제로는 한 번에 하나씩 실행(병행성 o, 병렬성 x).
// ConcurrencyOnly.java
import java.util.concurrent.*;
import java.time.Instant;
public class ConcurrencyOnly {
static Runnable ioLike(String label, long millis) {
return () -> {
Instant s = Instant.now();
try { Thread.sleep(millis); } catch (InterruptedException ignored) {}
Instant e = Instant.now();
System.out.printf("%s done: %dms%n", label, (e.toEpochMilli() - s.toEpochMilli()));
};
}
public static void main(String[] args) throws Exception {
ExecutorService one = Executors.newSingleThreadExecutor(); // 스레드 1개
long t0 = System.currentTimeMillis();
Future<?> f1 = one.submit(ioLike("A", 800));
Future<?> f2 = one.submit(ioLike("B", 800));
f1.get(); f2.get();
long t1 = System.currentTimeMillis();
System.out.printf("ALL done in %dms%n", (t1 - t0)); // 대략 1600ms 근처
one.shutdown();
}
}
병렬성 샘플코드
availableProcessors() 개수만큼 스레드를 만들어 CPU 바운드 작업(예: 소수 카운트)을 병렬로 수행.
CPU 바운드 작업
- 바운드 작업: 작업의 성격이 특정 자원에 묶여있다는 뜻
- CPU 바운드 작업: 연산이 거의 전부 CPU 계산 능력에 의해 결정되는 작업
// ParallelCpu.java
import java.util.*;
import java.util.concurrent.*;
import java.time.Instant;
public class ParallelCpu {
static boolean isPrime(long n) {
if (n < 2) return false;
for (long i = 2; i * i <= n; i++) if (n % i == 0) return false;
return true;
}
static long countPrimes(long limit) {
long c = 0;
for (long i = 2; i <= limit; i++) if (isPrime(i)) c++;
return c;
}
public static void main(String[] args) throws Exception {
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newFixedThreadPool(cores);
System.out.println("CPU cores: " + cores);
long limit = 200_000;
Callable<Long> job = () -> countPrimes(limit);
long t0 = System.currentTimeMillis();
List<Future<Long>> futures = pool.invokeAll(Arrays.asList(job, job)); // 두 개 작업 병렬
for (Future<Long> f : futures) f.get();
long t1 = System.currentTimeMillis();
System.out.printf("Parallel (pool=%d) done in %dms%n", cores, (t1 - t0));
pool.shutdown();
}
}
다중 스레드 프로그래밍의 도전 과제
작업 분할(Task Division) | 프로그램을 어떻게 스레드 단위로 나눌지 결정 |
부하 균형(Load Balancing) | 모든 스레드가 균형 있게 실행되도록 조정 |
데이터 분할 및 종속성 관리 | 스레드 간 공유 데이터 접근에서 동기화 필요 |
테스트 및 디버깅 어려움 | 동시성 오류(race condition, deadlock 등) 재현이 어려움 |
병렬 처리 모델
데이터 병렬 처리(Data Parallelism) | 데이터를 여러 코어에 분할하고 같은 연산을 병렬 실행 |
작업 병렬 처리(Task Parallelism) | 서로 다른 연산(작업)을 여러 코어에서 실행 |
스레드 모델
- 사용자 수준 스레드(User-level Thread): 응용 프로그램이 직접 생성/관리. 커널은 인식하지 못함.
- 커널 수준 스레드(Kernel-level Thread): 운영체제가 직접 생성/관리. CPU에서 실행됨.
- 매핑 모델
- 다대일(Many-to-One): 여러 사용자 스레드를 하나의 커널 스레드에 매핑. (병렬 실행 불가)
- 일대일(One-to-One): 하나의 사용자 스레드를 하나의 커널 스레드에 매핑. (병렬 실행 가능, 오버헤드 큼)
- 다대다(Many-to-Many): 여러 사용자 스레드를 여러 커널 스레드에 매핑. (유연성 높음)
암묵적 스레딩(Implicit Threading)
- 프로그래머가 직접 스레드를 생성하지 않고, 언어나 프레임워크가 자동으로 관리
- 기법 예시:
- 스레드 풀(Thread Pool)
- Fork-Join 프레임워크
- Grand Central Dispatch(GCD, macOS/iOS)
스레드 종료 방법
- 비동기 취소(Asynchronous Cancellation): 즉시 강제 종료 (데이터 불일치 위험).
- 지연 취소(Deferred Cancellation): 스레드가 스스로 안전한 시점에 종료. (일반적으로 선호).
Linux의 스레드 처리
- Linux는 프로세스와 스레드를 동일하게 "태스크(Task)"로 취급.
- clone() 시스템 콜을 이용해 프로세스와 유사한 태스크 또는 스레드와 유사한 태스크를 생성할 수 있음.