본문 바로가기

스레드의 개념

프로세스 안의 실행 흐름인 스레드가 무엇을 함께 쓰고 무엇을 따로 가지는지 구분하고, 스레드를 쓰면 얻는 이점과 암달의 법칙이 정하는 한계를 계산해 봅니다. 동시성과 병렬성의 차이, 스레드 방식과 이벤트 루프 방식의 서버, 공유 메모리가 만드는 위험을 정리합니다.

워드프로세서로 긴 문서를 편집한다고 생각해 봅시다. 사용자가 글자를 입력하는 동안 프로그램은 화면을 다시 그리고, 맞춤법을 검사하고, 몇 분마다 자동으로 저장합니다. 이 일들을 프로세스 하나가 한 줄로 차례차례 처리한다면, 자동 저장이 디스크를 기다리는 동안 화면이 멈추고 입력도 받지 못할 것입니다. 그렇다고 일마다 프로세스를 따로 만들면, 3장에서 본 것처럼 프로세스끼리는 메모리를 공유하지 않으므로 편집 중인 문서의 내용을 IPC로 계속 주고받아야 합니다.

스레드(thread)는 이 둘 사이의 방법입니다. 프로세스가 실행 중인 프로그램이라면, 스레드는 그 프로세스 안에서 명령을 차례로 실행해 나가는 하나의 흐름입니다. 프로세스 하나는 스레드를 여러 개 가질 수 있고, 같은 프로세스의 스레드들은 메모리를 함께 쓰면서 각자 따로 실행됩니다. 이 글에서는 스레드가 무엇을 함께 쓰고 무엇을 따로 가지는지, 스레드를 쓰면 무엇이 좋아지고 어디에 한계가 있는지 살펴봅니다.

함께 쓰는 것과 따로 가지는 것

같은 프로세스의 스레드들이 무엇을 공유하고 무엇을 따로 가지는지 정확히 아는 것이 여러 스레드로 프로그램을 짜는 일의 출발점입니다.

스레드 · 공유 자원과 실행 문맥

주소 공간은 함께 쓰고, 실행을 재개할 문맥은 따로 갖습니다

같은 프로세스의 스레드 A와 B를 비교합니다. 스레드별 스택은 실행 저장소이며 다른 스레드가 접근할 수 없는 보호 영역이라는 뜻은 아닙니다.

같은 주소 공간 안의 공유 저장소와 두 스택주소 공간 안에 공유 코드 데이터 힙과 별도 A B 스택이 있다. 두 실행 문맥은 각자 스택 및 같은 공유 저장소를 사용한다. 보호 경계나 동시에 실행되는 시간선을 뜻하지 않는다. 한 프로세스의 주소 공간 공유 코드 · 전역 데이터 · 힙 수명과 동기화 규칙 필요 A의 호출 스택 B의 호출 스택 스레드 A 실행 문맥 PC · 레지스터 · TLS 상태 스레드 B 실행 문맥 PC · 레지스터 · TLS 상태 화살표: 실행 흐름이 해당 저장소를 사용함 스택 분리는 접근을 금지하는 보호 경계가 아님
공유 영역과 두 스레드의 저장소 사용데스크톱과 같은 공유 코드 데이터 힙 및 A B 스택과 별도 실행 문맥을 세로 배치한다. 네 화살표의 저장소 사용 의미가 같다. 같은 주소 공간 공유 코드 전역 데이터 · 힙 A 호출 스택 B 호출 스택 A 실행 문맥 PC · 레지스터 TLS 상태 B 실행 문맥 PC · 레지스터 TLS 상태 화살표: 저장소 사용 접근 보호·실행 순서 아님

프로세스의 같은 주소 공간 · 공유 자원

코드·전역 데이터·힙과 열린 파일 디스크립터, 등록한 시그널 핸들러를 함께 씁니다.

같은 데이터를 변경할 때는 언어와 API의 동기화 규칙을 지켜야 합니다.

스레드 A · 별도 실행 상태

스레드 ID / PC / 레지스터

호출 스택 / TLS / 시그널 마스크

스레드 B · 별도 실행 상태

스레드 ID / PC / 레지스터

호출 스택 / TLS / 시그널 마스크

다른 스레드에 스택 값의 주소를 넘기면 수명·접근 규칙·동기화가 필요합니다. 같은 시점의 CPU 병렬 실행 폭은 사용 가능한 논리 CPU와 런타임 제약에 따릅니다.

스레드들은 프로세스의 주소 공간 하나를 함께 씁니다. 그래서 코드 영역의 함수, 데이터 영역의 전역 변수, 힙에 할당한 객체를 모든 스레드가 같은 주소로 접근합니다. 열린 파일 디스크립터와 신호 처리 함수, 현재 작업 디렉터리도 프로세스 단위로 관리되므로 함께 씁니다. 한 스레드가 연 파일을 다른 스레드가 바로 읽을 수 있고, 한 스레드가 고친 전역 변수를 다른 스레드가 볼 수 있습니다.

반면 실행을 이어 가는 데 필요한 상태는 스레드마다 따로 있습니다. 각 스레드는 서로 다른 코드를 실행하고 있을 수 있으므로 자기의 프로그램 카운터와 레지스터 값을 가지고, 컨텍스트 스위칭 때 이 값들이 스레드 단위로 저장되고 복원됩니다. 함수 호출의 흐름도 스레드마다 다르므로 스택도 따로 있습니다. 스레드 A가 f()에서 g()를 부르는 동안 스레드 B는 x()에서 y()를 부를 수 있고, 각 호출의 지역 변수와 돌아갈 주소는 각자의 스택에 쌓입니다. 이 밖에 스레드를 구분하는 스레드 ID, 어떤 신호를 잠시 막을지 정하는 신호 마스크, C의 errno처럼 스레드마다 따로 두는 스레드 지역 저장소(thread-local storage, TLS)도 스레드마다 있습니다.

함께 쓰는 것스레드마다 따로 가지는 것
코드, 전역 변수, 힙프로그램 카운터와 레지스터
열린 파일 디스크립터스택(지역 변수, 돌아갈 주소)
신호 처리 함수신호 마스크
주소 공간과 페이지 테이블스레드 ID, 스레드 지역 저장소

다음 Python 코드에서 이 차이를 확인할 수 있습니다. 스레드 넷이 각자 제곱을 계산해 하나의 리스트에 넣습니다.

import threading

squares = []             # 모든 스레드가 함께 보는 리스트

def work(n):
    result = n * n       # 지역 변수는 스레드마다 따로 있다
    squares.append(result)

threads = [threading.Thread(target=work, args=(n,)) for n in range(1, 5)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(sorted(squares))
[1, 4, 9, 16]

각 스레드의 result는 자기 스택에만 있으므로 서로 섞이지 않고, squares는 하나뿐이므로 네 스레드의 결과가 모두 한 리스트에 모입니다. 3장에서 자식 프로세스가 바꾼 전역 변수가 부모에게 보이지 않았던 것과 대조됩니다. 스레드가 끝나는 순서는 실행할 때마다 다를 수 있으므로, 결과를 정렬해서 출력했습니다.

여기서 하나 오해하기 쉬운 점이 있습니다. 스택이 스레드마다 따로 있다고 해서 다른 스레드가 접근할 수 없는 보호된 영역은 아닙니다. 모든 스택은 같은 주소 공간에 있으므로, 스레드 A가 자기 지역 변수의 주소를 B에게 넘기면 B도 그 주소로 값을 읽고 쓸 수 있습니다. 이때는 두 스레드가 같은 값을 함께 쓰는 것이므로 동기화가 필요하고, A의 함수가 반환되어 그 지역 변수가 사라진 뒤에 B가 접근하면 이미 다른 용도로 쓰이는 메모리를 건드리게 됩니다. 지역 변수라는 이름이 보호나 동기화를 뜻하지는 않습니다.

더 깊이: Linux에서 스레드는 무엇인가

Linux 커널은 프로세스와 스레드를 따로 구분하지 않고 모두 태스크(task)로 다룹니다. 새 태스크를 만드는 clone() 시스템 콜에 무엇을 공유할지 플래그로 알려 주는데, 주소 공간(CLONE_VM), 파일 디스크립터 표(CLONE_FILES), 신호 처리 설정(CLONE_SIGHAND) 등을 공유하도록 만들면 그것이 스레드가 됩니다. 아무것도 공유하지 않고 복사하면 fork()로 만든 프로세스와 같아집니다.

그래서 Linux에서는 스레드마다 커널이 붙인 고유 번호(TID)가 있고, 우리가 PID라고 부르는 값은 사실 스레드 묶음의 번호(thread group ID)입니다. 프로세스의 첫 스레드는 TID가 PID와 같습니다. 한 프로세스의 스레드 목록은 /proc/<PID>/task/ 디렉터리나 ps -L -p <PID>로 볼 수 있습니다.

스레드를 쓰면 좋아지는 것

스레드를 쓰는 첫째 이유는 응답성(responsiveness)입니다. 화면을 그리고 입력을 받는 UI 스레드와 파일을 내려받는 작업 스레드를 나누면, 내려받기가 네트워크를 기다리는 동안에도 UI 스레드는 버튼 클릭에 반응할 수 있습니다. 반대로 UI 스레드에서 블로킹 네트워크 호출을 하면 응답이 올 때까지 화면이 멈춥니다. Android는 이 실수를 막으려고, 일정 SDK 버전 이상을 대상으로 하는 앱이 메인 스레드에서 네트워크 작업을 하면 NetworkOnMainThreadException을 던집니다. 다만 응답성을 지키는 방법이 스레드만은 아니어서, 뒤에서 볼 비동기 입출력으로 하나의 스레드가 기다리지 않고 다른 일을 하게 만들 수도 있습니다.

둘째는 자원 공유입니다. 스레드들은 메모리를 함께 쓰므로, 파이프나 소켓을 만들지 않고도 같은 자료구조를 바로 읽고 씁니다. 큰 데이터를 주고받을 때 복사할 필요도 없습니다. 이 편리함은 뒤에서 볼 위험과 맞닿아 있습니다.

셋째는 경제성입니다. 스레드를 만들 때는 주소 공간과 파일 표를 새로 만들 필요 없이 기존 것을 함께 쓰므로, 프로세스를 만드는 것보다 준비할 것이 적습니다. 같은 프로세스의 스레드 사이를 전환할 때도 앞 장에서 본 것처럼 페이지 테이블을 바꿀 필요가 없습니다. 그래도 스레드마다 스택과 스레드 지역 저장소, 커널의 태스크 구조체가 필요하므로 공짜는 아니며, 두 방식의 비용 차이가 얼마인지는 운영체제와 작업에 따라 측정해 봐야 합니다.

넷째는 여러 CPU 코어를 쓸 수 있다는 것입니다. 스레드 하나는 한 순간에 논리 CPU 하나에서만 실행되지만, 스레드가 넷이고 논리 CPU가 넷이면 네 스레드가 정말로 같은 순간에 실행될 수 있습니다. 계산을 네 조각으로 나누어 각 스레드에 맡기면, 이상적인 경우 네 배 빨리 끝납니다.

코어를 늘려도 빨라지지 않는 부분: 암달의 법칙

그런데 실제로는 코어를 네 배로 늘려도 네 배 빨라지는 경우가 드뭅니다. 거의 모든 프로그램에는 나누어 실행할 수 없는 순차적인 부분이 있기 때문입니다. 순차 실행에 100초 걸리는 작업 가운데 25초는 나눌 수 없고 75초만 나눌 수 있다고 합시다. CPU 4개로 나눌 수 있는 부분을 똑같이 나누면 75 ÷ 4 = 18.75초가 걸리고, 여기에 나눌 수 없는 25초를 더해 모두 43.75초가 걸립니다. 가속비는 100 ÷ 43.75 ≈ 2.29배입니다. CPU를 아무리 늘려도 25초는 그대로 남으므로, 가속비는 100 ÷ 25 = 4배를 넘을 수 없습니다.

이 관계를 일반화한 것이 암달의 법칙(Amdahl's law)입니다. 순차 부분의 비율을 ss, CPU 수를 nn이라고 하면 이상적인 가속비는 1s+(1−s)/n\frac{1}{s + (1 - s)/n}이고, nn이 아무리 커져도 1s\frac{1}{s}를 넘지 못합니다. 다음 코드는 순차 부분이 25%일 때 CPU 수에 따라 가속비가 어떻게 바뀌는지 계산합니다.

def speedup(serial, cpus):
    """순차 부분의 비율이 serial일 때 CPU cpus개로 얻는 이상적인 가속비"""
    return 1 / (serial + (1 - serial) / cpus)

for cpus in [1, 2, 4, 8, 16, 1_000_000]:
    print(f"CPU {cpus:>9,}개: {speedup(0.25, cpus):.2f}배")
CPU         1개: 1.00배
CPU         2개: 1.60배
CPU         4개: 2.29배
CPU         8개: 2.91배
CPU        16개: 3.37배
CPU 1,000,000개: 4.00배

CPU를 2개에서 4개로 늘리면 0.69배가 더 빨라지지만, 8개에서 16개로 늘리면 0.46배만 더 빨라지고, 백만 개를 써도 4배에 머뭅니다. CPU를 늘릴수록 나눌 수 있는 부분의 시간은 줄어들지만 순차 부분의 시간은 그대로이기 때문입니다. 이 계산은 스레드 사이의 통신, 잠금 경쟁, 작업이 고르게 나뉘지 않는 문제를 모두 무시한 것이므로, 실제 가속비는 이보다 낮습니다. 스레드를 늘리기 전에 순차 부분이 얼마인지부터 확인해야 하는 이유입니다.

서버가 동시에 여러 요청을 처리하는 방법

웹 서버를 예로 들어 세 가지 방식을 비교해 보겠습니다. 요청마다 데이터베이스 응답을 500ms 기다려야 한다고 가정합니다.

가장 단순한 서버는 스레드 하나로 요청을 하나씩 처리합니다. 앞 요청이 데이터베이스를 500ms 기다리는 동안 뒤 요청은 모두 줄을 서서 기다리므로, 다른 비용을 무시해도 1초에 2개밖에 처리하지 못합니다. 이 서버의 CPU는 대부분의 시간 동안 아무 일도 하지 않습니다.

스레드 방식의 서버는 요청마다 스레드를 하나씩 맡깁니다. Apache httpd의 worker나 event 방식이 그 예입니다. 한 스레드가 데이터베이스를 기다리며 잠들어 있는 동안 CPU는 다른 스레드의 요청을 처리하므로, 스레드가 100개라면 데이터베이스가 감당하는 한 1초에 최대 200개까지 처리할 수 있습니다. 대신 동시 요청 수만큼 스레드가 필요하고, 스레드마다 스택 메모리와 전환 비용이 듭니다.

이벤트 루프(event loop) 방식의 서버는 스레드 하나로도 많은 연결을 처리합니다. 입출력을 요청한 뒤 결과를 기다리며 멈추지 않고, "끝나면 이 함수를 불러 달라"고 등록해 둔 채 다른 요청을 처리하다가, 입출력이 끝났다는 알림이 오면 등록한 함수를 실행합니다. Node.js가 이 방식이며, 이벤트 루프의 바탕이 되는 입출력 다중화는 네트워크 교재의 I/O 멀티플렉싱에서 다룹니다. 정리하면, 스레드 방식은 여러 스레드가 각자 블로킹 입출력을 하며 기다리고, 이벤트 루프 방식은 하나의 스레드가 논블로킹 입출력으로 여러 작업을 번갈아 진행합니다.

실제 서버는 이 방식들을 섞어 씁니다. Nginx는 마스터 프로세스와 여러 워커 프로세스로 이루어지고, 각 워커가 이벤트 루프로 많은 연결을 처리하며, 필요하면 파일 입출력을 위한 스레드 풀을 따로 둡니다. Node.js도 JavaScript를 실행하는 이벤트 루프는 하나지만, 파일 입출력이나 일부 DNS 조회는 libuv의 스레드 풀이 처리하고, 계산을 병렬로 실행하려면 worker_threads로 스레드를 따로 만듭니다.

동시성과 병렬성

여기서 자주 섞여 쓰이는 두 용어를 구분해 두겠습니다. 동시성(concurrency)은 여러 작업이 진행 중인 상태로 함께 존재하는 것입니다. CPU가 하나뿐이어도 운영체제가 짧은 간격으로 작업을 번갈아 실행하면 여러 작업이 함께 진행되며, 사람이 보기에는 동시에 실행되는 것처럼 보입니다. 병렬성(parallelism)은 여러 작업이 정말로 같은 순간에 실행되는 것으로, CPU가 여러 개 있어야 가능합니다.

한 사람이 요리하다가 빨래를 돌리고 다시 요리로 돌아오면 동시성이고, 두 사람이 한 사람은 요리를, 다른 사람은 빨래를 하면 병렬성입니다. 다만 이 비유에서 빨래는 세탁기가 돌리는 동안 사람이 할 일이 없다는 점에서, 요리와 빨래가 한 사람만으로도 일부 겹칠 수 있습니다. 컴퓨터에서도 CPU가 계산하는 동안 장치가 입출력을 진행할 수 있으며, 이것은 CPU 병렬성과는 다른 축의 겹침입니다.

동시성은 프로그램을 여러 작업으로 나누어 짜는 구조의 문제이고, 병렬성은 그 작업들을 실제로 같은 순간에 실행하는 실행의 문제입니다. 동시성 있게 짠 프로그램을 CPU가 여러 개인 컴퓨터에서 실행하면 병렬성도 얻을 수 있지만, CPU가 하나라면 작업들은 번갈아 실행될 뿐입니다.

메모리를 함께 쓰는 데 따르는 위험

스레드가 메모리를 함께 쓴다는 장점은 그대로 위험이 되기도 합니다. 자세한 내용은 6장과 7장에서 다루고, 여기서는 어떤 문제가 있는지만 짚어 둡니다.

첫째는 경쟁 조건(race condition)입니다. count++는 한 줄이지만 실제로는 값을 읽고, 1을 더하고, 다시 쓰는 세 단계입니다. 두 스레드가 동시에 이 일을 하면 둘 다 같은 옛 값을 읽어 한 번의 증가가 사라질 수 있습니다. C와 C++에서는 동기화 없이 여러 스레드가 같은 변수에 접근해 그중 하나라도 쓰면 정의되지 않은 동작(undefined behavior)이 되므로, 증가 하나가 사라지는 정도로 끝난다고 기대할 수도 없습니다.

둘째는 교착 상태(deadlock)입니다. 스레드 A가 자원 1을 쥐고 자원 2를 기다리는데, 스레드 B가 자원 2를 쥐고 자원 1을 기다리면 두 스레드는 영원히 멈춥니다. 셋째는 우선순위 역전(priority inversion)입니다. 우선순위가 낮은 스레드가 쥔 잠금을 우선순위가 높은 스레드가 기다리는데, 그 사이에 중간 우선순위의 스레드가 CPU를 차지하면 높은 우선순위의 스레드가 오래 기다리게 됩니다.

더 깊이: 서로 다른 변수인데도 느려지는 거짓 공유

스레드들이 서로 다른 변수만 고치면 문제가 없을 것 같지만, 성능 문제는 남을 수 있습니다. 2장에서 본 것처럼 CPU 캐시는 64바이트 같은 캐시 라인 단위로 일관성을 맞춥니다. 두 스레드가 고치는 변수가 같은 캐시 라인에 있으면, 한 스레드가 쓸 때마다 다른 코어에 있는 그 라인의 사본이 무효화되어 라인이 코어 사이를 계속 오갑니다. 이를 거짓 공유(false sharing)라고 하며, 정확성에는 문제가 없지만 스레드를 늘렸는데 오히려 느려지는 원인이 되기도 합니다. 스레드마다 자주 고치는 값은 서로 다른 캐시 라인에 놓이도록 배치하거나, 스레드마다 지역 변수에 모았다가 마지막에 한 번만 합치는 방법으로 피합니다.

정리

  • 스레드는 프로세스 안의 실행 흐름입니다. 같은 프로세스의 스레드들은 코드, 전역 변수, 힙, 열린 파일을 함께 쓰고, 프로그램 카운터와 레지스터, 스택, 신호 마스크는 따로 가집니다.
  • 스레드별 스택은 보호된 영역이 아니므로, 지역 변수의 주소를 다른 스레드에 넘기면 동기화와 수명을 함께 따져야 합니다.
  • 스레드를 쓰면 응답성이 좋아지고, IPC 없이 데이터를 공유하며, 프로세스보다 적은 비용으로 만들고 전환할 수 있고, 여러 코어를 쓸 수 있습니다.
  • 암달의 법칙에 따라 순차 부분의 비율이 ss이면 CPU를 아무리 늘려도 가속비는 1s\frac{1}{s}를 넘지 못합니다.
  • 동시성은 여러 작업이 함께 진행되도록 나눈 구조이고, 병렬성은 여러 작업이 같은 순간에 실행되는 것입니다.
  • 메모리를 함께 쓰는 대가로 경쟁 조건, 교착 상태, 우선순위 역전 같은 문제가 생기며, 서로 다른 변수라도 같은 캐시 라인에 있으면 거짓 공유로 느려질 수 있습니다.

연습 문제

1. 병렬화할 수 있는 부분이 60초라면

순차 실행에 100초 걸리는 작업 가운데 60초만 나누어 실행할 수 있습니다. CPU 4개를 쓸 때 이상적인 실행 시간과 가속비를 구하고, CPU를 무한히 늘릴 때 가속비의 상한도 구하세요. 이 작업을 스레드 8개로 나누면 CPU 4개에서 나눌 수 있는 부분을 8등분한 시간에 끝낼 수 있을까요?

해설 보기

나눌 수 없는 부분이 40초이므로 CPU 4개를 쓰면 40 + 60 ÷ 4 = 55초가 걸리고, 가속비는 100 ÷ 55 ≈ 1.82배입니다. CPU를 무한히 늘려도 40초는 남으므로 상한은 100 ÷ 40 = 2.5배입니다. 앞의 speedup(0.4, 4)로 계산해도 1.82가 나옵니다. 스레드를 8개 만들어도 CPU가 4개라면 한 순간에 실행되는 스레드는 최대 4개이므로, 나눌 수 있는 부분은 여전히 60 ÷ 4 = 15초보다 빨리 끝날 수 없습니다. 오히려 스레드가 CPU보다 많으면 전환 비용이 더해져 조금 더 느려질 수 있습니다.

2. 사라진 지역 변수

다음 C 코드의 start_worker는 지역 변수 job의 주소를 새 스레드에 넘기고 바로 반환합니다. 새 스레드가 *(int *) arg를 읽을 때 어떤 문제가 생길 수 있는지, 스레드별 스택의 성질로 설명하고 고치는 방법을 제시하세요.

#include <pthread.h>
#include <stdio.h>

static void *worker(void *arg)
{
    printf("작업 번호: %d\n", *(int *) arg);
    return NULL;
}

void start_worker(pthread_t *tid)
{
    int job = 42;
    pthread_create(tid, NULL, worker, &job);
}
해설 보기

job은 start_worker를 실행한 스레드의 스택에 있습니다. 스택은 보호된 영역이 아니므로 새 스레드도 그 주소로 값을 읽을 수 있지만, start_worker가 반환하면 그 스택 프레임은 사라지고 그 자리는 다음 함수 호출이 덮어쓸 수 있습니다. 새 스레드가 언제 실행될지는 정해져 있지 않으므로, 실행이 늦으면 42가 아닌 엉뚱한 값을 읽게 되며, C에서는 이것이 정의되지 않은 동작입니다. 고치려면 넘기는 값이 새 스레드가 쓰는 동안 살아 있게 해야 합니다. malloc으로 힙에 값을 만들어 넘기고 새 스레드가 다 쓴 뒤 free하거나, 호출한 쪽이 pthread_join으로 새 스레드가 끝날 때까지 기다린 뒤에 반환하게 할 수 있습니다.

3. 기다리는 시간이 긴 서버의 처리량

요청마다 데이터베이스 응답을 500ms 기다리고, 그 밖의 처리 시간은 무시할 만큼 짧은 서버가 있습니다. 스레드 하나로 요청을 차례로 처리할 때와, 스레드 50개가 요청을 하나씩 맡아 처리할 때 1초에 처리할 수 있는 요청 수를 각각 구하세요. 스레드 50개 방식의 처리량을 실제로 얻으려면 어떤 조건이 필요할까요?

해설 보기

스레드 하나로 처리하면 요청 하나에 500ms가 걸리므로 1초에 2개를 처리합니다. 스레드 50개면 50개의 요청이 함께 데이터베이스를 기다릴 수 있으므로, 500ms마다 50개씩 끝나 1초에 100개를 처리할 수 있습니다. 대기 시간이 처리량을 막고 있었으므로, CPU가 하나여도 이 효과는 얻을 수 있습니다. 다만 이 계산은 데이터베이스가 동시에 50개의 질의를 받아도 응답 시간이 500ms로 유지된다고 가정한 것입니다. 데이터베이스의 연결 수나 처리 능력이 부족하면 응답이 늦어지거나 연결을 얻으려고 기다리게 되므로, 스레드를 늘리는 것만으로 처리량이 늘지는 않습니다. 스레드마다 스택 메모리가 드는 것도 함께 고려해야 합니다.

다음 글 멀티스레딩 모델에서는 스레드를 사용자 공간의 라이브러리가 관리하는 방식과 커널이 관리하는 방식이 어떻게 다르고, 둘을 어떻게 짝짓는지 살펴봅니다.