Namespaces
Variants

Multi-threaded executions and data races (since C++11)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

실행 스레드 는 특정 최상위 함수의 호출로 시작하여( 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)
1) 빈 간단 문(empty simple statement)을 루프 본문으로 갖는 while 문(statement) .
2) 빈 복합문을 루프 본문으로 갖는 while .
3) do - while 으로, 루프 본문이 빈 단순 문장인 경우.
4) do - while 으로, 루프 본문이 빈 복합문인 경우.
5) 루프 본문이 빈 단순 문장인 for 에서, for 문은 iteration-expression 을 가지지 않습니다.
6) 루프 본문이 빈 복합문인 for 에서, for 문은 iteration-expression 을 가지지 않습니다.

사소하게 빈 반복 문의 제어 표현식 은 다음과 같습니다:

1-4) condition .
5,6) condition 이 있는 경우, 그렇지 않으면 true .

사소한 무한 루프 는 변환된 제어 표현식이 상수 표현식 이고, 명백하게 상수 평가 될 때 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)

스레드가 약한 병렬 진행 보장 을 제공하는 경우, 다른 스레드들이 진행을 하는지 여부와 관계없이 결국 진행을 이루도록 보장하지 않습니다.

이러한 스레드도 진행 보장 위임을 통한 블로킹으로 진행이 보장될 수 있습니다: 스레드 P 가 이러한 방식으로 스레드 집합 S 의 완료를 대기하며 블록하는 경우, S 내의 적어도 하나의 스레드는 P 와 같거나 더 강력한 진행 보장을 제공합니다. 해당 스레드가 완료되면, S 내의 다른 스레드가 유사하게 강화됩니다. 집합이 비게 되면 P 는 블록이 해제됩니다.

C++ 표준 라이브러리의 병렬 알고리즘 은 라이브러리 관리 스레드들의 지정되지 않은 집합의 완료를 기다리며 진행 보장 위임 방식으로 블록합니다.

(C++17부터)

결함 보고서

다음의 동작 변경 결함 보고서들은 이전에 발표된 C++ 표준에 소급 적용되었습니다.

DR 적용 대상 게시된 동작 올바른 동작
CWG 1953 C++11 저장 공간이 겹치는 객체들의 수명을 시작/종료하는
두 표현식 평가가 충돌하지 않음
충돌함
LWG 2200 C++11 컨테이너 데이터 경쟁 요구사항이 시퀀스 컨테이너에만
적용되는지 불명확했음
모든 컨테이너에 적용됨
P2809R3 C++11 "trivial" 무한 루프 실행의 동작이 [1]
정의되지 않았음
"trivial infinite loops"를 명확히 정의하고
동작을 잘 정의됨으로 만듦
  1. 여기서 "Trivial"이란 무한 루프 실행이 어떤 진전도 이루지 못함을 의미합니다.