3학년 학부과정중에 “객체지향 프로그래밍”이라는 이름으로 C# 언어를 배우고 있다. 강의계획서상 쓰레드도 잠시 다룬다고 나와있는데, 짜피 쓰레드쪽에 깊게 파고들건 아닐것 같다. 그래서 이왕이면 알면 좋을것들을 정리해 본다.

쓰레드는 “실제 작업 흐름”을 나타낸다. 우리는 “프로그램”(또는 프로세스)가 실행된다고 이야기 하는데, OS 입장에서는 쓰레드가 작동된다고 파악한다. 아마 컴퓨터구조론 또는 운영체제 과목에서 프로세스 스케쥴링에 대해 배웠을 것이다. 이때 스케쥴링의 단위가 쓰레드이다. OS는 각 쓰레드 정보를 가지고 있다. 그리고 공정하게 각 쓰레드에게 CPU 사용시간을 준다. 함수가 길든, 한 줄 짜리 코드든 OS 입장에서는 다 똑같다. 크롬을 실행하는것은 OS입장에서 크롬의 쓰래드를 실행하는것이다. 게임을 실행하는것은 OS입장에서는 게임의 쓰래드를 실행하는것이다.

다음과 같은 프로그램이 있다고 하자. CPU 입장에서는 그저 어셈블리로 구성된 데이터일 뿐이다. 이 데이터를 실행하는 주체, 기준이 쓰레드이다. 쓰래드가 2개면 2개 지점에서 코드가 실행된다. 쓰래드가 100개면 100개 지점에서 코드가 실행된다.

Untitled

이때, 쓰래드는 “동시성”을 보인다. 사용자가 보기에는 이 쓰레드들이 “동시에” 실행되는것으로 보인다. 하지만 이것은 맞지 않다. 정확히 하자면, [하나의 CPU 쓰레드]에서는 [하나의 프로그램 쓰레드]만 실행될 수 있다. 이게 뭔 말인지 헷갈릴 수 있다. 일단 용어 정립부터 잘 해야한다. 작업관리자에서 CPU정보를 보면 [8코어 16쓰레드] 라는것을 볼 수 있다. 여기서 쓰레드는 “CPU내에서 동시에 실행할 수 있는 흐름의 수, 또는 동시에 실행할 수 있는 프로세스의 쓰레드의 수”라고 생각하면 된다.

CPU내에 쓰래드가 여러개면 동시에 여러개의 프로세스 쓰래드가 동작할 수 있다. 그러나 만약 하나의 쓰래드만 있다면 어떻게 될 까? 이때는 “동시에 돌아가는것 처럼 보인다”. 마치 당신의 컴퓨터에 크롬, 엣지, 게임, 유튜브가 동시에 실행되는 느낌이다. 하지만 실제로는 그렇지 않다.

Untitled

윗 사진은 내 작업용 노트북의 작업관리자이다. 보면 프로세스가 267개, 스레드가 3764개라고 나온다. 물론 IO나 Sleep중인 쓰레드도 많을것이다. 그러나 실제 실행중인 쓰래드가 최소한 16개는 있을것이다. 최소한 CPU의 쓰레드 보다 “실행이 필요한 프로세스의 쓰래드”가 더 많을테다. 하지만 우리가 보기에는 각 쓰레드가 동시에 실행되는것 처럼 보인다.

이것을 알아보려면 CPU와 커널에 대해 봐야한다. 리눅스의 Generic 커널 기준으로, CPU에는 4ms마다 Interrupt이 발생한다. (Low Latency Kernel에서는 1ms마다 Interrupt이 발생한다) CPU에 있는 타이머가 4ms마다 “시간 지났어!” 라고 통보하는 셈이다. Interrupt을 수신받은 커널은 하던 작업을 멈춘다. 즉, 실행중인 쓰레드를 놓고 커널 작업을 수행한다. 이때 kernel은 “실행을 필요로 하는 다음 쓰래드”에게 CPU 사용권을 부여한다. 쉽게 말해서 4ms마다 실행중인 쓰래드를 교체해 준다. 4ms마다 다른 쓰래드가 실행된다. 250Hz로 실행하는 코드가 바뀐다. 250Hz로 쓰래드가 교체되기 때문에 우리 눈에는 프로그램이 동시에 실행되는것 처럼 보인다. 윈도우에서는 64Hz, 매 15.6ms마다 이 작업을 한다.

이것은 코어 단위로 발생한다. 내 노트북은 8 core 16 thread이다. 실제 계산을 담당하는 영역은 8개이다. 그런데 어떻게 CPU는 16개의 쓰래드를 동시에 작동할 수 있을까? 이것은 “하이퍼쓰래드” 기능이 있기 때문에 가능하다. 이것은 또 따로 다뤄야할 내용이다. 대충 적자면, 1개의 명령어라도 실제 처리 시간이 다를 수 있다. 예를들어, 메모리에서 데이터를 읽는데는 수십~수백 클럭이 발생한다. 이런식으로 남는 클럭을 이용해서 추가적인 작업을 한다.

4ms마다 쓰래드가 바뀐다고 생각하자. 그러면 내가 만든 프로그램도 4ms마다 CPU 사용권이 부여되고 탈락될 것이다. 다시 CPU의 사용권을 얻게 된다면, 어디까지 계산됐는지 알 수 있어야 한다. “작업정보를 복원하는 기능”이 필요해진다. 4ms마다 프로그램을 처음부터 실행할 수는 없기 때문이다. OS는 스케쥴링을 할 때 이것을 수행한다. 어셈블리 또는 컴퓨터 구조론을 배웠다면, 레지스터에 대해 배웠을 것이다. 여기 레지스터에 있는 정보만 잘 저장하고 복원하면 “계속해서 작업을 이어 나갈 수 있다.” 이 과정이 Context-Switching이다. 흔히들 Thread Programming을 하면 CPU Bound Task에서 Context-Switching 때문에 오히려 느려질 수 있다고 말한다. 물론 캐싱(파이프라인)의 깨짐도 있지만, OS가 Thread를 전환하면서 이런 잡다한 처리를 추가로 해야하기 때문에 느려지는것도 크다. OS는 쓰래드 정보에 각 레지스터의 값을 매번 저장하고, 불러오고, 큐의 맨 뒤에 넣고, 그 외 일련의 과정을 해야한다.

게임을 할 때에 다른 프로그램을 실행하면 끊김이 발생한다는것이 이 때문이다. 윈도우 기준으로 64Hz마다 쓰래드를 교체해 주는데, 다른 프로그램이 실행중이라면 해당 프로그램도 쓰래드를 받게 될 것이다. 그리고 Context-Switching에 따른 성능 감소도 생길것이다. (물론 I/O 작업이나 특정 지점을 만나면 CPU 사용권을 OS 스케쥴러에게 넘겨준다) 게임 입장에서는 쭉~~~ CPU를 사용하지 못하고 성능 감소까지 먹게되니 끊김이 느껴질 수 밖에 없다.

데이터 접근에 대해서도 생각할 것이 많아진다. 특히 TSO 같은것들 말이다. ARM 프로세서 위에서 코딩을 하면 더더욱 체크해야한다. https://doc.rust-lang.org/nomicon/atomics.html 이런 글을 읽어보자.