---
title: "경쟁 상태: 올바른 코드 두 개가 함께 실행되면 왜 틀릴까"
author: "Lee"
published_at: "2026-10-18T14:07:03.145Z"
updated_at: "2026-09-30T09:08:05.780Z"
tags: ["컴퓨터공학","운영체제"]
canonical_url: "https://ariser.io/@writer_a1cd615ab19e/%EA%B2%BD%EC%9F%81-%EC%83%81%ED%83%9C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%BD%94%EB%93%9C-%EB%91%90-%EA%B0%9C%EA%B0%80-%ED%95%A8%EA%BB%98-%EC%8B%A4%ED%96%89%EB%90%98%EB%A9%B4-%EC%99%9C-%ED%8B%80%EB%A6%B4%EA%B9%8C"
ai_assisted: false
---

# 경쟁 상태: 올바른 코드 두 개가 함께 실행되면 왜 틀릴까

[원문](https://ariser.io/@writer_a1cd615ab19e/%EA%B2%BD%EC%9F%81-%EC%83%81%ED%83%9C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%BD%94%EB%93%9C-%EB%91%90-%EA%B0%9C%EA%B0%80-%ED%95%A8%EA%BB%98-%EC%8B%A4%ED%96%89%EB%90%98%EB%A9%B4-%EC%99%9C-%ED%8B%80%EB%A6%B4%EA%B9%8C)

각각 혼자 실행할 때는 올바른 작업도 함께 실행하면 예상 밖의 결과를 만들 수 있다. 경쟁 상태는 결과의 정확성이 작업들의 상대적인 실행 순서나 타이밍에 달린 상황이다. 운영체제가 두 실행 흐름을 번갈아 진행하거나 여러 코어가 동시에 진행하는 순서가 달라지면, 같은 입력에도 다른 결과가 나올 수 있다.

![두 스레드가 모두 0을 읽고 각각 1을 써서 증가 두 번의 결과가 1이 되는 실행 순서](/api/v1/media/592d24ac-06dd-4363-8f0d-66d9e3646c1f)

*읽기와 변경을 하나의 보호된 동작으로 묶지 않으면 갱신이 사라질 수 있다.*

## 한 줄이 하나의 동작은 아니다

공유 변수 `counter`가 0이고 두 스레드가 각각 `counter++`를 한다고 하자. 이를 읽기, 1 더하기, 쓰기의 세 단계로 나누면 문제가 보인다. A가 0을 읽고 B도 0을 읽은 뒤, A가 1을 쓰고 B가 1을 쓰면 최종 값은 1이다. 두 번 증가했다는 의도와 달리 한 번의 갱신이 사라진다.

소스 코드 한 줄이라는 사실은 다른 실행 흐름이 사이에 끼어들 수 없다는 보장이 아니다. 컴파일러와 CPU의 실행 순서도 고려해야 한다. C나 C++에서는 동기화되지 않은 충돌 접근이 언어 규칙상 정의되지 않은 동작이 될 수 있어, 위 단계 예제를 실제 프로그램의 모든 가능한 결과라고 해석하면 안 된다. 예제는 갱신 손실의 개념을 보여 주는 모델이다.

## 데이터 경쟁과는 범위가 다르다

데이터 경쟁은 일반적으로 같은 메모리 위치에 대한 동시 충돌 접근이 적절히 동기화되지 않은 조건을 뜻하며, 정확한 정의는 언어 메모리 모델을 따른다. 경쟁 상태는 더 넓은 논리적 개념이다. 각 접근을 개별적으로 원자화했더라도 두 단계 사이의 순서 때문에 프로그램의 약속이 깨질 수 있다.

예를 들어 잔액을 읽는 일과 잔액을 갱신하는 일은 각각 안전하지만, 두 요청이 동시에 충분한 잔액을 확인하고 출금하면 합계가 허용 범위를 넘을 수 있다. 보호해야 하는 것은 읽기 한 번이 아니라 확인과 변경을 포함한 규칙 전체다. [OSTEP의 동시성 버그 장](https://pages.cs.wisc.edu/~remzi/OSTEP/threads-bugs.pdf)은 원자성 위반과 순서 위반이 어떻게 나타나는지 구분한다.

## 합의의 범위를 정하는 것이 해결의 시작

뮤텍스로 확인·변경 구간을 묶거나, 하나의 원자적 읽기수정쓰기 연산을 사용할 수 있다. 자료 소유자를 한 실행 흐름으로 제한하고 다른 작업은 메시지로 요청하게 만드는 방법도 있다. 데이터베이스에서는 트랜잭션과 제약 조건이 비슷한 문제를 다룬다. 어떤 방법이 적절한지는 보호할 불변식과 작업의 경계에 달려 있다.

잠금을 사용해도 공유 자료를 읽는 모든 경로가 같은 규칙을 따르지 않으면 보호가 빠진다. 잠금을 너무 넓게 잡으면 불필요한 작업이 줄을 서고, 너무 작게 나누면 필요한 원자성이 사라진다. 따라서 먼저 반드시 함께 유지돼야 하는 상태의 관계를 적고, 그 관계를 바꾸는 경로를 모두 찾아야 한다.

경쟁 상태는 실행마다 재현되지 않아 찾기 어렵다. 로그를 추가하면서 타이밍이 바뀌어 증상이 사라지기도 한다. 특정 인터리빙을 강제로 재현하는 실험과 동적 분석 도구는 도움이 되지만, 한 번 오류가 없었다고 안전이 증명되는 것은 아니다. 순서를 우연에 맡기는 부분을 명확히 하고, 프로그램이 모든 허용 순서에서 규칙을 지키는지 설명할 수 있어야 한다.

### 참고 자료

- [OSTEP — Concurrency Bugs](https://pages.cs.wisc.edu/~remzi/OSTEP/threads-bugs.pdf)
