2020 이전/C++

거부할 수 없는 너의 흑마법은 멀티스레딩

이상해C++ 2019. 8. 5. 00:04

 

샤이니 - 루시퍼

현대 K-POP 에서 멀티스레딩할때(=약간 괴롭고힘들때) 듣기 좋은 노래 샤이니 루시퍼랑 함수 일렉트릭 쇼크

 

내용물은 스레드 2개로 1억까지 더하기 

#include <windows.h>

using namespace std;

volatile int sum = 0;
CRITICAL_SECTION cs;

DWORD _stdcall ThreadFunc(LPVOID lpVoid)
{
	for (int i = 1; i <= 25000000; ++i)
	{
		EnterCriticalSection(&cs);
		sum += 2;
		LeaveCriticalSection(&cs);
	}
	return 0;
}

DWORD _stdcall noCSThreadFunc(LPVOID lpVoid)
{
	for (int i = 1; i <= 25000000; ++i)	sum += 2;
	return 0;
}

int main()
{
	InitializeCriticalSection(&cs);
	//unsinged long = DWORD
	DWORD addr;
	
	DWORD tStart = GetTickCount();

	HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr);
	HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr);

	WaitForSingleObject(hThread2, INFINITE);
	WaitForSingleObject(hThread3, INFINITE);
	
	DWORD tEnd = GetTickCount();

	printf_s("Time with critical section : %lu\n", tEnd - tStart);
	printf_s("Result is %d\n", sum);

	CloseHandle(hThread2);
	CloseHandle(hThread3);

	tStart = GetTickCount();

	hThread2 = CreateThread(NULL, 0, noCSThreadFunc, NULL, 0, &addr);
	hThread3 = CreateThread(NULL, 0, noCSThreadFunc, NULL, 0, &addr);

	WaitForSingleObject(hThread2, INFINITE);
	WaitForSingleObject(hThread3, INFINITE);

	tEnd = GetTickCount();

	printf_s("Time with no critical section : %lu\n", tEnd - tStart);
	printf_s("Result is %d\n", sum);
	
	CloseHandle(hThread2);
	CloseHandle(hThread3);
	
	getchar();
}

결과:

디버그
릴리즈

락 거는것과 안거는것과 차이는 20~50배 난다<<쓸 이유가 없다는 소리

Atomic

lock을 쓰지않으면서 sum+=2를 실행할 때는 하나의 스레드만 접근하도록

DWORD _stdcall asmThreadFunc(LPVOID lpVoid)
{
	for (int i = 1; i <= 25000000; ++i)
	{
		_asm _emit 0xf0
		_asm add sum, 2
	}
	return 0;
}

릴리즈

명령어 자체를 수행할때 해당 명령어를 atomic하게 만드는 방법이다. lock을 걸면 실행 중간에 다른 스레드가 실행되고,  락을 받아올때까지 기다리게되지만 Interlock Opertaion을 쓰면 해당 명령어가 완수될때까지 CPU가 다른 프로세서를 hold하고 해당 프로세만 실행하는 방법으로 원자성을 보장한다. 

Volatile

Volatile은 컴파일러의 최적화를 막는 키워드이다. VS나 gcc도 똑같이 컴파일러 최적화가 들어가기 때문에 무한루프에 빠질 수 있다. i=10까지 점차적으로 더하는 코드의 어셈블리 코드를 까면 명령어로 퉁치는것을 알 수있다. 

 

이런식으로 최적화를 해준다. 

volatile  키워드를 쓰면 최적화를 하지 않고 제대로 생성해준다. 

volatile 키워드를 쓰지 않은채 멀티스레드 코드를 돌려보면?

bool flag = false;
int data;
int my_data;

DWORD _stdcall volatile1(LPVOID lpVoid)
{
	data = 1;
	flag = true;
	return 0;
}

DWORD _stdcall volatile2(LPVOID lpVoid)
{
	while (!flag) { ; }
	my_data = data;
	return 0;
}

int main(void)
{
	DWORD addr;

	HANDLE hThread1 = CreateThread(NULL, 0, volatile1, NULL, 0, &addr);
	HANDLE hThread2 = CreateThread(NULL, 0, volatile2, NULL, 0, &addr);
	
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);

	CloseHandle(hThread1);
	CloseHandle(hThread2);
}

실제로 돌려보면 3번에 한번 꼴로 무한루프에 빠진다. 왜?

 

volatile2 함수의 내부 구문을 보면 한번 테스트에 실패를 하면 movzx eax,~~ flag 구문으로 가는것이 아니라 test al, al쪽으로 점프하기 때문에 계속 무한루프를 돌게 된다.  volatile bool flag = false 로 선언하면 해결된다.

volatile을 쓰면 반드시 순서를 지키기 때문에 무한루프에 빠지지않는다.

volatile은 싱글 스레드환경에서 컴파일러 최적화를 막아주지만 멀티 스레드 환경에서 memory barrier의 기능을 수행한다는 소리가 아니므로 주의한다. 

 

 

참고:

멀티스레딩

https://www.slideshare.net/deview/242-naver2?ref=https://deview.kr/2013/detail.nhn?topicSeq=64

인라인 어셈블리

https://wiki.kldp.org/KoreanDoc/html/EmbeddedKernel-KLDP/app3.basic.html 

Volatile

https://ko.wikipedia.org/wiki/Volatile_%EB%B3%80%EC%88%98

https://skyul.tistory.com/337

Volatile은 메모리 베리어가 아니다

https://kldp.org/node/112532

https://jacking.tistory.com/1026

 

'2020 이전 > C++' 카테고리의 다른 글

vector대 list  (0) 2019.10.11
CPU도 구라를 치고 C++도 구라를 친다  (0) 2019.08.08
해시 버켓 사이즈 비교 실험  (0) 2019.07.06
C++의 까다롭고 유별난 친구들2  (0) 2019.03.22
C++의 까다롭고 유별난 친구들  (0) 2019.03.13