사전 CLOCK_THREAD_CPUTIME_ID
개념

CLOCK_THREAD_CPUTIME_ID

gabury1고친 사람 github-actions[bot]

CLOCK_THREAD_CPUTIME_ID 를 넘기면 운영체제가 스레드 하나가 계산에 쓴 시간을 알려 줍니다. 이 시계는 그 스레드가 계산하는 동안에만 갑니다. 같은 프로그램의 다른 스레드가 일한 시간은 세지 않습니다. 덕분에 스레드 여럿이 함께 도는 서버에서도 요청 하나를 맡은 스레드의 몫이 따로 보입니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 운영체제가 들고 있는 여러 시계 가운데 「이 스레드 하나가 계산에 쓴 시간」만 세는 시계를 고르는 이름입니다. 0.22초 걸린 요청을 맡은 스레드가 계산에는 0.02초만 썼다는 것을 이 시계로 압니다.

왜 이렇게 하나 — 서버는 스레드 여럿이 요청을 나눠 처리합니다. 프로그램 전체의 계산 시간을 재면 옆 스레드가 다른 요청에 쓴 시간까지 섞입니다. 스레드 하나만 세면 그 요청에 든 계산만 남습니다.

어떻게 도나

  1. 일을 시작하기 전에 그 스레드에서 이 시계를 읽습니다
  2. 일이 끝나면 같은 스레드에서 한 번 더 읽습니다
  3. 뒤 값에서 앞 값을 빼면 그 사이 이 스레드가 계산에 쓴 시간입니다

대가 — 기다린 시간은 안 잡힙니다. 요청이 중간에 다른 스레드로 넘어가거나 한 스레드가 요청 여럿을 번갈아 처리하면 뺀 값이 요청 하나의 몫이 아니게 됩니다.

상세

가게에 수리 기사 셋이 와서 함께 일한다고 해 봅시다. 가게가 받는 청구서에는 세 사람이 손을 움직인 시간이 모두 더해져 적힙니다. 기사마다 들고 다니는 작업 일지에는 자기 손으로 일한 시간만 적힙니다.

CLOCK_THREAD_CPUTIME_ID 는 이 작업 일지입니다. 청구서는 형제 시계 CLOCK_PROCESS_CPUTIME_ID 가 맡습니다.

프로세스는 실행 중인 프로그램 하나입니다. 스레드는 그 프로세스 안에서 따로 도는 실행 흐름입니다. 웹 서버는 대개 프로세스 하나 안에 스레드를 여럿 둡니다. 요청은 그 스레드들에 하나씩 나눠 맡깁니다.

운영체제의 시계 가운데 하나를 고르는 이름

응용 프로그램은 시간을 스스로 세지 않습니다. 운영체제의 중심부인 커널에 물어봅니다. 커널은 쓰임이 다른 시계를 여러 개 들고 있습니다. 어느 시계를 읽을지는 이름으로 고릅니다.

커널에 시계를 묻는 호출이 clock_gettime 입니다. 첫 인자로 시계 이름을 넘기면 그 시계의 값이 돌아옵니다. CLOCK_THREAD_CPUTIME_ID 는 이 첫 인자로 넘기는 이름 가운데 하나입니다.

clock_gettime 은 시스템 콜입니다. 시스템 콜은 프로그램이 커널에 일을 부탁하는 호출입니다. 시계는 커널이 들고 있으니 커널에 물어야 읽을 수 있습니다.

시계 이름들은 POSIX(Portable Operating System Interface, 이식 가능한 운영체제 인터페이스)가 정해 둡니다. POSIX 는 리눅스와 macOS 같은 유닉스 계열 운영체제가 함께 지키는 규격입니다.

C 프로그램에서는 이 이름을 헤더 파일 time.h 에 정의된 상수로 씁니다. 헤더 파일은 여러 프로그램이 함께 쓰는 이름을 모아 둔 파일입니다.

이 이름은 네 조각으로 읽습니다. CLOCK 은 시계, THREAD 는 스레드, CPUTIME 은 CPU(Central Processing Unit, 중앙 처리 장치) 시간, ID(identifier, 식별자)는 고르는 이름이라는 뜻입니다. 합치면 「이 스레드의 CPU 시간을 세는 시계」입니다.

이 시계는 모든 운영체제에 있지는 않습니다. 리눅스와 macOS 에는 있습니다.

리눅스에서는 이 시계의 값을 바꿀 수 없습니다. 시계 값을 바꾸는 호출 clock_settime 에 이 이름을 넘기면 호출이 실패합니다.

스레드의 CPU 시간

스레드의 CPU 시간은 CPU 가 그 스레드의 명령을 실행한 시간을 모두 더한 값입니다. 커널은 스레드마다 이 합계를 적어 둡니다. CLOCK_THREAD_CPUTIME_ID 로 시계를 읽으면 이 합계가 돌아옵니다.

CPU 는 프로그램의 명령을 하나씩 실행하는 부품입니다. 요즘 CPU 에는 명령을 따로따로 실행하는 코어가 여럿 들어 있습니다. 코어 하나는 한순간에 한 스레드의 명령만 실행합니다.

커널은 코어를 여러 스레드에 번갈아 나눠 줍니다. 한 스레드가 잠깐 돌고 나면 다음 스레드가 돕니다. 이렇게 순서를 정하는 일을 스케줄링이라고 합니다. 커널이 코어를 나눠 주는 단위가 스레드입니다.

스레드의 CPU 시간에는 두 가지가 들어갑니다. 하나는 스레드가 자기 코드를 실행한 사용자 시간입니다. 계산이나 문자열 처리처럼 프로그램이 직접 하는 일이 여기에 들어갑니다.

다른 하나는 시스템 시간입니다. 스레드가 시스템 콜로 부탁한 일을 커널이 코어에서 대신 처리한 시간입니다. 파일 읽기라면 커널이 디스크에 요청을 넘기고 받은 데이터를 옮겨 주는 일이 여기에 들어갑니다. 디스크가 데이터를 내줄 때까지 기다리는 시간은 들어가지 않습니다.

기다리는 동안은 멈춘다

스레드가 기다리는 동안에는 코어가 그 스레드의 명령을 실행하지 않습니다. 이 시계도 그동안은 가지 않습니다. 데이터베이스의 응답을 기다릴 때, 디스크에서 파일이 올라오기를 기다릴 때, 일부러 쉬게 하는 sleep 을 불렀을 때가 모두 그렇습니다.

락은 여러 스레드가 한 데이터를 동시에 고치지 못하게 막는 장치입니다. 한 스레드가 락을 쥐고 있으면 다른 스레드는 락이 풀릴 때까지 기다립니다.

락이 풀리기를 잠들어 기다리면 이 시계도 멈춥니다. 잠들지 않고 락을 계속 확인하며 기다리면 그동안은 시계가 갑니다. 확인하는 일도 코어가 명령을 실행하는 일이기 때문입니다.

코어를 다른 스레드에 뺏긴 동안에도 멈춥니다. 같은 프로세스의 다른 스레드에 뺏겨도 마찬가지입니다. 이 스레드는 돌 준비가 되어 있어도 차례가 올 때까지 기다립니다.

프로세스 시계와 무엇이 다른가

CLOCK_PROCESS_CPUTIME_ID 는 프로세스 안 모든 스레드의 CPU 시간을 더합니다. CLOCK_THREAD_CPUTIME_ID 는 이름을 넘긴 스레드 하나만 셉니다. 아래 그림은 스레드 셋을 둔 프로세스에서 두 시계가 무엇을 세는지 보입니다.

flowchart TD
    subgraph P["프로세스 · CLOCK_PROCESS_CPUTIME_ID 3.0초"]
        A["스레드 A · 자기 시계 1.5초"]
        B["스레드 B · 자기 시계 1.0초"]
        C["스레드 C · 자기 시계 0.5초"]
    end

스레드 A 가 자기 시계를 읽으면 1.5초만 나옵니다.

스레드 하나는 한순간에 코어 하나에서만 돕니다. 그 때문에 이 시계는 걸린 시간을 넘지 않습니다. 프로세스 시계는 코어 여럿이 동시에 돌면 걸린 시간보다 커지기도 합니다.

두 시계의 차이는 바쁜 서버에서 드러납니다. 요청 하나를 처리하는 동안 프로세스 시계를 재면 다른 스레드가 다른 요청에 쓴 계산까지 들어옵니다. 스레드 시계는 그 요청을 맡은 스레드의 몫만 셉니다.

같은 이름, 스레드마다 다른 시계

이 이름은 시계 하나를 가리키지 않습니다. 부른 스레드가 누구냐에 따라 가리키는 시계가 바뀝니다. 스레드 A 가 읽으면 A 의 시계가, 스레드 B 가 읽으면 B 의 시계가 돌아옵니다.

앞뒤 두 번 읽기는 같은 스레드에서 해야 합니다. 앞 값을 A 에서 읽고 뒤 값을 B 에서 읽으면 서로 다른 시계를 뺀 셈이라 아무 뜻이 없습니다.

한 스레드가 요청 여럿을 번갈아 처리하는 코드도 조심해야 합니다. 파이썬의 asyncio 같은 비동기 방식이 그렇습니다. 비동기 방식에서는 한 요청이 데이터베이스를 기다리는 동안 같은 스레드가 다른 요청을 먼저 처리합니다. 그 다른 요청의 계산도 이 시계에 들어갑니다.

이 방식에서 요청 하나를 맡는 단위는 코루틴입니다. 코루틴은 기다리는 동안 스레드를 내주는 작은 함수입니다. 스레드 하나가 코루틴 여럿을 번갈아 돌리니, 이 시계로는 코루틴 하나의 몫을 따로 볼 수 없습니다.

다른 스레드의 시계를 읽으려면 pthread_getcpuclockid 를 씁니다. 스레드를 넘기면 그 스레드 전용 시계 이름을 돌려줍니다. CLOCK_THREAD_CPUTIME_ID 는 부른 스레드를 가리키지만, 이 이름은 늘 넘긴 그 스레드를 가리킵니다. 이 이름을 clock_gettime 에 넘기면 어느 스레드에서 읽든 그 스레드의 값이 나옵니다.

걸린 시간과 나란히 읽기

이 절은 요청 하나 앞뒤에서 시계 셋을 함께 읽습니다. 걸린 시간을 재는 CLOCK_MONOTONIC, 이 스레드의 CLOCK_THREAD_CPUTIME_ID, 프로세스 전체의 CLOCK_PROCESS_CPUTIME_ID 입니다. 셋을 견주면 걸린 시간이 계산과 기다림으로 갈립니다. 다른 스레드의 몫이 얼마나 섞이는지도 보입니다.

아래 코드의 서버에는 작업 스레드가 여럿 있습니다. 다른 스레드들은 다른 요청을 처리하는 중입니다. 그중 한 스레드가 요청을 처리하는 함수 handle_request() 앞뒤에서 세 시계를 읽습니다. 파이썬의 time.clock_gettime 은 C 의 clock_gettime 을 바로 부릅니다.

Python
import time
M = time.CLOCK_MONOTONIC
T = time.CLOCK_THREAD_CPUTIME_ID
P = time.CLOCK_PROCESS_CPUTIME_ID
m0 = time.clock_gettime(M)
t0 = time.clock_gettime(T)
p0 = time.clock_gettime(P)
handle_request()
m1 = time.clock_gettime(M)
t1 = time.clock_gettime(T)
p1 = time.clock_gettime(P)
round(m1 - m0, 2)  # 0.22
round(t1 - t0, 2)  # 0.02
round(p1 - p0, 2)  # 0.35

m1 - m0 은 걸린 시간 0.22초입니다. t1 - t0 은 이 스레드가 계산에 쓴 0.02초입니다. 나머지 0.20초는 이 스레드가 기다린 시간입니다.

p1 - p0 은 0.35초로 걸린 시간보다도 큽니다. 같은 0.22초 동안 다른 스레드들이 다른 요청을 계산한 몫이 들어 있기 때문입니다. 이 값을 이 요청의 계산량으로 읽으면 틀립니다.

값의 시작점과 스레드 풀

읽은 값은 그 스레드가 생긴 뒤 쓴 CPU 시간의 합입니다. 스레드가 막 생겼을 때는 0 에 가깝습니다.

스레드 풀은 스레드를 미리 만들어 두고 요청마다 돌려 쓰는 방식입니다. 백엔드 서버는 대개 이 방식으로 요청을 처리합니다. 돌려 쓰는 스레드의 시계에는 앞서 처리한 요청들의 계산이 쌓여 있습니다. 값 하나만 보지 말고 요청 앞뒤의 차이를 봐야 하는 까닭입니다.

언어별로 스레드 CPU 시간을 읽는 함수

백엔드 코드는 clock_gettime 을 직접 부를 일이 드뭅니다. 언어마다 지금 스레드의 CPU 시간을 돌려주는 함수를 따로 둡니다. 파이썬은 초 단위로, 자바는 나노초 단위로 돌려줍니다. 나노초는 10억분의 1초입니다.

언어 함수 돌려주는 값
파이썬 time.thread_time() 지금 스레드의 CPU 시간 · 초
자바 ThreadMXBean.getCurrentThreadCpuTime() 지금 스레드의 CPU 시간 · 나노초

파이썬의 time.thread_time() 은 리눅스에서 CLOCK_THREAD_CPUTIME_ID 를 읽습니다. 자바의 함수는 java.lang.management 패키지의 ThreadMXBean 에 있습니다. ThreadMXBean 은 자바 프로그램 안의 스레드 정보를 알려 주는 관리용 객체입니다. 두 함수 모두 이 시계와 같은 것을 잽니다.

쓰는 때와 안 쓰는 때

이 시계는 스레드 여럿이 도는 서버에서 작업 하나가 CPU 를 얼마나 먹는지 알고 싶을 때 씁니다. 프로세스 시계로 재면 옆 스레드의 계산이 섞입니다. 걸린 시간과 함께 로그에 찍어 두면 계산 탓인지 기다림 탓인지가 한 줄로 보입니다.

스레드마다 CPU 시간을 모아 보이는 프로파일러도 이 시계를 씁니다. 어느 스레드가 계산을 많이 하는지가 갈려 보입니다.

사용자가 느끼는 응답 시간은 이 시계로 재지 않습니다. 사용자는 기다린 시간까지 겪기 때문입니다. 응답 시간은 CLOCK_MONOTONIC 으로 잽니다.

타임아웃도 이 시계로 걸지 않습니다. 데이터베이스의 응답을 5초까지만 기다리는 기한을 이 시계로 잡으면 5초가 영영 오지 않습니다. 기다리는 동안 시계가 멈춰 있기 때문입니다.

지금이 몇 시인지도 이 시계로는 모릅니다. 날짜와 시각은 CLOCK_REALTIME 이 알려 줍니다.

관련 항목

CLOCK_THREAD_CPUTIME_ID 와 나란히 선 운영체제 시계

CLOCK_PROCESS_CPUTIME_ID · CLOCK_MONOTONIC · CLOCK_REALTIME · CLOCK_BOOTTIME · CLOCK_MONOTONIC_RAW · CLOCK_TAI

CLOCK_THREAD_CPUTIME_ID 를 읽고 기준으로 삼는 호출

clock_gettime · clock_getres · clock_settime · pthread_getcpuclockid · clock_getcpuclockid · timer_create

CLOCK_THREAD_CPUTIME_ID 값을 담는 타입과 단위

struct timespec · clockid_t · time.h · 나노초

CLOCK_THREAD_CPUTIME_ID 처럼 스레드 CPU 시간을 알려 주는 다른 수단

time.thread_time · ThreadMXBean · getrusage · time.process_time

CLOCK_THREAD_CPUTIME_ID 가 세는 CPU 시간의 구성 요소

CPU 시간 · 사용자 시간 · 시스템 시간 · 사용자 모드 · 커널 모드 · 벽시계 시간

CLOCK_THREAD_CPUTIME_ID 의 값을 늘리고 멈추는 커널 동작

스케줄링 · 문맥 교환 · CPU 코어 · 락 · 스핀락 · 블로킹 I-O

CLOCK_THREAD_CPUTIME_ID 가 재는 실행 단위

스레드 · 프로세스 · 스레드 풀 · 코루틴 · 이벤트 루프 · 비동기

CLOCK_THREAD_CPUTIME_ID 로 CPU 사용을 재는 작업

프로파일러 · 프로파일링 · 벤치마크 · CPU 바운드 · I-O 바운드 · 응답 시간

CLOCK_THREAD_CPUTIME_ID 를 정하고 내주는 규격과 소프트웨어

POSIX · 리눅스 · 유닉스 · 커널 · 운영체제 · 시스템 콜 · 시간과 시계

다른 이름: clock_thread_cputime_id · 스레드 CPU 시간 시계 · 스레드 CPU 시계