Multi-threaded executions and data races (since C++11)
실행 스레드 는 특정 최상위 함수의 호출로 시작하여( std::thread , std::async , std::jthread (C++20부터) 또는 다른 수단으로), 그리고 스레드에 의해 이후에 실행되는 모든 함수 호출을 재귀적으로 포함하는 프로그램 내의 제어 흐름입니다.
- 하나의 스레드가 다른 스레드를 생성할 때, 새 스레드의 최상위 함수에 대한 초기 호출은 생성하는 스레드가 아닌 새 스레드에 의해 실행됩니다.
모든 스레드는 프로그램 내의 어떤 객체와 함수에도 잠재적으로 접근할 수 있습니다:
- 자동 저장 기간과 스레드 지역 storage duration 을 가진 객체들은 여전히 다른 스레드에 의해 포인터나 참조를 통해 접근될 수 있습니다.
- hosted implementation 에서는 C++ 프로그램이 동시에 실행되는 여러 스레드를 가질 수 있습니다. 각 스레드의 실행은 이 페이지의 나머지 부분에 정의된 대로 진행됩니다. 전체 프로그램의 실행은 모든 스레드들의 실행으로 구성됩니다.
- freestanding implementation 에서는 프로그램이 둘 이상의 실행 스레드를 가질 수 있는지 여부는 구현에 따라 정의됩니다.
시그널 핸들러 가 std::raise 호출의 결과로 실행되지 않는 경우, 시그널 핸들러 호출을 포함하는 실행 스레드는 명시되지 않습니다.
목차 |
데이터 경쟁
서로 다른 실행 스레드는 항상 동시에 서로 다른 memory locations 에 접근(읽기 및 수정)하는 것이 허용되며, 간섭이나 동기화 요구 사항 없이 수행됩니다.
두 개의 표현식 평가 가 충돌 하는 경우는, 둘 중 하나가 메모리 위치를 수정하거나 메모리 위치에서 객체의 수명을 시작/종료하고, 다른 하나가 동일한 메모리 위치를 읽거나 수정하거나 해당 메모리 위치와 중첩되는 저장 공간을 차지하는 객체의 수명을 시작/종료할 때입니다.
두 개의 충돌하는 평가(evaluation)를 가진 프로그램은 데이터 경쟁(data race) 을 가지지 않습니다.
- 두 평가가 동일한 스레드에서 실행되거나 동일한 시그널 핸들러 내에서 실행되는 경우, 또는
- 두 충돌 평가가 모두 원자 연산인 경우 (참조: std::atomic ), 또는
- 충돌 평가 중 하나가 다른 평가에 대해 happens-before 관계를 가지는 경우 (참조: std::memory_order ).
데이터 경쟁이 발생할 경우, 프로그램의 동작은 정의되지 않습니다.
(특히, std::mutex 의 해제는 synchronized-with 관계를 가지며, 따라서 다른 스레드가 동일한 뮤텍스를 획득하는 것은 happens-before 관계를 형성합니다. 이로 인해 뮤텍스 락을 사용하여 데이터 레이스를 방지할 수 있습니다.)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // 정의되지 않은 동작
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // 정상
컨테이너 데이터 경쟁
표준 라이브러리의 모든
컨테이너
는
std
::
vector
<
bool
>
를 제외하고, 동일한 컨테이너 내의 서로 다른 요소들에 대해 포함된 객체의 내용을 동시에 수정하는 것이 데이터 경쟁을 초래하지 않음을 보장합니다.
std::vector<int> vec = {1, 2, 3, 4}; auto f = [&](int index) { vec[index] = 5; }; std::thread t1{f, 0}, t2{f, 1}; // 정상 std::thread t3{f, 2}, t4{f, 2}; // 정의되지 않은 동작
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // 정의되지 않은 동작
메모리 순서
스레드가 메모리 위치에서 값을 읽을 때, 초기값, 동일한 스레드에서 기록된 값, 또는 다른 스레드에서 기록된 값을 볼 수 있습니다. 다른 스레드에서 수행된 쓰기 작업이 다른 스레드에 가시적으로 되는 순서에 대한 자세한 내용은 std::memory_order 를 참조하십시오.
순차적 진행
방해 자유성
표준 라이브러리 함수에서 차단되지 않은 단일 스레드만이 atomic function 중 lock-free인 함수를 실행할 때, 해당 실행은 완료가 보장됩니다(모든 표준 라이브러리 lock-free 연산은 obstruction-free 입니다).
락 프리덤
하나 이상의 락 프리 원자 함수가 동시에 실행될 때, 그 중 적어도 하나는 완료됨이 보장됩니다(모든 표준 라이브러리 락 프리 연산은 lock-free — 다른 스레드에 의해 무기한 라이브 락되지 않도록 구현체가 보장해야 합니다. 예를 들어 캐시 라인을 지속적으로 가로채는 경우 등).
진행 보장
유효한 C++ 프로그램에서 모든 스레드는 결국 다음 중 하나를 수행합니다:
- 종료합니다.
- std::this_thread::yield 를 호출합니다.
- 라이브러리 I/O 함수를 호출합니다.
- volatile glvalue를 통해 접근을 수행합니다.
- 원자적 연산 또는 동기화 연산을 수행합니다.
- 사소한 무한 루프의 실행을 계속합니다 (아래 참조).
스레드는 위의 실행 단계 중 하나를 수행하거나, 표준 라이브러리 함수에서 블록되거나, 차단되지 않은 동시 스레드 때문에 완료되지 않는 원자적 락-프리 함수를 호출할 때 진행을 한다 고 말합니다.
이는 컴파일러가 관찰 가능한 동작이 없는 모든 루프를 제거, 병합 및 재정렬할 수 있도록 허용합니다. 이러한 관찰 가능한 동작을 수행하지 않고 영원히 실행될 수 있는 스레드가 존재하지 않는다고 가정할 수 있기 때문에 해당 루프들이 결국 종료된다는 것을 증명할 필요가 없습니다. 사소한 무한 루프에 대한 배려가 마련되어 있으며, 이러한 루프는 제거되거나 재정렬될 수 없습니다.
단순 무한 루프
사소하게 비어 있는 반복문(trivially empty iteration statement) 은 다음 형식 중 하나에 해당하는 반복문입니다:
while (
condition
) ;
|
(1) | ||||||||
while (
condition
) { }
|
(2) | ||||||||
do ; while (
condition
) ;
|
(3) | ||||||||
do { } while (
condition
) ;
|
(4) | ||||||||
for (
init-statement condition
(선택적)
; ) ;
|
(5) | ||||||||
for (
init-statement condition
(선택적)
; ) { }
|
(6) | ||||||||
사소하게 빈 반복 문의 제어 표현식 은 다음과 같습니다:
사소한 무한 루프 는 변환된 제어 표현식이 상수 표현식 이고, 명백하게 상수 평가 될 때 true 로 평가되는 사소하게 비어 있는 반복문입니다.
사소한 무한 루프의 루프 본문은 std::this_thread::yield 함수 호출로 대체됩니다. 이 대체가 freestanding implementations 에서 발생하는지는 구현에 따라 정의됩니다.
for (;;); // 사소한 무한 루프, P2809 기준으로 명확히 정의됨 for (;;) { int x; } // 정의되지 않은 동작
동시 진행 보장(Concurrent forward progress)스레드가 동시 진행 보장 을 제공하는 경우, 해당 스레드는 종료되지 않는 한 다른 스레드들이 진행을 하고 있는지 여부와 관계없이 유한한 시간 내에 (위에서 정의된 대로) 진행 을 이루게 됩니다. 표준은 메인 스레드와 std::thread 및 std::jthread (C++20부터) 로 시작된 스레드들이 동시 진행 보장을 제공하도록 권장하지만, 이를 요구하지는 않습니다. 병렬 진행 보장(Parallel forward progress)스레드가 병렬 진행 보장 을 제공하는 경우, 아직 실행 단계(입출력, volatile, atomic 또는 동기화 작업)를 하나도 실행하지 않은 스레드가 결국 진행을 이루도록 보장할 필요는 없지만, 일단 이 스레드가 한 단계를 실행하면 동시 진행 보장 을 제공합니다(이 규칙은 작업을 임의 순서로 실행하는 스레드 풀의 스레드를 설명합니다). 약한 병렬 진행 보장(Weakly parallel forward progress)스레드가 약한 병렬 진행 보장 을 제공하는 경우, 다른 스레드들이 진행을 하는지 여부와 관계없이 결국 진행을 이루도록 보장하지 않습니다.
이러한 스레드도 진행 보장 위임을 통한 블로킹으로 진행이 보장될 수 있습니다: 스레드
C++ 표준 라이브러리의 병렬 알고리즘 은 라이브러리 관리 스레드들의 지정되지 않은 집합의 완료를 기다리며 진행 보장 위임 방식으로 블록합니다. |
(C++17부터) |
결함 보고서
다음의 동작 변경 결함 보고서들은 이전에 발표된 C++ 표준에 소급 적용되었습니다.
| DR | 적용 대상 | 게시된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1953 | C++11 |
저장 공간이 겹치는 객체들의 수명을 시작/종료하는
두 표현식 평가가 충돌하지 않음 |
충돌함 |
| LWG 2200 | C++11 |
컨테이너 데이터 경쟁 요구사항이 시퀀스 컨테이너에만
적용되는지 불명확했음 |
모든 컨테이너에 적용됨 |
| P2809R3 | C++11 |
"trivial" 무한 루프 실행의 동작이
[1]
정의되지 않았음 |
"trivial infinite loops"를 명확히 정의하고
동작을 잘 정의됨으로 만듦 |
- ↑ 여기서 "Trivial"이란 무한 루프 실행이 어떤 진전도 이루지 못함을 의미합니다.