일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- material
- thread
- EFFECTIVE C++
- C
- 유니크포인터
- 멀티코어
- stl
- Atomic
- 멀티코어 프로그래밍
- 멀티쓰레드
- 쓰레드
- random access
- vector
- observer pattern
- MultiCore
- 게임공학과
- Multithread
- 옵저버 패턴
- c++
- Unreal
- 옵저버
- 디자인패턴
- sequential
- Design Pattern
- multi-core
- 복사생성자
- 프레임워크
- 스마트포인터
- 메모리관리
- 한국산업기술대학교
- Today
- Total
태크놀로지
멀티쓰레드 프로그램 작성시 주의해야할점 - 컴파일러 본문
다음의 멀티쓰레드 프로그램에 어떠한 문제점이 있을까?
bool data_ready = false;
int g_data;
void Consumer()
{
while (data_ready == false);
int my_data = g_data;
cout << "Received : " << my_data << endl;
}
void Producer()
{
g_data = 999;
data_ready = true;
cout << "data ready = true\n";
cout << "Producer End\n";
}
int main()
{
thread c(Consumer);
thread p(Producer);
p.join();
c.join();
}
디버깅시 결과는 잘 나오지만 릴리즈모드시 무한루프에 빠지게 된다.
-> 디스어셈블리 디버깅을 해보자
디버깅 디스어셈블리 창
void Consumer()
{
005D1010 mov al,byte ptr [data_ready (05D53F8h)]
while (data_ready == false);
005D1015 test al,al
while (data_ready == false);
005D1017 je Consumer+5h (05D1015h)
int my_data = g_data;
cout << "Received : " << my_data << endl;
.
.
.
005D1010 mov - al, byte ptr [data_ready (05D53F8h)] : al은 레지스터이다. bool(1byte)값을 저장한다.
005D1015 test - al, al : 각 레지스터 al을 검사한 다음에 flag 레지스터에 등록한다. ( 0인가? 아닌가? / 어느쪽이 더 큰가? / 차이가 음수인가 양수인가? )
005D1017 je - Consumer+5h(05D1015h) : je(jump equal)은 (05D1015h)주소로 점프를 해라.
05D53F8h의 메모리
bool값도 1(true)값이 잘들어간걸 확인할 수 있다.
디스어셈블리코드를 보면 005D1017 je - Consumer+5h(05D1015h)에서 while문 조건과 상관없이 05D1015h로 점프하고 있다.
릴리즈는 왜 다르게 작동하는가?
컴파일 개수가 줄어든다. (자동 최적화)
메모리 버스는 하나이기 때문에 다른 코어에 영향을 준다.
C언어는 싱글쓰레드 기반이여서 멀티쓰레드에 대한 정의가 없다.
해결책
Case1: 애초에 data ready가 데이터 레이스다. lock을 걸어보자.
void Consumer()
{
m_CP.lock();
while (data_ready == false);
m_CP.unlock();
int my_data = g_data;
cout << "Received : " << my_data << endl;
}
void Producer()
{
g_data = 999;
m_CP.lock();
data_ready = true;
m_CP.unlock();
cout << "data ready = true\n";
cout << "Producer End\n";
}
이럴시 데드락 현상이 발생, while문 내에서 조건문에 따라 lock/unlock을 사용해서 데드락문제를 해결했어도 성능상에 영향을 끼침.
Case2: Volatile을 사용하면 된다.
- 반드시 메모리를 읽고 쓴다.
- 변수를 레지스터에 할당하지 않는다.
- 읽고 쓰는 순서를 지킨다.
[volatile를 사용한 디버깅 디스어셈블리 창]
while (data_ready == false);
00FE1010 cmp byte ptr [data_ready (0FE53F8h)],0
00FE1017 je Consumer (0FE1010h)
cmp부분이 추가 되었다. volatile은 무조건 메모리에서 읽고 무조건 메모리에 쓴다(레지스터 사용안함)
volatile을 만든 목적 memory wrapped I/O
Memory wrapped I/O란, 기계어엔 연산 점프등이 있는데 이것만으로는 대화가 안되기 때문에 I/O 명령어를 만듬. 컴파일러는 I/O장치를 최적화하면 안되고(I/O메모리에 값을 사용해야하는데 쓰지 않을때) 메모리만 최적화 해야하는데 이를 위해 volatile을 만듬.
volatile 사용시 주의해야할점
무엇이 문제일까?
struct Qnode {
volatile int data;
volatile Qnode* next;
};
void ThreadFunc1 ()
{
...
while (qnode->next == NULL) {}
my_data = qnode->next->data;
...
}
while(qnode->next == NULL) {}
- volatile int* a
- *a = 1; // 메모리에 접근한다.
- a = b; // 메모리에 접근하지 않는다.
- int* volatile a;
- *a = 1 // 메모리에 접근하지 않는다.
- a = b // 메모리에 접근한다.
※ 포인터를 사용해야 할 경우를 주의해야한다.
volatile 디스어셈블리 코드 분석
- volatile x
- 자동으로 최적화해주고 있다.
- int* volatile
- mov: eax에 dword ptr [sum] 배열 주소를 복사한다.
- add: 2를 더해준다.
- sub: 1바이트로 stack 1개를 할당한다.
- volatile int*
- mov: eax에 dword ptr [esi+ecx]를 복사한다.
- mov: dword ptr [sum] 배열 주소를 복사한다.
- add: 2를 더해준다.
- mov: dword ptr[esi+ecx]에 eax를 다시 복사한다.
- 하단 내용은 int* volatile과 동일하다.
- pop: 스택공간을 해제하고 esi값을 스택에서 뺀다.
volatile int*는 dword ptr[esi+ecx] 메모리까지 접근하고 있다.
최종정리
- 여러개의 쓰레드가 공유하는 변수는 volatile을 사용해야한다.
- volatile을 사용하면 컴파일러는 프로그래머가 지시한 대로 메모리에 접근한다.
- volatile이 없으면 컴파일러는 싱글쓰레드를 기준으로 프로그램을 최적화한다.
출처
한국산업기술대학교 게임공학부 정내훈 교수님 강의 - 멀티코어 프로그래밍
'멀티코어' 카테고리의 다른 글
[실습1] 베이커리 알고리즘 (0) | 2020.09.21 |
---|---|
멀티쓰레드 프로그램 작성시 주의해야할점 - CPU (0) | 2020.09.17 |
멀티쓰레드 프로그래밍은 어디서 사용되고 있는가? (0) | 2020.09.13 |
멀티쓰레드 프로그래밍으로 덧셈프로그램 만들기 (이외 방법) (0) | 2020.09.13 |
멀티쓰레드 프로그래밍으로 덧셈프로그램 만들기 (0) | 2020.09.08 |