이 글은 @RanolP_777 님의 트윗을 기본 레퍼런스로 하여 작성되었습니다.
멀티쓰레드 등을 활용한 동시성 프로그래밍을 하다 보면, 데이터 레이스(Data Race, 데이터 경쟁)에 관해서 접하게 됩니다. 흔히 여러 쓰레드가 동시에 한 메모리 영역에 접근할 때 읽기/쓰기 순서가 꼬여서 데이터가 뒤죽박죽이 되는 걸 보고 데이터 레이스라고 하고, 대충 그러니까 우리는 공유 변수에 접근할 때는 뮤텍스를 써야 한다! 정도로만 이야기하고 넘어가는 게 일상이죠. 그런데 과연 저것이 데이터 레이스의 정확한 정의일까요? 또 종종 듣기로는 레이스 컨디션(Race Condition, 경쟁 상태)이라는 것도 있는 것 같던데, 대충 둘 다 레이스 들어가니 비슷한 뜻인 걸까요? 이번 글에서는 사람들이 자주 헷갈려하는 이 두 개념에 대해 다뤄 보도록 하겠습니다.
먼저 레이스 컨디션의 정의부터 살펴보겠습니다. 레이스 컨디션은 현재 작업이 제어할 수 없는 또다른 작업과의 진행 순서, 즉 타이밍에 따라 결과가 달라져 여러 결과를 만들어낼 수 있는 바람직하지 않은 상황을 포괄적으로 일컫는 단어입니다. 언뜻 읽어보기엔 위에서 간단하게 이야기한 데이터 레이스의 정의와 유사해 보이기도 하지만, 조금만 더 생각해 보면 단순히 데이터 접근 이외에도 순서대로 값을 출력해야 하는 경우 등, 순서가 실행 결과에 영향을 미치는 수많은 경우에 레이스 컨디션의 정의에 부합하는 상황들이 발생할 수 있다는 것을 알 수 있습니다.
데이터 레이스의 엄밀한 정의도 같이 알아보겠습니다. 데이터 레이스는 다른 곳에서 읽을 가능성이 있는 어떤 메모리 위치에 쓰기 작업을 하는 것, 특히 그것이 위험한 상황을 의미합니다. 데이터 레이스는 일반적으로 레이스 컨디션의 부분집합이지만, 종종 그렇지 않은 경우도 발생합니다. 데이터 레이스의 정의에는 쓰레드라던가 읽기/쓰기 순서라던가 하는 내용은 일체 들어가 있지 않은데, 이는 종종 데이터 레이스이지만 레이스 컨디션이 아닌 상황을 발생시킵니다. 이에 대하여 좀 더 자세히 알아보겠습니다.
위에서도 언급하였지만, 일반적인 경우에 데이터 레이스는 레이스 컨디션의 부분집합입니다. 하지만 그렇지 않은 경우 역시 존재한다고 말씀드렸는데요, 몇 가지 상황을 이야기해보면서 각각의 상황이 레이스 컨디션이나 데이터 레이스에 해당되는지 알아보도록 하겠습니다.
송금(송금액, 출금_계좌, 입금_계좌) {
if (출금_계좌.잔액 < 송금액) return false
입금_계좌.잔액 += 송금액
출금_계좌.잔액 -= 송금액
return true
}
(위 코드는 이해를 돕기 위해 가상의 언어로 작성되었습니다)
위 코드는 쓰레드 간 동기화를 위해 아무 수단도 사용하지 않았으며, 따라서 싱글 쓰레드 환경에서는 잘 작동하지만, 멀티 쓰레드 환경에서는 데이터 레이스(여러 쓰레드가 동시에 한 계좌의 잔액을 업데이트하려 할 수 있음)와 레이스 컨디션(병렬 실행 시 계산 과정 중간에 돈이 사라질 수 있음)에 모두 해당하게 됩니다.
송금2(송금액, 출금_계좌, 입금_계좌) {
동기화됨 {
출금_계좌_잔액 = 출금_계좌.잔액
}
if (출금_계좌_잔액 < 송금액) return false
동기화됨 {
입금_계좌.잔액 += 송금액
}
동기화됨 {
출금_계좌.잔액 -= 송금액
}
return true
}
(동기화됨
이라고 표시된 부분은 Mutex 등의 락을 이용하거나, 아토믹 연산을 이용하는 등의 방법으로 데이터 레이스를 차단했음을 나타내는 부분입니다)
이 경우에는 개별 변수에 대한 읽고 쓰는 동작을 동기화 영역에 격리시켰기 때문에 데이터 레이스는 발생하지 않지만, 쓰레드별 연산의 실행 순서에 따라 실제 가능한 것 이상의 금액이 출금될 수 있으므로(출금 계좌에서 돈을 빼내기 전에 잔액을 체크한다던가) 여전히 레이스 컨디션의 정의에는 부합하게 됩니다.
송금3(송금액, 출금_계좌, 입금_계좌) {
동기화됨 {
if (출금_계좌.잔액 < 송금액) return false
입금_계좌.잔액 += 송금액
출금_계좌.잔액 -= 송금액
return true
}
}
데이터 레이스와 레이스 컨디션이 모두 존재하지 않는 예제로, 가장 권장되는 방식의 코드입니다. 아마 일반적으로 Mutex 등의 사용법을 배우실 때 위와 같이 사용하라고 배우셨을 것이기 때문에, 가장 익숙하신 방식일 것이라고 생각합니다.
송금4(송금액, 출금_계좌, 입금_계좌) {
출금_계좌.사용됨 = true
입금_계좌.사용됨 = true
동기화됨 {
if (출금_계좌.잔액 < 송금액) return false
입금_계좌.잔액 += 송금액
출금_계좌.잔액 -= 송금액
return true
}
}
위 상황은 여러 쓰레드에서 동시에 한 변수에 쓰기 시도를 할 수 있기 때문에 엄연히 데이터 레이스의 정의에 부합하지만, 연산이 어떤 순서로 실행되어도 결과는 같기 때문에 (두 계좌 모두 사용된 것으로 표시됨) 레이스 컨디션의 정의에는 부합하지 않습니다.
정리하자면, 사실 마지막 경우는 약간 말장난 같아 보이기도 하지만 데이터 레이스와 레이스 컨디션은 엄연히 별개의 개념이고, 하나가 없더라도 나머지 하나가 얼마든지 나타날 수 있음을 알 수 있습니다. 만약 글 내용에 대하여 질문이 있으시거나 잘못된 부분을 발견하셨다면, 댓글을 달아주시면 성심성의껏 답해 드리도록 하겠습니다.