동시성 재료 살펴보기
parrallelism vs concurrency
parrallelism => 여러 태스크가 진짜로 동시에 진행됨.
concurrency => 시분할로 여러개의 태스크가 동시에 진행되는 것 처럼 보이는 것.
일꾼
parallelism을 가능하게 하는 컴퓨터의 일꾼 늘이기 기법에 대해 알아보자.
일단 가장 간단하게는 cpu에 코어가 여러개 장착되어 있는 것을 생각할 수 있지만 이외에도 다양한 방법으로 parallelism을 지원하는데,
우선 파이프라이닝을 통한 명령어 수준의 병렬성이 있다.
- 하이퍼스레딩 (SMT)
cpu 사려고 찾아보면 이렇게 4코어 8쓰레드 이런식으로 적혀있는데 이게 뭔지 아시는가?
옛날에는 인텔이 하이퍼스레딩이라는 기법을 통해서 코어가 1개이지만 마치 2개인 것 같은 성능을 낸다라는 소리를 들었을때 도저히 어떻게 그게 가능한건가 이해가 안갔는데 이제와 생각해보니 한 클락에 여러 명령어(기계어 레벨)가 동시에 올라올 수 있게하는 파이프라이닝으로 구현한거였다! 그리고 컴구때 교수님이 파이프라이닝 수업 하면서 여러 멀티스레딩 방식을 설명했던 것도 같은게 언뜻 생각나고...
예전글 : 파이프라인 비유
fine-gran multithreading => 매 cycle 마다 thread 스위치를 한다.
coarse-grain multithreading => long stall 발생시에만 thread 스위치
SMT (simultaneous multithreading) => 1 cycle에 1개 이상의 instruction을 수행한다. 슈퍼스칼라 프로세서로 구현한다.
superscalar processor
In contrast to a scalar processor that can execute at most one single instruction per clock cycle, a superscalar processor can execute more than one instruction during a clock cycle by simultaneously dispatching multiple instructions to different execution units on the processor.
슈퍼스칼라는 CPU 내에 파이프라인을 여러 개 두어 명령어를 동시에 실행하는 기술이다
여기 이 SMT의 인텔버전 구현체가 하이퍼쓰레딩 (HT) 였다. (AMD에서는 똑같이 SMT라고 부른다.)
벤치마크 해봤을 때 쓰루풋이 평균적으로 코어 1개로 처리할 수 있는 쓰레드의 2배 정도가 나온다라는 뜻이지 않을까.
이외에도
- 비트 수준 병렬성 => 컴퓨터 아키텍처가 8bit -> 16bit -> 32 bit -> 64 bit로 발전.
- SIMD 같은 데이터 병렬성 => GPU 등에서 하나의 값으로 여러개의 데이터 동시에 계산. 이미지 처리에 많이 사용된다.
같은 것들이 있다고 한다.
일감
프로세스 vs 쓰레드
process => Different program in execution
thread => The basic unit of CPU utilization (threads) that exist as subsets of a process.
프로세스는 디스크에 있는 프로그램 (.exe) 을 실행을 위해 메모리에 적재한 것으로 (kernel space에는 PCB, user space에는 (code, data, heap, stack) 같은 것들) OS 스케줄링 단위이고 서로 다른 프로세스끼리는 독자적이므로 서로의 영역을 침범할 수 없고 IPC를 통해 통신해야 한다. (심지어 parent - child process 끼리도 pipe 라는 IPC를 통해 통신한다고 한다.)
fork()
유닉스 계열 OS에서 프로세스가 자기 자신을 복제하는 시스템 콜
virtual memory는 완전 동일하도록 한벌 copy해서 새로운 physical memory address에 맵핑하는 방식으로 이루어질 것 같다.memory에서 유저 스페이스에 할당되는 코드(instructions), 전역, static 변수 (Data, BSS 차이는 initialized 여부라고 함)
heap, 콜스택 데이터들 뿐만아니라 kernel space의 PCB까지 동일하게 copy. (물론 pid나 뭐 그런건 다르겠지)
fork() 직후의 전역변수나 지역변수 값은 parent와 child에서 동일하게 시작하는데 이제 각자의 길을 걷기 때문에, 이후의 변수 값 변화는 서로의 프로세스에 영향을 주지 않는다.
그리고 SP나 PC값도 copy되기 때문에 child process는 코드 첫번째 라인부터 실행되는게 아니라, 자기를 생성한 fork() 명령어 라인 다음줄 부터 실행된다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char* argv[]){
printf("1 me:%d\n", getpid());
fork();
printf("2 me:%d parent:%d\n", getpid(), getppid());
fork();
printf("3 me:%d parent:%d\n", getpid(), getppid());
fork();
printf("4 me:%d parent:%d\n", getpid(), getppid());
sleep(1);
return 0;
}
보통 fork하는 프로그램은 fork() 리턴값으로 child/parent 프로세스를 구분해서 분기처리 하는 식의 패턴으로 많이 짠다.
-1 => fork 성공 못함
0 => child process
양수 => parent process
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
const int SEVEN_AND_A_HALF_MILLION_YEARS = 3;
const int A_DAY = 1;
// Allocated in data segment.
static int the_answer = 0;
int main(int argc, char* argv[]){
// Allocated in stack segment.
int arthur = 0;
pid_t pid;
switch(pid = fork()){
default:
// HINT: The parent process should fall into this scope.
the_answer = 42;
arthur = 6 * 9;
sleep(SEVEN_AND_A_HALF_MILLION_YEARS);
break;
case 0:
// HINT: The child process should fall into this scope.
sleep(A_DAY * 2);
break;
case -1:
printf("WTF?");
return -1;
break;
}
printf("My pid is %ld (%s)\n", (long)getpid(), pid == 0 ? "child" : "parent");
printf("The answer to the ultimate question of life the universe and everything is %d.\n", the_answer);
printf("But Arthur replied that it was %d.\n\n", arthur);
return 0;
}
여기 스택오버플로우에 올라왔던 질문을 보면 포인터가 가리키는 address가 같은데도 child process의 y만 'Z'로 바꼈다.
이걸보면 C의 포인터가 가리키는게 virtual memory address 인가보다.
두 프로세스의 변수 y의 실제 physical memory 주소는 다르기 때문에 당연히 child 프로세스의 값만 달라질 수 밖에 없다.
예전 글 : 메모리 할당과 관리
쓰레드
프로세스의 subset으로 cpu utilization 단위. (작업 수행 단위)
새로운 스레드를 생성하면 코드, 전역/지역변수, heap 메모리는 다 공유하고 TCB랑 콜스택만 독자적으로 운영한다.
그렇기 때문에 서로 다른 스레드가 예상치 못한 방식으로 critical zone의 데이터에 같이 접근하여 알 수 없는 오염된 결과를 반환하는 concurrency 관련 이슈가 여기서 발생한다.
멀티쓰레딩
설명이 귀여워서 넣어봄
멀티프로세싱
=> 컴퓨터 시스템 한 대에 둘 이상의 중앙처리장치(CPU)를 이용하여 병렬로 처리하는 것을 가리킨다. 또, 이 용어는 하나 이상의 프로세서를 지원하는 시스템의 능력, 또는 이들 사이의 태스크를 할당하는 능력을 가리키기도 한다 (위키피디아)
여기서 프로세싱은 문맥상 process가 아니라 processor 인 것 같다. 컴퓨터의 다수 코어에서의 parallel한 일처리 능력을 가리킨다.
멀티쓰레딩
=> Multithreading is a model of program execution that allows for multiple threads to be created within a process
한 프로세스에서 cpu utilization 단위인 여러 쓰레드를 생성해서 작업을 수행하는 방식.
실제로 여러 코어에 적절하게 배치돼서 parallel하게 돌 수 있을지는 개발자가 컨트롤 할 수 없다.
프로그램의 두 부분을 병렬로 실행하려는 의도로 코드를 작성하는 경우, 프로그램이 실행될 때 실제로 그렇게 되리라는 보장이 있는가? 코어가 하나뿐인 기기에서 해당 코드를 실행하면 어떻게 되는가? 여러분 중 누군가는 이 코드가 병렬적으로 실행되리라고 생각할 수도 있지만, 사실은 그렇지 않다!
(중략)
이는 몇 가지 흥미롭고 중요한 사실을 암시한다. 첫째, 우리는 병렬적인 코드를 작성하는 것이 아니라. 병렬로 실행되기를 바라면서 동시성 코드를 작성하는 것이다. 다시 한번 말하지만, 병렬성은 코드의 속성이 아닌 프로그램 실행 시의 속성이다.
- ⌜Concurrency in Go⌟ 49-50p, 캐서린 콕스 부데이
비디오 플레이어에서 IO와 재생을 멀티스레드로 구현한다던가 하면 좋다.
장점 - 응답성 향상 (IO bound한 작업이 오래 걸리더라도 다른 스레드가 작업을 계속하면서 사용자에 응답성이 좋아진다),
자원 공유와 그로인한 효율성 향상. 컨텍스트 스위칭 비용도 프로세스보다 훨씬 적게든다, 다중 cpu 지원
단점 - 모든 스레드가 자원을 공유하기 때문에 한 스레드에 문제가 생기면 전체 프로세스에 영향이 생긴다. (예) IE vs chrome..)
* 한 쓰레드가 fork() 시스템 콜 호출 했을 경우 => 유닉스 os에서 (1) 해당 프로세스의 전체 thread를 복제하는 fork, (2) fork 호출한 쓰레드만 복제하는 fork 두가지 타입이 있다고 함.
유저 스레드 vs 커널 스레드
커널 스레드 = native thread => 커널이 직접 생성하고 관리하는 스레드.
유저 스레드 = green thread = virtual thread => 기본 운영체제가 아닌 런타임/라이브러리/가상머신에 의해 구현된 스레드. RTS가(런타임 시스템) 관리한다. 스케줄링 까지도. 거의 그냥 함수 같은 것.
스레드풀
a thread pool maintains multiple threads waiting for tasks to be allocated for concurrent execution by the supervising program. By maintaining a pool of threads, the model increases performance and avoids latency in execution due to frequent creation and destruction of threads for short-lived tasks
The size of a thread pool is the number of threads kept in reserve for executing tasks.
커널 스레드를 정해진 사이즈만큼 확보해두고 큐에 들은 task (= 유저 스레드?)를 놀고있는 커널스레드에 매핑 해주는 방식으로(n:m 모델처럼) 동작하는 건가..? 쓰레드를 구체적으로 어떻게 확보해둔다는 건지 잘 모르겠다.