programing

C/P 스레드 사용: 공유 변수가 변동성이 있어야 합니까?

megabox 2023. 10. 26. 20:57
반응형

C/P 스레드 사용: 공유 변수가 변동성이 있어야 합니까?

C 프로그래밍 언어와 Pthreads를 쓰레드 라이브러리로 사용합니다. 쓰레드 간에 공유되는 변수/구조를 volatile로 선언해야 합니까?잠금 장치에 의해 보호될 수도 있고 그렇지 않을 수도 있다고 가정할 때(장벽이 있을 수도 있음).

pthread POSIX 표준은 이것에 대한 발언권이 있습니까, 이 컴파일러에 의존하는 것입니까, 아니면 둘 다입니까?

추가할 편집:좋은 답변 감사합니다.하지만 자물쇠를 사용하지 않는다면 어떨까요? 예를 들어 장벽을 사용한다면 어떨까요?또는 비교-스왑과 같은 프리미티브를 사용하여 직접적이고 원자적으로 공유 변수를 수정하는 코드...

변수에 대한 액세스를 제어하기 위해 잠금 장치를 사용하는 한 변수에 휘발성이 있을 필요는 없습니다.사실, 만약 여러분이 변수에 변동성을 부여하고 있다면 여러분은 이미 틀렸을 것입니다.

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

정답은 '아니오'입니다. 적절한 동기화 프리미티브 외에 'volatile'을 사용할 필요는 없습니다.할 필요가 있는 모든 것은 이 원시적인 것들에 의해 이루어집니다.

volatile'을 사용하는 것은 필요하지도 충분하지도 않습니다.적절한 동기화 프리미티브가 충분하기 때문에 필요하지 않습니다.사용자에게 물릴 수 있는 모든 최적화가 아닌 일부 최적화만 비활성화하기 때문에 충분하지 않습니다.예를 들어, 다른 CPU에서 원자성이나 가시성을 보장하지는 않습니다.

휘발성을 사용하지 않는 한 컴파일러는 자유롭게 공유 데이터를 레지스터에 저장할 수 있습니다.데이터가 컴파일러의 재량에 따라 레지스터에 캐싱되지 않고 실제 메모리에 예측 가능하게 기록되도록 하려면 휘발성이 있는 것으로 표시해야 합니다.또는 기능을 수정한 후 공유 데이터에만 액세스하는 경우에도 문제가 없을 수 있습니다.하지만 저는 값이 레지스터에서 메모리로 다시 쓰여지는 것을 확실하게 하기 위해 맹운에 의존하지 않는 것을 제안합니다.

맞습니다. 하지만 휘발성을 사용하더라도 CPU는 공유 데이터를 쓰기 게시 버퍼에 얼마든지 자유롭게 캐시할 수 있습니다.사용자를 물어뜯을 수 있는 최적화 집합은 'volatile'이 비활성화하는 최적화 집합과 정확하게 같지 않습니다.따라서 'volatile'을 사용하면 맹운에 의존하게 됩니다.

반면 정의된 다중 스레드 의미론과 함께 동기화 프리미티브를 사용하면 작동이 보장됩니다.또한 '휘발성'이라는 엄청난 성능 히트를 감수하지 않아도 됩니다.그러면 왜 그런 식으로 일을 진행하지 않습니까?

휘발성의 매우 중요한 특성 중 하나는 변수를 수정할 때 메모리에 기록하고 액세스할 때마다 메모리에서 다시 읽게 한다는 것입니다.여기서 다른 답변은 휘발성과 동기화를 혼합한 것으로, 이 답변 이외의 다른 답변에서는 휘발성이 동기화 프리미티브(신용이 만기인 신용)가 아님을 알 수 있습니다.

휘발성을 사용하지 않는 한 컴파일러는 자유롭게 공유 데이터를 레지스터에 저장할 수 있습니다.데이터가 컴파일러의 재량에 따라 레지스터에 캐싱되지 않고 실제 메모리에 예측 가능하게 기록되도록 하려면 휘발성이 있는 것으로 표시해야 합니다.또는 기능을 수정한 후 공유 데이터에만 액세스하는 경우에도 문제가 없을 수 있습니다.하지만 저는 값이 레지스터에서 메모리로 다시 쓰여지는 것을 확실하게 하기 위해 맹운에 의존하지 않는 것을 제안합니다.

특히 레지스터가 풍부한 머신(즉, x86이 아닌)에서는 변수가 레지스터에서 꽤 오랜 기간 동안 유지될 수 있으며, 훌륭한 컴파일러는 구조의 일부 또는 전체 구조를 레지스터에 캐시할 수 있습니다.따라서 휘발성을 사용해야 하지만 성능을 위해 값을 로컬 변수에 복사한 다음 명시적인 쓰기-백을 수행해야 합니다.기본적으로 휘발성을 효율적으로 사용한다는 것은 C 코드에서 로드 스토어(load-store) 사고를 약간 수행한다는 것을 의미합니다.

어떤 경우든 정확한 프로그램을 만들기 위해서는 OS 수준의 동기화 메커니즘을 사용해야 합니다.

volatile의 약점에 대한 예는 http://jakob.engbloms.se/archives/65, 에서 decker의 알고리즘 예를 참조하십시오. 이는 volatile이 동기화하는 데 작동하지 않는다는 것을 꽤 잘 증명합니다.

휘발성이라는 키워드는 멀티 스레드 프로그래밍에 좋다는 인식이 널리 퍼져 있습니다.

Hans Boehm은 휘발성에 대한 휴대용 용도는 세 가지뿐이라고 지적합니다.

  • volatile은 값이 긴 jmp에 걸쳐 보존되어야 하는 setjmp와 동일한 범위에 있는 지역 변수를 표시하는 데 사용될 수 있습니다.문제의 지역 변수를 공유할 방법이 없는 경우 원자성과 순서 제약은 아무런 영향을 미치지 않기 때문에 어떤 부분의 사용이 느려질지는 불분명합니다. (모든 변수를 긴 jmp에 걸쳐 보존하도록 요구함으로써 어떤 부분의 사용이 느려질지조차 불분명합니다.그러나 이는 별개의 문제이며 여기서는 고려되지 않습니다.)
  • volatile은 변수가 "외부적으로 수정"될 수 있지만, 사실 수정은 스레드 자체에 의해 동기적으로 트리거됩니다. 예를 들어 기본 메모리가 여러 위치에 매핑되어 있기 때문입니다.
  • 휘발성 sigatomic_t는 제한된 방식으로 동일한 스레드에서 신호 핸들러와 통신하기 위해 사용될 수 있습니다.sigatomic_t 케이스에 대한 요구사항을 약화시키는 것을 고려할 수 있지만, 그것은 다소 직관적이지 않은 것으로 보입니다.

속도를 위해 멀티스레딩을 하는 경우 코드 속도를 줄이는 것은 확실히 원하는 것이 아닙니다.다중 스레드 프로그래밍의 경우 휘발성이 종종 실수로 해결되는 것으로 생각되는 두 가지 핵심 문제는 다음과 같습니다.

  • 원자성
  • 메모리 일관성, 즉 다른 스레드에서 볼 수 있는 스레드의 작업 순서입니다.

(1)을 먼저 다루도록 하겠습니다.휘발성은 원자 읽기 또는 쓰기를 보장하지 않습니다.예를 들어, 129비트 구조의 휘발성 읽기 또는 쓰기는 대부분의 최신 하드웨어에서 원자성이 아닐 것입니다.32비트 int의 휘발성 읽기 또는 쓰기는 대부분의 최신 하드웨어에서 원자이지만 휘발성은 이와 무관합니다.휘발성 물질이 없으면 원자일 가능성이 높습니다.원자성은 컴파일러의 마음에 걸려 있습니다.C나 C++ 표준에는 원자가 되어야 한다는 내용이 없습니다.

이제 이슈(2)를 생각해 봅니다.때때로 프로그래머들은 휘발성을 휘발성 접근의 최적화를 끄는 것으로 생각합니다.그것은 실제로는 대체로 사실입니다.하지만 그것은 휘발성이 없는 접근이 아니라 휘발성이 있는 접근일 뿐입니다.다음 조각을 고려합니다.

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

멀티 스레드 프로그래밍에서 매우 합리적인 작업을 시도하고 있습니다. 메시지를 작성한 다음 다른 스레드로 전송하는 것입니다.다른 스레드는 Ready가 0이 아닐 때까지 기다렸다가 Message를 읽습니다.gcc 4.0 또는 icc를 사용하여 "gcc -O2 -S"로 컴파일해 보십시오.둘 다 Ready로 먼저 스토어를 진행하기 때문에 i/10의 연산과 겹칠 수 있습니다.순서 바꾸기는 컴파일러 버그가 아닙니다.그것은 자신의 일을 수행하는 공격적인 옵티마이저입니다.

모든 메모리 참조를 휘발성으로 표시하는 것이 해결책이라고 생각할 수도 있습니다.그건 정말 바보같은 짓입니다.앞의 인용문에서 알 수 있듯이 코드 속도가 느려질 뿐입니다.최악의 경우 문제가 해결되지 않을 수도 있습니다.컴파일러가 참조 순서를 변경하지 않더라도 하드웨어는 그럴 수 있습니다.이 예에서 x86 하드웨어는 순서를 변경하지 않습니다.Itanium 컴파일러는 휘발성 저장소를 위한 메모리 펜스를 삽입하기 때문에 Itanium(TM) 프로세서도 마찬가지입니다.그것은 똑똑한 Itanium 확장입니다.하지만 파워(TM)와 같은 칩은 다시 주문할 것입니다.주문할 때 정말로 필요한 것은 메모리 장벽이라고도 불리는 메모리 펜스입니다.메모리 펜스는 펜스를 넘어 메모리 작업의 순서를 바꾸거나, 경우에 따라서는 한 방향의 순서를 바꾸는 것을 막습니다.휘발성은 기억의 울타리와는 상관이 없습니다.

그렇다면 다중 스레드 프로그래밍을 위한 해결책은 무엇일까요?원자 및 울타리 의미론을 구현하는 라이브러리 또는 언어 확장을 사용합니다.의도한 대로 사용하면 라이브러리의 작업에 올바른 펜스가 삽입됩니다.몇 가지 예:

  • POSIX 스레드
  • Windows(TM) 스레드
  • OpenMP
  • TBB

Arch Robison (Intel)의 기사를 기반으로 합니다.

내 경험으로는, 아니요. 당신은 단지 당신이 그러한 값들에 쓸 때 당신 자신을 적절하게 뮤텍스하거나, 스레드들이 다른 스레드의 동작에 의존하는 데이터에 접근하기 전에 스레드들이 멈추도록 당신의 프로그램을 구성하기만 하면 됩니다.제 프로젝트인 x264는 이 방법을 사용합니다. 스레드는 엄청난 양의 데이터를 공유하지만 대부분의 스레드는 읽기 전용이거나 스레드는 데이터에 액세스하기 전에 데이터가 사용 가능해지거나 최종적으로 완료되기를 기다리므로 음소거가 필요하지 않습니다.

연산 과정에서 많이 인터리빙되는 스레드가 많은 경우(이 스레드들은 서로의 출력에 따라 매우 세분화된 수준으로 결정됨), 이 작업은 훨씬 더 어려울 수 있습니다. 실제로 이 경우 스레드 간의 분리를 통해 더 깨끗하게 수행할 수 있는지 확인하기 위해 스레드 모델을 다시 검토해 보겠습니다.

아니요.

VolatileCPU 읽기/쓰기 명령과 독립적으로 변경할 수 있는 메모리 위치를 읽을 때만 필요합니다.쓰레드 상황에서는 CPU가 각 쓰레드에 대한 메모리 읽기/쓰기를 완전히 제어하므로 컴파일러는 메모리가 일관성이 있다고 가정하고 CPU 명령을 최적화하여 불필요한 메모리 액세스를 줄일 수 있습니다.

다음에 대한 주요 용도volatile메모리 매핑 I/O에 액세스하기 위한 것입니다.이 경우, 기본 장치는 CPU와 독립적으로 메모리 위치의 값을 변경할 수 있습니다.사용하지 않을 경우volatile이 조건에서 CPU는 새로 업데이트된 값을 읽는 대신 이전에 캐시된 메모리 값을 사용할 수 있습니다.

POSIX 7은 다음과 같은 기능을 보장합니다.pthread_lock또한 메모리를 동기화 합니다.

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_11 "4.12 메모리 동기화"는 다음과 같이 말합니다.

다음 기능은 메모리를 다른 스레드와 동기화합니다.

pthread_barrier_wait()
pthread_cond_broadcast()
pthread_cond_signal()
pthread_cond_timedwait()
pthread_cond_wait()
pthread_create()
pthread_join()
pthread_mutex_lock()
pthread_mutex_timedlock()
pthread_mutex_trylock()
pthread_mutex_unlock()
pthread_spin_lock()
pthread_spin_trylock()
pthread_spin_unlock()
pthread_rwlock_rdlock()
pthread_rwlock_timedrdlock()
pthread_rwlock_timedwrlock()
pthread_rwlock_tryrdlock()
pthread_rwlock_trywrlock()
pthread_rwlock_unlock()
pthread_rwlock_wrlock()
sem_post()
sem_timedwait()
sem_trywait()
sem_wait()
semctl()
semop()
wait()
waitpid()

따라서 변수가 다음 값 사이에서 보호되는 경우pthread_mutex_lock그리고.pthread_mutex_unlock그러면 추가 동기화가 필요하지 않습니다.volatile.

관련 질문:

휘발성은 한 스레드가 무언가를 쓰고 다른 스레드가 그것을 읽을 때까지 지연이 전혀 필요 없는 경우에만 유용할 것입니다.하지만 일종의 잠금이 없다면, 다른 스레드가 언제 데이터를 썼는지 알 수 없고, 가장 최근의 값일 뿐입니다.

단순한 값(int 및 float)의 경우 명시적인 동기점이 필요 없는 경우 뮤텍스가 오버킬일 수 있습니다.뮤텍스나 잠금 같은 것을 사용하지 않는 경우 변수 휘발성을 선언해야 합니다.뮤텍스를 사용하면 만반의 준비가 됩니다.

복잡한 유형의 경우에는 뮤텍스를 사용해야 합니다.작업은 원자가 아닌 작업이므로 뮤텍스가 없는 반변경 버전을 읽을 수 있습니다.

휘발성은 우리가 이 값을 얻거나 설정하기 위해 메모리로 가야 한다는 것을 의미합니다.volatile을 설정하지 않으면 컴파일된 코드가 데이터를 레지스터에 오랫동안 저장할 수 있습니다.

이것은 스레드 간에 공유하는 변수를 휘발성으로 표시하여 한 스레드가 값을 수정하기 시작하지만 두 번째 스레드가 실행되어 값을 읽기 전에 결과를 쓰지 않는 상황이 발생하지 않도록 해야 한다는 것을 의미합니다.

휘발성은 특정 최적화를 비활성화하는 컴파일러 힌트입니다.컴파일러의 출력 어셈블리가 없었다면 안전했을 수도 있지만 항상 공유 값에 사용해야 합니다.

시스템에서 제공하는 값비싼 스레드 동기화 개체를 사용하지 않는 경우에는 특히 중요합니다. 예를 들어 일련의 원자 변경 시에도 이 개체를 유효하게 유지할 수 있는 데이터 구조가 있을 수 있습니다.메모리를 할당하지 않는 많은 스택이 이러한 데이터 구조의 예입니다. 스택에 값을 추가한 다음 종료 포인터를 이동하거나 종료 포인터를 이동한 후 스택에서 값을 제거할 수 있기 때문입니다.이러한 구조를 구현할 때 휘발성은 원자 명령이 실제로 원자임을 확인하는 데 매우 중요합니다.

근본적인 이유는 C 언어 시맨틱이 단일 스레드 추상 기계를 기반으로 하기 때문입니다.그리고 컴파일러는 추상적 기계에서 프로그램의 '관찰 가능한 행동'이 변하지 않는 한 프로그램을 변형할 권리가 있습니다.인접하거나 중복되는 메모리 액세스를 병합하거나, 여러 번 메모리 액세스를 다시 실행하거나(예: 레지스터 스필링 시), 단일 스레드에서 실행해도 프로그램 동작이 변하지 않는다고 생각되는 경우 메모리 액세스를 폐기할 수 있습니다.따라서 의심할 수 있듯이 프로그램이 실제로 멀티 스레드 방식으로 실행되어야 하는 경우 동작이 변경됩니다.

Paul Mckenny는 유명한 Linux 커널 문서에서 다음과 같이 지적했습니다.

컴파일러가 READ_ONCE() 및 WRITE_ONCE()에 의해 보호되지 않는 메모리 참조로 사용자가 원하는 작업을 수행한다고 가정합니다.컴파일러가 없으면 컴파일러는 모든 종류의 "창의적인" 변환을 수행할 수 있는 권한 내에 있게 됩니다. 이는 COLLIGER BARRIER 섹션에서 다룹니다.

READ_ONCE() 및 WRITE_ONCE()는 참조된 변수의 휘발성 캐스트로 정의됩니다.따라서:

int y;
int x = READ_ONCE(y);

는 다음과 같습니다.

int y;
int x = *(volatile int *)&y;

따라서 '휘발성' 액세스를 수행하지 않는 한 어떤 동기화 메커니즘을 사용하든 액세스가 한 만 수행된다는 보장이 없습니다.외부 함수(예: pthread_mutex_lock)를 호출하면 컴파일러가 전역 변수에 메모리 액세스를 수행할 수 있습니다.그러나 이는 컴파일러가 외부 함수가 이러한 전역 변수를 변경하는지 여부를 파악하지 못할 때만 발생합니다.정교한 인터프로시저 분석과 링크 타임 최적화를 사용하는 현대적인 컴파일러는 이 기법을 쓸모없게 만듭니다.

요약하면 여러 스레드에서 공유하는 변수를 휘발성으로 표시하거나 휘발성 캐스트를 사용하여 액세스해야 합니다.


폴 맥케니도 지적한 바와 같이:

저는 아이들이 여러분의 아이들이 알기를 원하지 않는 최적화 기술에 대해 이야기할 때 그들의 눈에서 반짝임을 본 적이 있습니다!


하지만 C11/C++11에 무슨 일이 일어나는지 보세요.

어떤 사람들은 분명히 컴파일러가 동기화 호출을 메모리 장벽으로 취급한다고 가정하고 있습니다."케이시"는 CPU가 정확히 하나라고 가정하고 있습니다.

동기화 프리미티브가 외부 함수이고 문제의 기호가 컴파일 단위 외부(글로벌 이름, 내보내기 포인터, 이를 수정할 수 있는 내보내기 함수)에 표시되는 경우 컴파일러는 모든 외부에서 볼 수 있는 개체에 대한 메모리 펜스로 처리하거나 기타 외부 함수 호출을 처리합니다.

그렇지 않으면, 당신은 당신 혼자입니다.휘발성은 컴파일러가 정확하고 빠른 코드를 생성하는 데 사용할 수 있는 최고의 도구일 것입니다.일반적으로 휘발성이 필요할 때와 실제로 어떤 기능을 수행하는지는 시스템과 컴파일러에 따라 크게 달라지기 때문에 휴대가 불가능합니다.

아니요.

,volatile필요 없습니다.사용하지 않는 보장된 멀티스레드 시맨틱스를 제공하는 수많은 다른 작업이 있습니다.volatile 여기에는 원자 연산, 음소거 등이 포함됩니다.

,volatile충분하지 않습니다.C 표준은 선언된 변수에 대해 멀티스레드 동작에 대한 보장을 제공하지 않습니다.volatile.

따라서 필요하지도 충분하지도 않기 때문에, 그것을 사용하는 것은 큰 의미가 없습니다.

한 가지 예외는 멀티 스레드 의미론을 문서화한 특정 플랫폼(Visual Studio 등)입니다.

스레드 간에 공유되는 변수는 'volatile'로 선언해야 합니다.이것은 컴파일러에게 하나의 스레드가 그러한 변수에 쓸 때 쓰기는 (레지스터와 반대로) 메모리에 있어야 한다는 것을 알려줍니다.

언급URL : https://stackoverflow.com/questions/78172/using-c-pthreads-do-shared-variables-need-to-be-volatile

반응형