안녕하세요. 이번 시간에는 race condition과 관련이 깊은 toctou에 대해서 알아보려고 합니다. 검사 시점과 사용 시점을 주의깊게 보시면 됩니다.

 


 예제 프로그램을 하나 보겠습니다.

 

 유저 객체 하나가 있습니다. Task는 유저 객체 하나를 받습니다. 그리고 Thread에 유저 객체 u가 들어간 task를 넘겨줌을 알 수 있어요. Thread를 다 돌리고, 유저의 포인트가 0보다 작아지면 u.getPoint()의 값을 출력하고 break를 걸어요. 여기서 질문. 0보다 작아지는 경우가 생길 수 있을까요?

 

 

 Task는 간단한 일을 수행해요. 단지, run에서 7포인트를 소비할 뿐이에요.

 

 

 consumePoint는 현재 포인트가 a보다 클 때, 현재 포인트에서 a를 빼게 됩니다. 사실, 우리는 결과를 이미 알고 있어요. 스레드 안전하지 않아서 뒤죽 박죽 결과가 나올 거라고요. 문제는, 0보다 작은 경우가 나올까? 라는 건데요. 8번째 줄에 long time execution 이라는 주석을 적어 놓았어요.

 

 그리고, 100만번 for loop를 돌리는 상대적으로 조금 오래 걸리는 로직을 추가했습니다.

 

 

 그랬더니 -23이 나왔어요. 스레드 안전하지 않다는 것은 알겠는데, 이건 또 무슨 일일까요?

 

 


 다시, consumePoint로 돌아와 보겠습니다.

 

 

 여기서, 코드를 2개의 부분으로 나눌 수 있어요. this.point가 a보다 크거나 같은 검사 로직. 그리고, if 블록 안쪽에서 point를 감소시키는 사용 로직. 현재 point가 a보다 크거나 같은 조건이 통과한 시점과, point를 사용한 사용 시점으로 나눌 수 있어요.

 

 

 그리고 둘 이상의 쓰레드가 공유 객체인 u에 접근할 수 있는 상황이에요. 스택은 별개의 영역에서 관리가 되지만, 힙에 있는 공유 객체인 u는 여러 개의 쓰레드가 모두 바라볼 수도 있고, 바꿀 수도 있는 상황입니다. 현재 User의 p 값은 13이에요.

 

 

 Thread1과 Thread2가 이렇게 실행이 될 수도 있습니다. p가 소비 포인트인 7보다 크거나 같은가? 라는 조건을 Thread1이 실행하고 또 Thread2가 실행한 상황입니다. 이 때, p의 값은 13이였습니다.

 

 

 두 쓰레드 다 p가 7보다 크거나 같은지 검사하는 시점에서 p의 값은 13이였습니다. 그런데

 

 

 thread 1이 포인트를 7을 사용하고 난 후에는, point가 13에서 6으로 줄었습니다.

 

 

 그런데, Thread2도 이미 검사 조건은 이미 통과한 상황이였습니다. 검사했을 때 p의 값은 13이였기 때문입니다. 실제 포인트를 사용한 시점에서 p의 값은 13이 아닌 6이였다는 것이 문제지만요.

 

 

 그래서 p = p-7이 수행되게 되고, -1 포인트가 나오게 됩니다. 2번째 쓰레드 입장에서 보면, 검사한 시점에서는 13 포인트였는데, 사용한 시점에서는 7포인트가 줄어버린 6포인트였습니다. 그런데, 이미 검사는 통과해 버린 후였기 때문에, valid하지 않은 p = p-7이 수행이 되었고, 결과적으로는 예기치 않은 -1이 나오게 되었습니다.