사전 happens-before
개념

happens-before

gabury1고친 사람 github-actions[bot]

happens-before 는 두 일 가운데 어느 쪽이 먼저인지를 시계 없이 정해 줍니다. 기준은 시각이 아니라 한쪽이 다른 쪽에 영향을 줄 수 있었느냐입니다. 분산 시스템에서는 서버 사이의 사건 순서를 이것으로 따집니다. 멀티스레드 프로그램에서는 한 스레드가 쓴 값이 다른 스레드에 보이는지를 이것으로 따집니다.

쉽고 빠른 이해

happens-before 는 「앞의 일이 남긴 결과를 뒤의 일이 반드시 본다」는 보장을 세웁니다. 스레드 A 가 락 안에서 값을 고친 뒤 락을 놓습니다. 다음에 그 락을 잡은 스레드 B 는 고친 값을 반드시 봅니다.

이 보장이 없으면 다른 스레드가 쓴 값이 언제 보일지 모릅니다. 컴파일러와 프로세서가 빨리 돌려고 읽기와 쓰기의 순서를 바꾸기 때문입니다.

서버 사이에서는 기계마다 시계가 어긋나 있습니다. 그래서 시각으로 순서를 매기면 원인과 결과가 뒤집힙니다.

  1. 한 스레드나 한 서버 안에서는 먼저 한 일이 앞입니다
  2. 한쪽이 넘기고 다른 쪽이 받으면 넘긴 일이 앞입니다. 락을 놓고 잡는 것, 메시지를 보내고 받는 것이 그렇습니다
  3. 앞뒤가 이어지면 처음과 끝도 앞뒤입니다

대가는 동기화나 메시지를 일부러 넣어야 한다는 것입니다. 락은 다른 스레드를 기다리게 합니다. 순서를 지키게 하면 컴파일러와 프로세서가 순서를 바꿔 얻던 최적화가 막힙니다.

happens-before 는 락 없이 여러 스레드가 같은 변수를 읽고 쓸 때 따집니다. 여러 서버의 기록에서 무엇이 원인이었는지 가릴 때도 따집니다. 한 스레드 안에서만 도는 코드는 따지지 않아도 됩니다.

상세

회의에서 바뀐 일정을 팀원에게 쪽지로 알린다고 해 봅시다. 쪽지를 읽은 팀원은 새 일정대로 움직입니다. 쪽지를 받기 전에 잡은 약속은 새 일정을 모른 채 잡은 약속입니다.

이 절은 두 장면으로 happens-before 를 봅니다. 하나는 스레드 둘이 변수를 함께 쓰는 짧은 자바 코드입니다. 다른 하나는 서버 둘이 메시지를 주고받는 장면입니다. 먼저 두 장면에 공통인 규칙을 세웁니다. 그다음 장면마다 그 규칙이 무엇을 막아 주는지 봅니다.

영향을 줄 수 있었다는 관계

happens-before 는 규칙 셋으로 생깁니다. 규칙을 적기 전에 낱말 둘을 정합니다.

실행 흐름은 명령을 하나씩 차례로 처리하는 주체입니다. 한 프로그램 안의 스레드가 실행 흐름입니다. 분산 시스템의 서버 한 대도 실행 흐름입니다.

사건은 실행 흐름이 겪는 일 하나입니다. 변수에 값을 쓰는 것, 락을 잡는 것, 메시지를 한 통 보내는 것이 각각 사건 하나입니다.

사건 a 가 사건 b 보다 happens-before 라는 말은 a 가 b 에 영향을 줄 수 있었다는 뜻입니다. 이 문서에서 「a 가 b 보다 앞」은 a happens-before b 를 줄인 말입니다. 시각을 말할 때는 「앞」 대신 「이르다」·「늦다」를 씁니다.

이 관계는 아래 세 규칙으로만 생깁니다.

  1. 같은 실행 흐름 안에서는 먼저 처리한 사건이 앞입니다
  2. 한 흐름이 무언가를 넘기고 다른 흐름이 그것을 받으면, 넘긴 사건이 받은 사건보다 앞입니다
  3. a 가 b 보다 앞이고 b 가 c 보다 앞이면, a 는 c 보다 앞입니다

규칙 2 에서 무엇을 넘기고 받는지는 장면마다 다릅니다. 첫머리의 쪽지가 그 예입니다. 서버 사이에서는 메시지를 보내고 받는 것입니다. 스레드 사이에서는 락을 놓고 잡는 것 같은 동기화 동작입니다.

동시라는 말의 뜻

세 규칙을 어떻게 이어 붙여도 어느 쪽으로도 닿지 않는 두 사건이 있습니다. 이런 두 사건을 동시라고 부릅니다. 서로에게 영향을 줄 수 없었던 사이라는 뜻입니다.

쪽지를 받기 전에 팀원이 잡은 약속과 회의에서 바꾼 일정이 동시입니다. 둘 사이를 잇는 쪽지가 아직 없었기 때문입니다.

동시는 같은 시각에 일어났다는 뜻이 아닙니다. 벽시계로 몇 초 차이가 나도 둘 사이를 잇는 메시지나 동기화가 없으면 동시입니다. 거꾸로 시각이 이르다는 것만으로는 happens-before 가 되지 않습니다.

부분 순서는 모든 쌍이 아니라 일부 쌍에만 앞뒤가 정해지는 순서입니다. 동시인 쌍이 남으므로 happens-before 는 부분 순서입니다.

모든 쌍에 앞뒤를 정하는 순서는 전체 순서라고 합니다. happens-before 는 동시인 쌍을 남긴다는 점에서 전체 순서와 다릅니다.

스레드 사이에서 쓴 값이 보이는지

재료는 스레드 둘이 변수 둘을 함께 쓰는 짧은 자바 코드입니다. 먼저 happens-before 가 없을 때 무엇이 깨지는지 봅니다. 그다음 낱말 하나를 더해 고칩니다.

한 스레드가 변수에 쓴 값이 다른 스레드에 언제 보일지는 저절로 정해지지 않습니다. 컴파일러와 프로세서는 빨리 돌려고 읽기와 쓰기의 순서를 바꿉니다. 이것을 명령어 재배치라고 합니다.

한 스레드 안에서는 이 재배치가 드러나지 않습니다. 결과가 적은 순서대로 한 것과 같게 나오는 범위에서만 바꾸기 때문입니다. 재배치가 드러나는 것은 다른 스레드가 볼 때입니다.

아래 코드에서 스레드 A 는 값을 준비한 뒤 준비됐다는 표시를 켭니다. 스레드 B 는 표시가 켜진 것을 보고 값을 읽습니다.

Java
int data = 0;
boolean ready = false;

// 스레드 A
data = 42;
ready = true;

// 스레드 B
if (ready) {
    int r = data;  // 42 또는 0
}

B 가 ready 를 true 로 읽었는데도 data 는 0 일 수 있습니다. A 의 두 쓰기가 B 에게는 바뀐 순서로 보였을 수 있기 때문입니다.

규칙으로 따지면 까닭이 보입니다. A 안에서는 규칙 1 로 data 쓰기가 ready 쓰기보다 앞입니다. 그런데 A 의 ready 쓰기와 B 의 ready 읽기 사이에는 동기화가 없습니다.

그래서 A 의 data 쓰기와 B 의 data 읽기는 동시입니다. B 가 data 를 42 로 읽을지 0 으로 읽을지는 보장되지 않습니다.

같은 변수를 두 스레드가 건드리고 적어도 하나가 쓰기라고 합시다. 이때 둘 사이에 happens-before 가 없는 상태를 데이터 경쟁이라고 합니다. 데이터 경쟁이 있는 코드는 돌 때마다 다른 값을 낼 수 있습니다.

volatile 로 관계를 세우기

고치는 법은 ready 선언에 volatile 을 붙이는 것입니다. volatile 은 여러 스레드가 함께 보는 변수에 붙이는 자바 키워드입니다.

Java
volatile boolean ready = false;

volatile 변수에 쓴 사건은 그 값을 읽어 간 사건보다 앞입니다. 스레드 사이에 규칙 2 의 관계가 생긴 것입니다. 아래 그림에서 스레드 안의 화살표는 규칙 1 입니다. 스레드를 건너는 화살표는 규칙 2 입니다.

flowchart TD
    subgraph TA["스레드 A"]
        A1["data = 42"] --> A2["ready = true"]
    end
    subgraph TB["스레드 B"]
        B1["ready 읽기 · true"] --> B2["data 읽기"]
    end
    A2 -->|"volatile 쓰기 → 읽기"| B1

화살표를 따라가면 A 의 data = 42 에서 B 의 data 읽기까지 닿습니다. 규칙 3 으로 둘은 앞뒤가 되므로, ready 를 true 로 읽은 B 는 data 를 늘 42 로 읽습니다.

happens-before 는 그 순서대로 실행하라는 명령이 아닙니다. 결과가 그 순서대로 한 것과 같게만 나오면 컴파일러와 프로세서는 여전히 순서를 바꿀 수 있습니다. 막히는 것은 다른 스레드가 보는 결과를 바꾸는 재배치뿐입니다. 보장하는 것은 실행 순서가 아니라 보이는 결과입니다.

스레드 사이에 관계를 만드는 동작

volatile 말고도 규칙 2 를 세우는 동작이 여럿 있습니다. 자바에서 흔히 쓰는 것을 표로 모읍니다.

앞 사건 뒤 사건
락을 놓기 다음에 같은 락을 잡기
volatile 변수에 쓰기 그 값을 읽기
Thread.start() 로 스레드를 시작하기 시작된 스레드의 첫 동작
스레드의 마지막 동작 다른 스레드가 join() 으로 그 스레드가 끝난 것을 확인하기

표의 첫 줄 덕분에 synchronized 블록 안에서 고친 필드는 다음에 같은 락을 잡은 스레드가 최신 값으로 읽습니다. 락은 상호 배제로 한 번에 한 스레드만 들어오게 막습니다. 그와 함께 고친 값이 다음 스레드에 보이게도 합니다.

표의 네 줄처럼 스레드 사이에 규칙 2 를 세우는 관계를 따로 synchronizes-with 라고 부릅니다. happens-before 는 이 관계와 규칙 1 을 규칙 3 으로 이어 붙여 만들어집니다.

서버 사이에서 사건의 앞뒤

서버 A 가 서버 B 에 요청을 보냅니다. B 는 그 요청을 받아 처리합니다. happens-before 는 원래 레슬리 램포트가 이런 장면의 사건 순서를 따지려고 happened before 라는 이름으로 세운 관계입니다.

서버 사이에서 규칙 2 를 세우는 것은 메시지입니다. A 가 요청을 보낸 사건은 B 가 그 요청을 받은 사건보다 앞입니다.

시각으로 순서를 매기면 안 되는 까닭은 기계마다 시계가 조금씩 어긋나 있어서입니다. 이 어긋남을 클록 스큐라고 합니다.

예를 들어 서버 A 가 자기 시계로 10:00:05 에 요청을 보냅니다. B 의 시계는 A 보다 3초 느립니다. 요청이 곧바로 도착하면 B 는 받은 시각을 10:00:02 로 남깁니다. 받은 사건이 보낸 사건보다 이른 시각으로 기록됩니다.

happens-before 는 시계 대신 메시지를 따라갑니다. 그래서 시계가 어긋나도 원인과 결과가 뒤집히지 않습니다.

이 관계를 사건마다 번호로 적어 두는 방법이 논리 시계입니다. 대표적인 것이 램포트 시계와 벡터 시계 둘입니다.

램포트 시계는 사건마다 번호 하나를 붙입니다. a 가 b 보다 앞이면 a 의 번호가 b 의 번호보다 작습니다. 그러나 번호만 보고는 두 사건이 앞뒤인지 동시인지 가리지 못합니다.

벡터 시계는 서버마다 칸을 하나씩 둔 번호 묶음을 붙입니다. 두 묶음을 칸마다 견주면 앞뒤인지 동시인지 가릴 수 있습니다.

관계를 세우는 비용

happens-before 는 공짜로 생기지 않습니다. 스레드 사이에서는 락이나 volatile 같은 동기화를 일부러 넣어야 생깁니다.

동기화에는 비용이 따릅니다. 락은 다른 스레드를 기다리게 합니다. volatile 은 그 앞뒤에서 다른 스레드가 보는 결과를 바꿀 수 있는 재배치를 못 하게 합니다. 순서를 바꿔 얻던 최적화가 그만큼 막힙니다.

서버 사이에서는 번호를 메시지마다 실어 보내야 합니다. 벡터 시계처럼 동시인지까지 가리려면 서버 수만큼 칸을 든 번호를 실어야 합니다. 그만큼 메시지가 커집니다.

따져야 하는 경우와 아닌 경우

여러 스레드가 락 없이 같은 변수를 읽고 쓰는 코드에서는 따져야 합니다. 플래그 하나로 다른 스레드에 「준비됐다」는 신호를 보내는 ready 코드가 그렇습니다. 서버 여러 대의 기록을 모아 무엇이 무엇의 원인이었는지 가릴 때도 따집니다.

한 스레드 안에서만 도는 코드는 따질 필요가 없습니다. 규칙 1 이 저절로 앞뒤를 정해 줍니다.

스레드끼리 변수를 나누지 않으면 따질 일이 없습니다. 나누더라도 공유 변수를 늘 같은 락 안에서만 읽고 쓰면 락이 관계를 세워 줍니다.

관련 항목

happens-before 가 속하는 상위 분류

메모리 모델 · 동시성 · 분산 시스템 · 부분 순서 · 인과 관계

happens-before 를 세우는 동기화 수단

락 · 뮤텍스 · 상호 배제 · volatile · synchronizes-with · 원자적 연산 · 메모리 배리어 · 세마포어

happens-before 가 없을 때 나는 오류와 그 원인

데이터 경쟁 · 경쟁 상태 · 가시성 · 명령어 재배치 · CPU 캐시 · 이중 검사 잠금

happens-before 를 번호로 적는 논리 시계

논리 시계 · 램포트 시계 · 벡터 시계 · 하이브리드 논리 시계 · 버전 벡터

happens-before 가 대신하는 시각 기준

벽시계 · 타임스탬프 · 클록 스큐 · NTP

happens-before 와 맞세워지는 순서 모델

전체 순서 · 프로그램 순서 · 순차 일관성 · 선형화 가능성 · 인과 일관성

다른 이름: happened-before · happens before