이 게시글은 혼공학습단(혼공컴운) 14기의 2주차 과제를 포함하고 있습니다.
기본 개념부터 시작하기
코어(Core)란?
코어는 CPU 내부에서 실제로 연산을 수행하는 물리적인 처리 단위입니다. 쉽게 말해, 코어는 컴퓨터의 "두뇌"라고 할 수 있습니다. 하나의 코어는 한 번에 하나의 명령어를 실행할 수 있습니다.
과거에는 하나의 CPU에 하나의 코어만 있었지만, 기술이 발전하면서 하나의 CPU 칩 안에 여러 개의 코어를 집적할 수 있게 되었습니다. 이를 멀티 코어 프로세서라고 합니다.
일상생활 비유: 코어를 요리사라고 생각해보세요. 한 명의 요리사(싱글 코어)는 한 번에 하나의 요리만 만들 수 있지만, 여러 명의 요리사(멀티 코어)가 있으면 동시에 여러 요리를 만들 수 있습니다.
스레드(Thread)란?
스레드는 프로그램 내에서 실행되는 작업의 단위입니다. 하나의 프로세스 안에서 여러 개의 스레드가 동시에 실행될 수 있습니다. 스레드는 소프트웨어적인 개념으로, 실제 물리적인 코어에서 실행됩니다.
일상생활 비유: 스레드를 요리 레시피라고 생각해보세요. 한 명의 요리사(코어)가 여러 개의 레시피(스레드)를 번갈아가며 실행할 수 있습니다. 파스타를 끓이는 동안 샐러드를 만들고, 샐러드를 만드는 동안 소스를 준비하는 것처럼 말이죠.
하드웨어 스레드 vs 소프트웨어 스레드
여기서 중요한 구분을 해야 합니다:
하드웨어 스레드 (Hardware Thread)
하드웨어 스레드는 물리적 코어가 동시에 처리할 수 있는 명령어 스트림의 개수입니다. 인텔의 하이퍼스레딩(Hyper-threading) 기술이나 AMD의 SMT(Simultaneous Multithreading) 기술을 통해 하나의 물리적 코어가 두 개의 하드웨어 스레드를 지원할 수 있습니다.
소프트웨어 스레드 (Software Thread)
소프트웨어 스레드는 프로그램에서 생성하는 실행 단위입니다. 운영체제가 이 소프트웨어 스레드들을 하드웨어 스레드에 할당하여 실행합니다.
멀티 코어 시스템의 이해
멀티 코어란?
멀티 코어는 하나의 CPU 칩 안에 여러 개의 물리적 코어가 있는 것을 의미합니다. 현재 대부분의 컴퓨터는 최소 듀얼 코어(2개)부터 시작해서 많게는 수십 개의 코어를 가지고 있습니다.
멀티 코어의 장점:
- 진정한 병렬 처리: 각 코어가 독립적으로 작업을 수행할 수 있어 실제로 동시에 여러 작업이 실행됩니다.
- 전력 효율성: 클럭 속도를 높이는 것보다 코어 수를 늘리는 것이 전력 소비 측면에서 더 효율적입니다.
- 열 관리: 여러 코어가 작업을 분산하므로 열 발생이 분산됩니다.
멀티 코어 시스템에서의 작업 분배
멀티 코어 시스템에서는 운영체제의 스케줄러가 프로세스와 스레드를 각 코어에 할당합니다. 이 과정에서 다음과 같은 요소들이 고려됩니다:
- 부하 분산: 각 코어의 작업량을 균등하게 맞춤
- 캐시 지역성: 데이터 캐시의 효율성을 위해 관련 작업을 같은 코어에 할당
- 스레드 친화성: 특정 스레드를 특정 코어에 고정시키는 기법
멀티 스레드 프로그래밍
멀티 스레드란?
멀티 스레드는 하나의 프로그램 내에서 여러 개의 스레드가 동시에 실행되는 것을 의미합니다. 이는 프로그램의 성능을 향상시키고 사용자 경험을 개선하는 중요한 기법입니다.
멀티 스레드의 장점
응답성 향상: 사용자 인터페이스 스레드가 별도로 실행되므로 프로그램이 멈춘 것처럼 보이지 않습니다.
자원 공유: 같은 프로세스 내의 스레드들은 메모리 공간을 공유하므로 데이터 교환이 효율적입니다.
경제성: 새로운 프로세스를 생성하는 것보다 스레드를 생성하는 것이 시스템 자원을 덜 소모합니다.
멀티 스레드 프로그래밍 예시
// TypeScript에서의 멀티 스레드 예시 (Worker 사용)
class DownloadManager {
private downloadFile(filename: string): Promise<string> {
return new Promise((resolve) => {
console.log(`다운로드 시작: ${filename}`);
// 다운로드 시뮬레이션 - 실제로는 네트워크 요청
setTimeout(() => {
console.log(`다운로드 완료: ${filename}`);
resolve(`${filename} 다운로드 성공`);
}, 2000);
});
}
// 여러 파일을 동시에 다운로드하는 메서드
async downloadMultipleFiles(filenames: string[]): Promise<string[]> {
// Promise.all을 사용하여 모든 다운로드를 동시에 시작
const downloadPromises = filenames.map(filename =>
this.downloadFile(filename)
);
// 모든 다운로드가 완료될 때까지 대기
return await Promise.all(downloadPromises);
}
}
// 사용 예시
async function main() {
const downloader = new DownloadManager();
const files = ['파일1.zip', '파일2.zip', '파일3.zip'];
try {
// 모든 파일을 동시에 다운로드 시작
const results = await downloader.downloadMultipleFiles(files);
console.log('모든 다운로드 완료:', results);
} catch (error) {
console.error('다운로드 중 오류:', error);
}
}
main();
코어와 스레드의 관계
1:1 관계가 아닌 이유
많은 사람들이 착각하는 것 중 하나는 "4코어 CPU는 4개의 스레드만 실행할 수 있다"는 것입니다. 하지만 실제로는 그렇지 않습니다.
운영체제의 스케줄링: 운영체제는 시분할 시스템을 사용하여 매우 짧은 시간 간격으로 스레드들을 교체하면서 실행합니다. 이로 인해 수백 개의 스레드가 동시에 실행되는 것처럼 보입니다.
컨텍스트 스위칭: 스레드 간 전환을 컨텍스트 스위칭이라고 하며, 이 과정에서 CPU 레지스터의 값들이 저장되고 복원됩니다.
최적의 스레드 개수
일반적으로 CPU 집약적인 작업의 경우 "코어 수 = 스레드 수"가 가장 효율적입니다. 하지만 I/O 작업이 많은 경우에는 더 많은 스레드를 사용하는 것이 유리할 수 있습니다.
// 최적 스레드 개수 계산 예시
class ThreadPoolManager {
private readonly availableProcessors: number;
constructor() {
// Node.js에서 사용 가능한 CPU 코어 수 확인
this.availableProcessors = require('os').cpus().length;
}
// CPU 집약적 작업을 위한 스레드 풀 크기
getCpuIntensiveThreadCount(): number {
return this.availableProcessors;
}
// I/O 집약적 작업을 위한 스레드 풀 크기
getIoIntensiveThreadCount(): number {
return this.availableProcessors * 2;
}
// 혼합 작업을 위한 적응형 스레드 풀 크기
getAdaptiveThreadCount(ioRatio: number): number {
return Math.floor(this.availableProcessors * (1 + ioRatio));
}
}
// 사용 예시
const threadManager = new ThreadPoolManager();
console.log(`CPU 집약적 작업 스레드 수: ${threadManager.getCpuIntensiveThreadCount()}`);
console.log(`I/O 집약적 작업 스레드 수: ${threadManager.getIoIntensiveThreadCount()}`);
동시성 vs 병렬성
동시성(Concurrency)
동시성은 여러 작업이 논리적으로 동시에 실행되는 것처럼 보이는 것입니다. 싱글 코어 시스템에서도 시분할을 통해 동시성을 구현할 수 있습니다.
병렬성(Parallelism)
병렬성은 여러 작업이 물리적으로 동시에 실행되는 것입니다. 이는 멀티 코어 시스템에서만 가능합니다.
핵심 차이점: 동시성은 "동시에 다루는 것"이고, 병렬성은 "동시에 실행하는 것"입니다.
실제 개발에서의 고려사항
1. 스레드 안전성 (Thread Safety)
여러 스레드가 공유 자원에 접근할 때 발생할 수 있는 문제를 방지해야 합니다.
// 스레드 안전하지 않은 예시
class UnsafeCounter {
private count: number = 0;
// 여러 스레드가 동시에 실행하면 경쟁 조건(race condition) 발생 가능
increment(): void {
this.count++; // 읽기 -> 증가 -> 쓰기 과정이 원자적이지 않음
}
getCount(): number {
return this.count;
}
}
// 스레드 안전한 예시 (뮤텍스 패턴 사용)
class SafeCounter {
private count: number = 0;
private mutex: boolean = false; // 간단한 뮤텍스 구현
// 동기화를 통해 한 번에 하나의 스레드만 접근 가능
async increment(): Promise<void> {
// 뮤텍스 획득 대기
while (this.mutex) {
await new Promise(resolve => setTimeout(resolve, 1));
}
this.mutex = true; // 뮤텍스 잠금
try {
this.count++; // 임계 구역
} finally {
this.mutex = false; // 뮤텍스 해제
}
}
getCount(): number {
return this.count;
}
}
// 더 현대적인 방법: Promise 기반 큐를 사용한 동기화
class ModernSafeCounter {
private count: number = 0;
private operationQueue: Promise<void> = Promise.resolve();
increment(): Promise<void> {
// 모든 증가 연산을 순차적으로 실행하도록 큐에 추가
this.operationQueue = this.operationQueue.then(() => {
this.count++;
});
return this.operationQueue;
}
getCount(): number {
return this.count;
}
}
2. 데드락 (Deadlock)
두 개 이상의 스레드가 서로 다른 스레드가 점유한 자원을 기다리며 무한정 대기하는 상황입니다.
3. 성능 최적화
CPU 바운드 작업: 코어 수만큼 스레드를 생성하는 것이 최적입니다.
I/O 바운드 작업: 대기 시간이 많으므로 코어 수보다 더 많은 스레드를 사용할 수 있습니다.
최신 동향과 미래 전망
하이퍼스레딩과 SMT
인텔의 하이퍼스레딩과 AMD의 SMT 기술은 하나의 물리적 코어가 두 개의 논리적 코어로 작동하게 합니다. 이를 통해 코어 활용률을 높일 수 있습니다.
이기종 멀티 코어
ARM의 big.LITTLE 아키텍처처럼 성능 코어와 효율 코어를 결합한 설계가 모바일 기기에서 주목받고 있습니다.
비동기 프로그래밍
최근에는 스레드 대신 비동기 프로그래밍 패턴을 사용하는 추세입니다. 이는 적은 스레드로도 높은 동시성을 달성할 수 있게 해줍니다.
// 비동기 프로그래밍 예시
class AsyncTaskManager {
// 여러 비동기 작업을 동시에 처리
async processMultipleTasks(): Promise<void> {
// 각 작업을 비동기적으로 실행
const task1 = this.processData("데이터1");
const task2 = this.processData("데이터2");
const task3 = this.processData("데이터3");
// 모든 작업이 완료될 때까지 대기
const results = await Promise.all([task1, task2, task3]);
console.log('모든 작업 완료:', results);
}
// 개별 비동기 작업
private async processData(data: string): Promise<string> {
return new Promise((resolve) => {
// 비동기 작업 시뮬레이션
setTimeout(() => {
console.log(`${data} 처리 완료`);
resolve(`${data} 결과`);
}, Math.random() * 1000);
});
}
// 순차적 처리 vs 병렬 처리 비교
async sequentialProcessing(items: string[]): Promise<string[]> {
const results: string[] = [];
// 순차적 처리 - 각 작업이 끝날 때까지 다음 작업 대기
for (const item of items) {
const result = await this.processData(item);
results.push(result);
}
return results;
}
async parallelProcessing(items: string[]): Promise<string[]> {
// 병렬 처리 - 모든 작업을 동시에 시작
const promises = items.map(item => this.processData(item));
return await Promise.all(promises);
}
}
// Worker를 사용한 진정한 멀티 스레드 구현
class WorkerManager {
private workerPool: Worker[] = [];
private taskQueue: Array<{
data: any;
resolve: (result: any) => void;
reject: (error: Error) => void;
}> = [];
constructor(workerCount: number) {
// 워커 풀 초기화
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('./worker.js');
worker.onmessage = (event) => {
this.handleWorkerMessage(event);
};
this.workerPool.push(worker);
}
}
// 작업을 워커에게 할당
async executeTask(data: any): Promise<any> {
return new Promise((resolve, reject) => {
const availableWorker = this.workerPool.find(w => !w.busy);
if (availableWorker) {
this.assignTaskToWorker(availableWorker, data, resolve, reject);
} else {
// 사용 가능한 워커가 없으면 큐에 추가
this.taskQueue.push({ data, resolve, reject });
}
});
}
private assignTaskToWorker(
worker: Worker & { busy?: boolean },
data: any,
resolve: (result: any) => void,
reject: (error: Error) => void
): void {
worker.busy = true;
worker.postMessage(data);
// 워커 응답 처리 (실제 구현에서는 더 복잡한 메시지 매칭 필요)
worker.onmessage = (event) => {
worker.busy = false;
resolve(event.data);
// 큐에 대기 중인 작업이 있으면 할당
if (this.taskQueue.length > 0) {
const nextTask = this.taskQueue.shift()!;
this.assignTaskToWorker(worker, nextTask.data, nextTask.resolve, nextTask.reject);
}
};
}
private handleWorkerMessage(event: MessageEvent): void {
// 워커로부터 메시지 처리
console.log('워커에서 결과 수신:', event.data);
}
}
2주차 숙제
'혼공학습단 > 혼공컴운' 카테고리의 다른 글
[혼공컴운] 스택(Stack)과 큐(Queue) (0) | 2025.07.06 |
---|