OpenMP의 멀티 쓰레드 구현 과정
1. 병렬화할 부분을 찾아낸다.
2. 동기화가 필요하면 동기화 기능을 구현한다.
3. 프로그램 오류가 발생하면 디버깅을 한다.
4. 순차 프로그램과 병렬 프로그램 성능을 평가한다.
OpenMP 프로그래밍 시작
-
반복 루프의 병렬처리
반복 루프 작업을 여러 개의 스레드로 나누어 빠르게 처리할 수 있는 방법을 알아본다.
OpenMP 지시어는 다음과 같은 의미로 쓰이게 된다.
1. #pragma omp parallel : 지정된 스레드에 맞춰 스레드를 생성해 주세요.
2. #pragma omp for : 다음에 나오는 for 루프 작업을 생선된 스레드에 맞춰서 배분해 주세요.
// for 루프 작업 분할이 된 병렬프로그램
// forparallel.c
#include "stdafx.h"
#include <omp.h>
#include "math.h"
int _tmain(int argc, _TCHAR* argv[])
{
const int MAX = 100000000;
float* Data;
Data = new float[MAX];
int i = 0;
for (i = 0; i < MAX; i++)
Data[i] = i;
#pragma omp parallel
{
#pragma omp for
for (i = 0; i < MAX; i++)
Data[i] = sqrt(Data[i]);
}
printf("Data : %f, %f, %f, %f, %f\n", Data[0], Data[1], Data[2], Data[3], Data[4]);
delete Data;
return 0;
}
-
작업의 병렬처리
작업 단위로 병렬화하는 것을 살펴보자
작업의 병렬 처리는 각기 다른 작업을 여러 개의 스레드가 작업의 종류별로 처리하는 것을 의미
작업 A : 입출력이 많으며 계산이 적음
작업 B : 입출력이 적고 계산이 많음
2개의 스레드가 서로 다른 작업을 배분받아 수행하게 된다. 해야 할 작업이 스레드 별로 다르기 때문에 어떤 스레드가 먼저 종료될지는 알 수 없어 하나의 스레드가 미리 끝났을 경우 다른스레드의 작업시간 동안 대기하여 작업시간이 동일하게 된다.
#include "stdafx.h"
#include <omp.h>
#include "Math.h"
int tmain(int argc, _TCHAR* argv[])
{
const int MAX = 100000000;
int i = 0;
float* Data1 = new float[MAX];
float* Data2 = new float[MAX];
InputData(Data1, MAX);
InputData(Data2, MAX);
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
CalcSQRT(Data1, MAX);
#pragma omp section
CalcLOG(Data2, MAX);
}
}
printData(Data1);
printData(Data2);
delete Data1;
delete Data2;
return 0;
}
-
태스크(Task) 병렬처리
앞서 살펴본 루프 병렬화와 작업 분할 병렬화의 주체는 스레드이다.
루프 병렬화는 스레드가 반복 루프 작업을 배분받아 실행한다.
작업 분할 병렬화는 스레드가 서로 다른 작업을 배분받아 실행한다.
병렬화 시점부터 스레드가 작업을 받아서 수행하는 것이다.
지금부터 살펴볼 태스크 병렬처리는 병렬화의 주체가 태스크이다.
태스크 병렬처리 방법은 마치 회사에서 오늘까지 해야할 일감이 쌓여있고, 그 일을 다 마치면 퇴근하는 것과 같다.
1. 병렬처리를 수행해야 할 작업 전부를 한 번의 함수 호출로 진행되는 작업 또는 한 번의 구문으로 실행할 수 있는 작업으로 태스크의 단위를 작게 나눈다.
하나의 작업 = 1 / n(태스크로 나눈 총 개수)
2. 태스크 단위로 분리된 n개의 작업을 하나의 스레드가 태스크 큐에 모아 놓는다.
3. OpenMP 지시어로 생성된 스레드 중에서 일할 준비가 된 스레드부터 태스크를 가져와 실행한다.
4. 스레드가 하나의 태스크를 완료하면 다시 유휴 상태가 되어 일할 준비가 된다. 다시 작업 큐에 있는 태스크를 가져와 실행한다. 모든 태스크가 완료될 때까지 계속 반복한다.
이렇게 작업의 주체가 태스크가 되면서 OpenMP에서 지원하는 병렬화의 범위가 넓어졌다. 태스크 병렬화를 적용하면 재귀함수의 호출, C++반복자(iterator)를 이용한 구문, while문을 이용한 루프 구문에서 더 효율적인 프로그래밍을 할 수 있다.
Task 지시어 사용 방법
#pragma omp parallel // 멀티 스레드가 생성됨
{
#pragma omp single // for 루프 회수만큼 일감을 쌓으세요
for(i = 0; i < MAX; i++)
{
#pragma omp task //하나의 일감을 구분하는 단위
{
//태스크 처리하는 코드
}
}
}
병렬영역 시작 부분에서 #pragma omp parallel 지시어가 사용되며 병렬 스레드가 생성된다. 다수의 스레드가 생성되지만 #pragma omp single 지시어를 사용하여 하나의 스레드만 동작하게 된다.
하나의 스레드가 태스크 큐에 태스크 를 만들어 넣는 작업을 하게 된다. 하나의 스레드가 해야 할 작업을 for 루프 횟수만큼 반복하면서 작업장에 쌓아놓게 된다. 한개의 작업의 크기는 #pragma omp task로 둘러싸인 만큼의 양이다.
#pragma omp task 지시어의 영역으로 지정된 작업을 하나의 태스크로 설정하여 max카운트에 해당하는 개수만큼 태스크 큐에 들어간다. 태스크 큐에 작업이 들어가면 유휴 스레드부터 태스크를 가져와 작업하게 된다.
병렬영역 내에서 실행되는 재귀함수는 루프 병렬화로 효율을 내기가 어려운 부분이 있다. 재귀함수의 대표적인 예로 많이 나오는 피보나치 수열을 통해 알아보자.
// Fibonacci.cpp
#include "stdafx.h"
#include "omp.h"
int Fibonacci(int n)
{
int x, y;
if (n < 2)
return n;
else
{
x = Fibonacci(n - 1);
y = Fibonacci(n - 2);
return (x + y);
}
}
int FibonacciTask(int n)
{
int x, y;
if (n < 2)
return n;
else
{
#pragma omp task shared(x)
x = Fibonacci(n - 1);
#pragma omp task shared(y)
y = Fibonacci(n - 2);
#pragma omp taskwait
return (x + y);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
const int MAX = 41;
int FibNumber[MAX] = { 0 };
int i = 0;
#pragma omp parallel
{
#pragma omp single private(i);
for (i = 1; i < MAX; i++)
{
FibNumber[i] = FibonacciTask(i);
}
printf("피보나치수 : ");
for (i = 1; i < MAX; i++)
printf("%d ", FibNumber[i]);
return 0;
}
-
스레드 메모리 공간
각각의 객체는 고유한 메모리 영역을 가지게 된다. 클래스, 글로벌, 정적변수, 동적 메모리와 같이 다양한 객체들이 자신들 변수를 소유한다. OpenMP가 지원하는 스레드들도 각각의 고유한 메모리 영역을 가지게 되는데, 이것을 잘 구분해야만 병렬처리를 하면서 작업을 배분하거나 결과를 얻어낼 때 데이터 경쟁 같은 오류를 막을 수 있다.
공유 메모리 영역
#pragma omp parallel 지시어 영역 밖에 있는 메모리 변수는 생성된 스레드가 공유할 수 있다. 모든 스레드가 읽고 쓰기를 할 수 있다고 하여 공유 메모리(shared)변수라고 한다. 만일 2개의 스레드가 하나의 변수의 값을 수정하면 어떻게 동작하게 될지 살펴보자.
0으로 초기화 된 공유 메모리 변수 x가 있다. 0번 스레드가 x 의 값을 1로 변경하였다. 이후에 1번 스레드가 2라고 값을 변경하고 병렬영역이 종료된 후에 값 x는 2로 변하게 된다. 모두가 접근할 수 있기 때문에 마지막에 작성한 값이 계속 남아있게 된다.
이처럼 공유 메모리 변수에 대해 스레드 간의 입출력으로 발생하는 오류를 경쟁상태(Race Condition)라고 한다.
경쟁상태는 병렬프로그램에서 공유 메모리 변수를 잘못 사용함으로 인해 생기는 버그이다. 사실 변수의 잘못된 사용이라기 보다는 병렬프로그램을 잘못 설계하여 발생하는 버그에 가까운데 경쟁상태가 발생하면 심각함 오류를 발생시키고 디버그하기도 어렵다.
해당 변수에 대해 여러 스레드가 읽고 쓰기 위해서 경쟁하는 것을 막기 위해 개별 스레드가 소유하는 메모리 변수를 할당하거나 동기화 처리를 하기 된다.
공유메모리의 사용
#include "stdafx.h"
#include "omp.h"
int _tmain(int argc, _TCHAR* argv[])
{
int x = 0; // 공유 메모리 변수 X
printf("main area x : %d\n", x); // x값을 출력한다.
}
#pragma omp parallel
{
// 0번 스레드는 x값을 1로 변경
if( omp_get_thread_num() == 0)
x = 1;
// 1번 스레드는 x값을 2로 변경
else
x = 2;
// 스레드 번호와 값을 출력한다.
printf("Num %d thread area x : %d\n", omp_get_thread_num(), x);
}
// 병렬 영역 밖에서 X값을 출력한다
printf("main area x : %d\n", x);
return 0;
}
결과
main area x : 0
Num 0 thread area x : 1
Num 1 thread area x : 2
main area x : 2
처음에 선언한 변수 x는 main 스레드의 소유이다.
main 스레드 변수는 기본적으로 공유 가능한 메모리 변수로 OpenMP에서 생성한 스레드에서 읽고 쓰기가 가능하다.
1. #pragma omp parallel 지시어로 쿼드코어 CPU에서 4개의 스레드가 생성된다.
2. 0번 스레드는 공유 메모리 변수 x에 대해서 1로 변경하고 그 값을 출력한다.
3. 1번 스레드는 공유 메모리 변수 x에 대해서 2로 변경하고 그 값을 출력한다.
위 코드에서 OpenMP가 지원하는 omp_get_thread_num() API 함수를 사용하였다. 이 함수는 현재 실행 중인 스레드의 번호를 알려주는 기능을 한다.
따라서 위의
if(omp_get_thread_num()==0) 과 같이 작성하면 "0번 쓰레드만 수행하세요."라는 의미
스레드 프라이빗 변수
예제) 스레드 프라이빗 지정
#include "stdafx.h"
#include "omp.h"
int _tmain(int argc, _TCHAR* argv[])
{
int x = 0;
printf("main area x : %d\n", x);
}
#pragma omp parallel private(x) // 프라이빗 보조지시어 사용
{
if( omp_get_thread_num() == 0)
x = 1;
else
x = 2;
printf("Num %d thread area x : %d\n", omp_get_thread_num(), x);
}
printf("main area x : %d\n", x);
return 0;
}
결과
main area x : 0
Num 0 thread area x : 1
Num 1 thread area x : 2
main area x : 0
'개발자 > Programming' 카테고리의 다른 글
[OpenMP] 옵션 가장 많이 쓰이는 두 가지 (0) | 2020.10.02 |
---|---|
OpenMP 지시어(Directive) (0) | 2020.08.28 |
비쥬얼 스튜디오 코드 필수 익스텐션 10개 추천 (0) | 2020.06.23 |
리눅스 세마포어 사용 예제 (0) | 2020.05.19 |
VS Code 편집기 분할 단축키 (0) | 2020.05.19 |