시리즈/Concurrency

동시성 재료 살펴보기

빅또리 2021. 10. 27. 18:28

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나 뭐 그런건 다르겠지)

 

PCB는 kernel space에 위치한다! PCB에 Program Counter(다음 실행할 명령어 address), Stack Pointer(콜스택 포인터) 같은 값들도 저장되어 있다.

 

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 모델처럼) 동작하는 건가..? 쓰레드를 구체적으로 어떻게 확보해둔다는 건지 잘 모르겠다.