멀티코어 프로그래밍

멀티코어 프로그래밍(Multi-core programming)이란 하나의 작업을 위해 여러 개의 CPU 코어를 사용하기 위해 코드를 작성하는 작업을 말한다. 멀티코어를 사용하기 위해서는 여러 가지 방법이 있다.

1 기본 용어 정리[편집]

1.1 프로세스[편집]

프로세스(Process)란 간단하게 실행 중인 프로그램을 말한다. 좀 더 자세히 말하면 다음과 같은 요소로 이루어진다.

  • 프로세스가 실행할 프로그램 명령문이 저장된 텍스트 메모리 공간
  • 프로세스 내에서 함수가 호출된 순서과 호출한 위치를 저장하는 스택 메모리 공간
  • 프로세스에서 동적으로 할당된 힙 메모리 공간
  • 프로세스의 현재 상태가 저장된 CPU의 레지스터들

1.2 스레드[편집]

스레드(Thread)란 프로세스 내에서 프로세스의 텍스트 메모리 공간과 힙 메모리 공간을 공유하며 별개의 스택 메모리 공간과 CPU 레지스터를 가지는 별개의 실행 흐름이다. 스레드에는 유저 레벨 스레드와 커널 레벨 스레드로 구분된다.

1.3 컨텍스트[편집]

Context는 문맥이라 번역하는데 프로세스 혹은 스레드의 현재 무엇을 실행하고 있는지, 어디까지 실행하고 있는 정보를 말한다.

1.4 컨텍스트 스위칭[편집]

Context switching은 현재 실행 중이던 스레드가 다른 스레드로 교체되는 작업을 말한다. 이 작업은 운영체제에서 담당한다.

2 멀티 코어 사용 기법[편집]

2.1 멀티 프로세스 프로그래밍[편집]

멀티 프로세스 프로그래밍은 하나의 작업을 위해서 여러 개의 프로세스를 생성하여 여러 CPU코어를 사용하기 위해 코드를 작성하는 작업을 말한다. 프로세스는 각자 별개의 메모리 영역을 가지고 있기 때문에 서로의 메모리 공간에 접속할 수 없기 때문에 IPC(InterProcess Communication)이라는 기법이 필요하다.

2.2 멀티 스레드 프로그래밍[편집]

멀티 스레드 프로그래밍은 하나의 작업을 위해서 하나의 프로세스에서 여러 스레드를 생성하여 여러 CPU코어를 사용하기 위해 코드를 작성하는 작업을 말한다. 스레드는 하나의 프로세스의 힙 메모리 공간과 텍스트 메모리 공간을 공유하고 있기 때문에 서로 변수나 객체의 교환이 가능하다.

2.3 GPU를 이용한 멀티 코어 프로그래밍[편집]

그래픽카드는 CPU보다 훨씬 많은 산술 연산 장치를 가지고 있는 하드웨어이다. 특정 라이브러리(CUDA, OpenCL, Direct Compute)를 이용하면 CPU보다 연산 속도가 빠른 GPU를 사용할 수 있다. 하지만 복잡하다는 단점이 있다.

2.4 멀티 프로세스와 멀티 스레드의 장단점[편집]

멀티 프로세스 프로그램의 경우에는 여러 프로세스를 사용해서 서로 간의 프로세스 통신을 해야 하고 프로세스와 프로세스 사이의 컨텍스트 스위칭이 스레드와 스레드 사이의 컨텍스트 스위칭 보다 부하(overhead)가 크므로 전체 속도에서 손해를 볼 수 있다. 반면 각각의 프로세스는 별개의 메모리 공간을 지니고 있으므로 하나의 프로세스에서 오류가 생겨 종료한다 해도 다른 프로세스에는 영향이 없다는 장점이 있다.

멀티 스레드 프로그램의 경우에는 한 프로세스에서 여러 스레드를 사용하므로 서로 간의 통신또한 빠르며 프로세스와 프로세스의 컨텍스트 스위칭이 없으므로 속도가 빠를 수 있다. 하지만 여러 스레드가 하나의 프로세스에 존재하므로 하나의 스레드에 문제가 생겨 종료가 되면 다른 스레드에서 영향을 미쳐 프로세스가 종료가 된다는 단점이 있다.

3 멀티 코어 사용시에 생길 수 있는 문제[편집]

3.1 공유 자원 문제[편집]

공유 자원이란 시스템 안에서 각 프로세스, 스레드끼리 함께 접근을 할 수 있는, 모니터, 프린터, 메모리, 파일, 네트워크등의 자원을 말한다. 공유자원은 프로세스에서 접근하여 수정할 수 있기 때문에 A라는 프로세스에서 B라는 파일을 수정하고 있을 때, C라는 프로세스가 B파일을 읽는다면 C프로세스가 읽는 B파일 데이터에 오류가 생길 수 있으며, A프로세스와 C프로세스가 B파일을 동시에 수정하려 할 때는 충돌이 일어날 수 있다.

이런 문제를 해결하기 위해서 운영체제에서 공유 자원을 관리하기 때문에 멀티 프로세스 프로그램에서는 속도는 느리지만 대부분의 공유자원 문제에서 자유롭다. 하지만 멀티 스레드 프로그램의 경우에는 프로세스 안에서 메모리를 공유하기 때문에 메모리 공유의 문제가 생긴다.

멀티 코어 프로그램을 제작할 때 발생하는 대부분의 문제는 이 공유자원의 접근으로 일어난다.

3.2 데드락 문제[편집]

4 멀티 코어 프로그래밍을 위한 동기화 및 통신 기법[편집]

이런 공유자원에 접근하는 등, 동시에 여러 프로세스나 스레드가 접근하면 안 되는 코드 구간을 임계영역(Critical Section)이라 부른다. 이런 임계영역에 대한 접근을 제한하기 위한 동기화기법이 있으며 멀티 프로세스, 멀티 스레드에서 각 프로세스 혹은 스레드끼리 상황이나 상태를 통신하기 위한 통신기법이 있다.

4.1 뮤텍스[편집]

Mutex는 Mutual Exclusion(상호 배제)의 약자로서 공공 화장실에 비유할 때 화장실 열쇠를 들고 공공 화장실에 들어갈 때 들어가서 문을 잠그면(Lock) 다른 사람들은 그 사람이 나올 때까지 대기해야 한다(Wait). 이후 들어갔던 사람이 문에서 나와서 열쇠를 다시 제자리에 둔다(unlock)그러면 대기하던 사람들 중 한 명이 다시 열쇠를 들고 공공화장실에 들어간다. 이처럼 임계영역에 들어가야 하는 스레드 혹은 프로세스 중, 하나가 들어가면 다른 것들을 못들어 오도록 막는 것을 상호 배제(Mutual Exclusion)이라 한다.

프로그래밍에서 뮤텍스는 프로세스 내에서 사용할 수 있는 상호배제를 위한 객체이다. 어떤 스레드가 뮤텍스 객체를 이용해서 임계영역에 들어간 다음에 잠그면(lock)하면 다른 스레드는 그 임계영역에 들어가 잠그려 할 때, 이미 들어간 스레드가 있으면 그 위치에서 대기한다. 이 때 대기하는 스레드는 이미 들어간 스레드가 나올 때까지 뮤텍스의 lock이 풀렸는 지를 반복문을 돌면서 확인하는 데 이를 spinlock이라고 한다.

4.2 세마포어[편집]

Semaphore는 Mutex와 거의 비슷한 기능을 하지만 더 정교한 작업을 할 수 있는데, 보통 프로그래밍에서는 프로세스간에 세마포어를 공유할 수 있으며, 임계구역에 들어갈 수 있는 프로세스나 스레드 수를 지정할 수 있다. 이 때 뮤텍스처럼 임계구역에 하나의 프로세스나 스레드만 들어갈 수 있는 세마포어를 바이너리 세마포어라고 부른다.

세마포어는 뮤텍스와 다른 점은, 뮤텍스는 임계구역에 들어간 프로세스나 스레드가 나올 때까지 반복문을 돌면서 확인하지만, 세마포어는 계속 확인하는 것이 아니라, 이미 다른 스레드가 들어가 있으면 임계구역에 들어가려 하는 다른 프로세스나 스레드들은 대기상태가 되여 CPU를 사용하기 위해 기다리는 대기열에서 빠진다.

이들 대기상태에 빠진 프로세스나 스레드들은 임계구역에 들어갔던 프로세스나 스레드나 나올 때 unlock을 하면, 프로세스나 스레드들 중 하나가 대기 상태에서 풀리며 CPU를 사용하기 위해 기다리는 대기열에 들어간다.

4.3 파이프[편집]

4.4 메일박스[편집]

4.5 공유메모리[편집]

5 실제 프로그램의 예[편집]

멀티 코어 프로그래밍은 운영체제에 따라 상당 부분이 다를 수 있다. 같은 기법이지만 이름이 다르다거나 사용하는 함수가 다르거나, 사용하기 위한 순서가 다른 경우가 있다. 따라서 각 운영체제에 따라 실제 프로그램 코드도 달라진다. 나중에 다른 사람이 부족한 부분을 채워줬으면 한다.

5.1 C++11 표준 라이브러리[편집]

C++11에서 표준 라이브러리에 스레드와 뮤텍스가 추가되었다. 따라서 기본적인 멀티 스레드 프로그램은 C++11의 표준을 이용하여 충분히 구현할 수 있다.

5.2 Windows API[편집]

WIndows API에서 스레드가 지원되며 동기화를 위한 방법과 통신을 위한 방법 두 가지를 다 지원한다.

5.3 POSIX THREAD API[편집]

POSIX에 규정된 thread API를 구현한 것을 pthread라이브러리라 한다. pthread는 리눅스, 유닉스, maxOS, Windows등 다양한 운영체제에서 구현된 라이브러리가있다.

5.4 기본적인 멀티 스레드 프로그램 코드[편집]

다음은 스레드를 이용하여 짝수와 홀수를 각각 합하는 스레드를 만든 후, 메인 함수에서 두 값을 합하는 코드이다.

5.4.1 C++11 표준 라이브러리[편집]

#include <thread>
#include <mutex>
#include <iostream>
int main()
{
	int odd = 0;
	int even = 0;
	std::thread oddAdder = std::thread{ [](int * odd)->void{
		for (int i = 1; i < SHRT_MAX * 2; i += 2)
		{
			*odd += i;
		}
	},&odd};
	std::thread evenAdder = std::thread{ [](int * even)->void {
		for (int i = 0; i < SHRT_MAX * 2; i += 2)
		{
			*even += i;
		}
	},&even };
	oddAdder.join();
	evenAdder.join();
	std::cout<<"Sum from 0 To "<< SHRT_MAX<<" is " << even + odd << std::endl;

    return 0;
}

5.4.2 Windows API[편집]

#include <Windows.h>
DWORD WINAPI OddAdderThreadRun(void * arg)
{
	int * odd = (int*)arg;
	for (int i = 1; i < SHRT_MAX * 2; i += 2)
	{
		*odd += i;
	}
	return NULL;
}
DWORD WINAPI EvenAdderThreadRun(void * arg)
{
	int * even = (int*)arg;
	for (int i = 0; i < SHRT_MAX * 2; i += 2)
	{
		*even += i;
	}
	return NULL;
}
int main()
{
	HANDLE hThreads[2] = { NULL, NULL };
	DWORD threadId;
	int odd = 0;
	int even = 0;
	TCHAR msg[1024];
	
	hThreads[0] = CreateThread(NULL, 0, EvenAdderThreadRun, &even, 0, &threadId);
	hThreads[1] = CreateThread(NULL, 0, OddAdderThreadRun, &odd, 0, &threadId);
	WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
	wsprintf(msg, TEXT("sum from 0 to %d is %d"), SHRT_MAX * 2, odd + even);
	MessageBox(NULL, msg, TEXT("WIN32API Thread Test"), MB_OK);
	return 0;
}

5.4.3 POSIX THREAD API[편집]

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

void* odd_adder_run(void* arg)
{
    int* odd = (int*)arg;
    for (int i = 1; i < SHRT_MAX * 2; i += 2)
	{
		*odd += i;
	}
    return NULL;
}
void* even_adder_run(void* arg)
{
    int* even = (int*)arg;
    for (int i = 0; i < SHRT_MAX * 2; i += 2)
	{
		*even += i;
	}
    return NULL;
}

int main()
{
    pthread_t even_thread = NULL;
    pthread_t odd_thread = NULL;

    int even = 0;
    int odd = 0;

    pthread_create(&even_thread, NULL, even_adder_run, &even);
    pthread_create(&odd_thread,NULL, odd_adder_run, &odd);
    pthread_join(even_thread, NULL);
    pthread_join(odd_thread, NULL);

    printf("sum from 0 to %d is %d", SHRT_MAX * 2, even + odd);
    return 0;
}

5.5 뮤텍스를 이용한 동기화[편집]

뮤텍스를 이용한 동기화를 이용하는 대표적인 경우는 바로 메모리, 변수에 접근하여 값을 수정하거나 변경할 때이다. 다음과 같은 경우, 결과값이 항상 다른 것을 알 수 있다.

#include <iostream>
#include <thread>
using namespace std;

int main()
{
    std::thread* threads[8];
    long long res = 0;
    volatile bool wait = true;
    auto adder = [&wait](long long * s)->void{
        while(wait == true);
        for(int i = 0 ; i < INT_MAX; i++)
        {
            *s = *s + 1;
        }
    };
    for(int i = 0 ; i < 8 ;i++)
    {
        res = 0;
        wait = true;
        for(int j = 0; j < 8 ; j++)
        {

            threads[j] =new std::thread{adder, &res};

        }
        wait = false;
        for(int j = 0 ; j < 8 ; j++)
        {
            threads[j]->join();
            delete threads[j];
        }
        std::cout<<"result is "<<res<<std::endl;
    }
    return 0;
}

6 각주