num이라는 정보를 보관하는 데 필요한 것은 정수형 4바이트 데이터 공간 하나뿐입니다. 하지만 Array<int> primes 정보를 보관하는 데 필요한 데이터는 하나 이상입니다. 우선 Array<int>는 배열 객체를 가리키는 포인터 변수와 배열이 크기를 멤버 변수로 가질 것입니다. 배열 객체는 메모리 힙에서 할당되었을 것이고, 메모리 힙은 현재 실행 중인 프로세스의 런타임 라이브러리로 다루어집니다.
▲ 그림 1-23 Array<int>의 데이터 구조
각 스레드는 소수를 찾아내면 Array<int> 객체에 Add() 함수를 사용하여 배열 맨 뒤를 채웁니다. 기존에 가진 배열 객체에 더 이상 넣을 공간이 없으면, 메모리를 재할당합니다. C 언어의 런타임 같은 경우 메모리를 재할당한 후 메모리를 가리키는 주소가 달라지는 경우가 종종 있습니다. 그리고 메모리 재할당 여부와 관계없이 배열 크기를 의미하는 변수 값도 1 증가할 것입니다.
두 스레드가 동시에 Array<int>의 Add() 함수를 호출하면 여러 스레드가 Array<int> 변수들을 변경합니다. 그러면 두 변수 중 하나만 변경된 상태에서 다른 스레드가 그대로 배열을 액세스하겠지요. 이 과정에서 배열을 가리키는 포인터 변수는 엉뚱한 값, 예를 들어 이미 힙에서 해제된 메모리를 잠시 가리킬 수도 있습니다. 충돌이 발생하는 이유는 바로 이 때문입니다. 이 역시 데이터 레이스 현상 중 하나입니다.
따라서 Array<int>를 스레드가 액세스할 때는 Array<int> 안의 두 멤버 변수를 모두 바꾸든지, 아니면 하나도 바꾸지 않든지 해야 합니다. 즉, 두 멤버 변수를 건드리는 동안에는 다른 스레드가 절대 건드리지 못하게 해야 합니다. 이를 원자성(atomicity)이라고 합니다. 그래야 Array<int>의 두 변수는 항상 일관성 있는 상태를 유지할 수 있습니다. 이를 일관성(consistency)이라고 합니다.
멀티스레드 프로그래밍을 하다 보면 이렇게 원자성과 일관성을 유지하는 특수한 조치를 해야 할 때가 있습니다. 이러한 조치들을 통칭하여 동기화(synchronize)라고 하며, 대표적인 것이 임계 영역과 뮤텍스(또는 상호 배제라고도 함), 잠금(lock, 락) 기법입니다.