volatile 키워드는 변수 등이 컴파일러에 의한 최적화에 의한 영향을 받지 않도록 하는 역할을 한다.
일반적인 초급 프로그래밍에서는 거의 쓸 일이 없지만, 후에 concurrent한 프로그램을 작성한다든지, 시스템의 로우
레벨을 건드리게 되는 경우에는 지나치게 똑똑한 컴파일러에 의한 예상치 못한 최적화로 인하여 곤란에 빠질 수가 있다.
예제를 위해 다음의 두 함수를 만들어 보았다.
[CODE] 쓰레드로 돌릴 두 함수
int z;
// z를 찍기만 하는 쓰레드
DWORD WINAPI thread_a( LPVOID )
{
int i ;
for( i = 0; i < 10; ++i )
{
printf( "%d\n", z );
Sleep(1);
}
return 1;
}
// z를 증감시키기만 하는 쓰레드
DWORD WINAPI thread_b( LPVOID )
{
int i, j;
for( i = 0; i < 10; ++i )
{
for( j = 0; j < 100000; ++j ) ++z;
Sleep(1);
for( j = 0; j < 100000; ++j ) --z;
}
return 1;
}
위 코드에서 int z를 전역으로 선언하였다는 사실과, 쓰레드는 전역변수를 공유한다는 사실을 가슴 속에 새겨놓도록 하자. 그리고 아래는 각각의 쓰레드를 돌릴 main() 함수이다.
[CODE] 쓰레드를 돌려보자꾸나!
int main ( void )
{
DWORD id[2];
HANDLE h[2];
h[0] = CreateThread( NULL, 0, thread_a, 0, 0, &id[0] );
h[1] = CreateThread( NULL, 0, thread_b, 0, 0, &id[1] );
WaitForMultipleObjects( 2, h, TRUE, INFINITE );
CloseHandle( h[0] );
CloseHandle( h[1] );
return 0;
}
위 코드를 실행시키면 쓰레드를 concurrent하게 돌아간다. (물론 엄밀히 말하면 스위칭이 일어나기 전까지는
sequential 하지만 세세한건 일단 논외로 하자) 그럼 결과는 어떻게 되어야 할까? 한 번 Release 모드로 실행을
시켜보도록 하자.
z를 찍는 쓰레드와 z를 증감시키는 쓰레드를 동시에 실행시켰으니 z값이 변화하면서 찍혀야 할 것이다. 그러나....
OUTPUT
0
100000
100000
100000
100000
100000
100000
100000
100000
100000
이게 왠일이란 말인가! thread_b(증감시키는 쓰레드)가 for문을 마칠때까지 스위칭이 일어나지 않은 것은 아닐까?를
비롯해 여러가지 의문이 들겠지만 디버거로 실행파일을 열어다보면 우리의 컴파일러가 놀라운 최적화를 한 것을 알 수 있다.
[CODE] 최적화 후의 코드를 가상적으로 복원해 봤을 경우의 모습
DWORD WINAPI thread_b( LPVOID )
{
int i, j;
for( i = 0; i < 10; ++i )
{
z += 100000;
Sleep(1);
z -= 100000;
}
return 1;
}
우리의 무시무시한 컴파일러는 z를 증감시키는 for()문이 공회전한다는 사실을 알아채고는 위와 같이 한방에 z값을 변화시키는
놀라운 최적화를 이룩해내었다. ㅅㅂ.. . 그럼 내가 원하던 대로 하려면 어떻게 해야 되냐.. 거기에 대한 해답이 바로
volatile인 것이다. 이녀석을 z를 선언할때 사용하면 컴파일러가 z에 관련된 최적화를 하지 않게 되어 문제가 해결이 된다.
(물론 스위칭이 일어나는 시점의 대부분이 sleep(1);부분이 되어 만족스런 결과가 나오지 않을 때도 있다. 하지만 비록
결과가 같게 나온다고 하더라도 volatile과 함께라면 z는 for문을 돌며 변화하게 된다는 사실을 명심하자.)
[CODE] 새로운 z의 선언
int z;
volatile int z; 또는 int volatile z;
OUTPUT
1661
87844
95067
67811
100000
72587
63865
77272
92885
댓글 영역