programing

휘발성이 멀티스레드 C 또는 C++ 프로그래밍에서 유용하다고 생각되지 않는 이유는 무엇입니까?

prostudy 2022. 6. 28. 22:49
반응형

휘발성이 멀티스레드 C 또는 C++ 프로그래밍에서 유용하다고 생각되지 않는 이유는 무엇입니까?

최근 투고한 이 답변에서 알 수 있듯이, 저는 이 답변의 효용(또는 그 결여)에 대해 혼란스러워하는 것 같습니다.volatile멀티패키지 프로그래밍 컨텍스트에서 사용합니다.

제가 이해한 바로는, 변수가 코드에 액세스 하는 코드의 제어 흐름 밖에서 변경될 수 있는 경우, 그 변수는 다음과 같이 선언되어야 합니다.volatile신호 핸들러, I/O 레지스터, 다른 스레드에 의해 수정된 변수 모두 그러한 상황을 구성합니다.

글로벌 int가 있는 경우foo,그리고.foo(적절한 머신명령어를 사용하여) 한 스레드에 의해 읽혀지고 다른 스레드에 의해 원자적으로 설정됩니다.읽기 스레드는 이 상황을 신호 핸들러에 의해 조정되거나 외부 하드웨어 상태에 의해 변경된 변수를 보는 것과 동일한 방식으로 인식합니다.foo선언해야 한다volatile(또는 멀티스레드 상황에서는 메모리 펜스로 둘러싸인 부하로 액세스 하는 것이 좋습니다).

제가 어떻게, 어디서 틀렸나요?

에 관한 문제volatile멀티스레드 환경에서는 필요한 모든 보증을 제공하지 않습니다.필요한 속성은 몇 가지 있지만, 모든 속성은 아니기 때문에 신뢰할 수 없습니다.volatile 나홀로

그러나 나머지 속성에 대해 사용해야 하는 기본 요소도 다음과 같은 기능을 제공합니다.volatile그렇기에 사실상 불필요합니다.

공유 데이터에 대한 스레드 세이프 액세스를 위해서는 다음과 같은 보증이 필요합니다.

  • 읽기/쓰기가 실제로 발생합니다(컴파일러는 값을 레지스터에 저장하는 대신 메인 메모리 업데이트를 훨씬 늦게 연기합니다).
  • 재배열은 이루어지지 않습니다.를 사용하고 있다고 가정합니다.volatilevariable을 플래그로 지정하여 일부 데이터를 읽을 수 있는지 여부를 나타냅니다.우리 코드의 경우 데이터를 준비한 후 플래그를 설정하기만 하면 되기 때문에 모든 이 정상으로 보입니다.하지만 명령어 순서를 변경하여 플래그를 먼저 설정하면 어떻게 될까요?

volatile첫 번째 포인트를 보증합니다.또한 서로 다른 휘발성 읽기/쓰기 간에 순서 변경이 발생하지 않도록 보장합니다.모든.volatile메모리 액세스는 지정된 순서대로 이루어집니다.그게 우리가 필요한 전부야volatileI/O 레지스터 또는 메모리 매핑하드웨어를 조작하는 것을 목적으로 하고 있습니다만, 멀티스레드 코드에서는 도움이 되지 않습니다.volatile개체는 종종 비휘발성 데이터에 대한 액세스를 동기화하는 데만 사용됩니다.이러한 액세스의 순서를 변경할 수 있습니다.volatile하나.

순서 변경을 방지하기 위한 해결책은 메모리 장벽을 사용하는 것입니다.메모리 장벽은 컴파일러와 CPU 모두에 대해 이 시점에서 메모리액세스를 할 수 없음을 나타냅니다.휘발성 변수 액세스에 이러한 장벽을 설치하면 비휘발성 액세스도 휘발성 변수 액세스에 걸쳐 정렬되지 않으므로 스레드 세이프 코드를 작성할 수 있습니다.

그러나 메모리 장벽은 또한 장벽에 도달했을 때 보류 중인 모든 읽기/쓰기가 실행되도록 하기 때문에 필요한 모든 것을 효과적으로 제공할 수 있기 때문에volatile불필요.그냥 뺄 수 있어요volatile완전 한정자

C++11 이후 원자 변수(std::atomic<T>)는 모든 관련 보증을 제공합니다.

Linux 커널 설명서에서도 이를 고려할 수 있습니다.

C 프로그래머들은 변수가 현재 실행 스레드 밖에서 변경될 수 있다는 것을 의미하는 휘발성을 종종 가져왔습니다. 그 결과, 때때로 공유 데이터 구조가 사용될 때 커널 코드에서 변수를 사용하려고 합니다.즉, 휘발성 유형을 쉬운 원자 변수로 취급하는 것으로 알려져 있지만, 실제로는 그렇지 않습니다.커널 코드의 휘발성 사용은 거의 정확하지 않습니다.이 문서에서는 그 이유에 대해 설명합니다.

휘발성에 관해 이해해야 할 요점은 최적화를 억제하는 것이 목적이라는 것입니다.최적화를 억제하는 것은 실제로 하고 싶은 일이 거의 없습니다.커널에서는 공유 데이터 구조를 원치 않는 동시 액세스로부터 보호해야 합니다.이것은 매우 다른 작업입니다.원치 않는 동시성으로부터 보호하는 프로세스도 보다 효율적인 방법으로 거의 모든 최적화 관련 문제를 방지할 수 있습니다.

휘발성과 마찬가지로 데이터에 대한 동시 접근을 안전하게 하는 커널 프리미티브(스핀록, 뮤텍스, 메모리 장벽 등)는 원치 않는 최적화를 방지하도록 설계되었습니다.올바르게 사용되고 있는 경우는, 휘발성도 사용할 필요가 없습니다.여전히 휘발성이 필요한 경우 코드 어딘가에 버그가 있는 것이 거의 확실합니다.올바르게 기술된 커널 코드에서 휘발성은 속도가 느려질 수 있습니다.

커널 코드의 일반적인 블록을 고려합니다.

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

모든 코드가 잠금 규칙을 따르는 경우 shared_data 값이 잠금 상태 동안 예기치 않게 변경될 수 없습니다.그 데이터로 재생하고 싶은 다른 코드는 잠금으로 대기하고 있습니다.스핀록 프리미티브는 메모리 장벽으로 기능합니다.이러한 장벽은 명시적으로 작성되어 있기 때문에 데이터 액세스가 최적화되지 않습니다.따라서 컴파일러는 shared_data에 무엇이 있는지 알고 있다고 생각할 수 있지만, spin_lock() 호출은 메모리 장벽으로 작용하기 때문에 알고 있는 모든 것을 잊도록 강요합니다.해당 데이터에 대한 액세스에 대한 최적화 문제는 없습니다.

shared_data가 휘발성으로 선언된 경우에도 잠금이 필요합니다.그러나 컴파일러는 다른 누구도 shared_data를 사용할 수 없다는 것을 알고 있을 때 critical 섹션 내에서 shared_data에 대한 접근을 최적화하는 것도 금지되어 있습니다.잠금이 유지되는 동안 shared_data는 휘발성이 없습니다.공유 데이터를 다룰 때 적절한 잠금으로 휘발성이 불필요해지고 잠재적으로 유해합니다.

휘발성 스토리지 클래스는 원래 메모리 매핑 I/O 레지스터를 위한 것입니다.커널 내에서 레지스터 액세스도 잠금으로 보호되어야 하지만 컴파일러가 중요한 섹션 내에서 레지스터 액세스를 "최적화"하는 것을 원하지 않습니다.그러나 커널 내에서 I/O 메모리 액세스는 항상 접근자 기능을 통해 이루어집니다. 포인터를 통해 I/O 메모리에 직접 액세스하는 것은 바람직하지 않으며 모든 아키텍처에서 동작하는 것은 아닙니다.이러한 액세스 장치는 불필요한 최적화를 방지하기 위해 작성되었기 때문에 다시 한 번 휘발성이 필요하지 않습니다.

휘발성을 사용하고 싶은 또 다른 상황은 프로세서가 변수 값을 대기하고 있는 경우입니다.비지 웨이트를 실행하는 올바른 방법은 다음과 같습니다.

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax() 콜은 CPU 소비전력을 낮추거나 하이퍼스레드 트윈프로세서를 만들 수 있습니다.메모리 장벽으로서 기능하는 경우도 있기 때문에 휘발성은 불필요합니다.물론, 바쁜 기다림은 일반적으로 반사회적 행위이다.

커널에서 휘발성이 유효한 경우는 아직 몇 가지 있습니다.

  • 상기의 액세스 기능에서는, 직접 I/O메모리 액세스가 기능하는 아키텍처에서는 휘발성을 사용할 수 있습니다.기본적으로 각 접근자콜은 그 자체로 조금 중요한 섹션이 되어 프로그래머가 예상한 대로 액세스가 이루어지도록 합니다.

  • 메모리를 변경하지만 다른 눈에 띄는 부작용이 없는 인라인어셈블리 코드는 GCC에 의해 삭제될 위험이 있습니다.volatile 키워드를 asm 문에 추가하면 이 삭제가 방지됩니다.

  • jiffies 변수는 참조될 때마다 다른 값을 가질 수 있지만 특별한 잠금 없이 읽을 수 있다는 점에서 특별합니다.따라서 jiffies는 변동성이 있을 수 있지만 이러한 유형의 다른 변수를 추가하는 것은 강하게 거부됩니다.그런 점에서 지피스는 "우둔한 유산" 문제로 간주되고 있다; 그것을 고치는 것은 가치보다 더 골칫거리가 될 것이다.

  • I/O 장치에 의해 수정될 수 있는 일관성 있는 메모리의 데이터 구조에 대한 포인터는 합법적으로 휘발성이 있을 수 있습니다.네트워크 어댑터가 포인터를 변경하여 처리한 디스크립터를 나타내는 링 버퍼가 이러한 유형의 상황의 예입니다.

대부분의 코드에 대해 위의 휘발성에 대한 정당성은 적용되지 않습니다.그 결과 휘발성 사용은 버그로 간주될 수 있으며 코드에 대한 추가적인 정밀 조사가 필요합니다.휘발성을 사용하고 싶은 개발자는 한 걸음 물러서 진정으로 달성하려는 것이 무엇인지 생각해 보아야 합니다.

이것이 "휘발성"이 하는 전부입니다: "이봐, 컴파일러, 이 변수는 로컬 명령이 작용하지 않더라도 언제든지(클럭 틱에서) 변경될 수 있습니다.이 값을 레지스터에 캐시하지 마십시오."

이것이 IT입니다.이것은 컴파일러에 당신의 값이 휘발성이라는 것을 알려줍니다.이 값은 외부 논리(다른 스레드, 다른 프로세스, 커널 등)에 의해 언제든지 변경될 수 있습니다.이는 레지스터에서 본질적으로 안전하지 않은 값을 사일런트 캐시하는 컴파일러 최적화를 억제하기 위해서만 존재합니다.

멀티 스레드 프로그래밍의 만병통치약처럼 변덕스러운 "Dr. Dobbs"와 같은 기사를 접하게 될 수도 있습니다.그의 접근 방식은 장점이 전혀 없는 것은 아니지만, 캡슐화 위반과 같은 문제를 가지고 있는 스레드 안전성에 대한 책임을 오브젝트 사용자에게 지우는 근본적인 결함이 있습니다.

틀리지 않은 것 같습니다.값이 스레드A 이외의 것에 의해 변경되었을 경우 스레드A가 값의 변경을 인식할 수 있도록 하기 위해서는 휘발성이 필요합니다.제가 이해하기로는 휘발성은 기본적으로 컴파일러에게 "이 변수를 레지스터에 캐시하지 말고, 모든 액세스에서 RAM 메모리에서 항상 읽고 쓰도록 하세요."라고 말하는 방법입니다.

이러한 혼란은 휘발성이 많은 것들을 구현하기에 충분하지 않기 때문입니다.특히 최신 시스템은 여러 수준의 캐싱을 사용하고 있으며, 최신 멀티코어 CPU는 런타임에 고급 최적화를 수행하며, 최신 컴파일러는 컴파일 시에 고급 최적화를 수행하며, 이러한 모든 부작용은 소스 코드만 보면 예상할 수 있는 순서와는 다른 순서로 나타날 수 있습니다.

따라서 휘발성 변수의 '관측된' 변경은 사용자가 생각하는 정확한 시간에 발생하지 않을 수 있다는 점을 염두에 둔다면 휘발성은 괜찮습니다.특히 스레드 간에 작업을 동기화하거나 순서를 지정하는 방법으로 휘발성 변수를 사용하지 마십시오. 이 방법은 안정적으로 작동하지 않습니다.

개인적으로는 메인(유일한)volatile 플래그의 사용은 "Please GoAwayNow" 부울로 사용됩니다.연속적으로 루프하는 워커 스레드가 있는 경우 루프의 각 반복에서 휘발성 부울을 체크하고 부울이 true이면 종료합니다.메인 스레드는 부울을 true로 설정하고 pthread_join()을 호출하여 워커 스레드가 없어질 때까지 안전하게 워커의 스레드를 청소할 수 있습니다.

volatilespinlock mutex의 기본구조를 구현하는 데 유용하지만(매우 불충분하지만), 일단 spinlock mutex(또는 우수한 것)가 있으면 다른 것은 필요 없습니다.volatile.

멀티스레드 프로그래밍의 일반적인 방법은 기계 수준에서 모든 공유 변수를 보호하는 것이 아니라 프로그램 흐름을 안내하는 가드 변수를 도입하는 것입니다.대신volatile bool my_shared_flag;그랬어야 했는데

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

이것은 「하드 파트」를 캡슐화할 뿐만 아니라, 기본적으로 필요합니다.C는 뮤텍스를 실장하기 위해서 필요한 원자 연산을 포함하지 않습니다.그것은 단지volatile일반 업무에 대한 추가 보증을 할 수 있습니다.

이제 다음과 같은 것이 있습니다.

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag캐시할 수 없음에도 불구하고 휘발성일 필요는 없습니다.

  1. 다른 스레드에서 액세스할 수 있습니다.
  2. 그 말은 언젠가 (와 함께) 참조된 적이 있을 것이라는 의미입니다.&오퍼레이터).
    • (또는 격납 구조물에 대한 참조가 취해졌다)
  3. pthread_mutex_lock라이브러리 기능입니다.
  4. 즉, 컴파일러는 이 컴파일러에 의해pthread_mutex_lock어떻게 해서든 그 참조를 얻을 수 있습니다.
  5. 즉, 컴파일러는 이 공유 플래그를 수정한다고 가정해야 합니다.
  6. 따라서 변수는 메모리에서 새로고침되어야 합니다. volatile는, 이 문맥에서는 의미가 있지만, 관계가 없습니다.

당신의 이해는 정말 틀렸습니다.

휘발성 변수가 갖는 속성은 "이 변수에서 읽고 쓰는 것은 프로그램의 인식 가능한 동작의 일부"입니다.즉, 이 프로그램은 (적절한 하드웨어가 있는 경우) 동작합니다.

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

문제는, 이것은 우리가 스레드 세이프로부터 원하는 것이 아니라는 것입니다.

예를 들어 스레드 세이프 카운터는 다음과 같습니다(linux-kernel과 같은 코드, c++0x와 동등한 것을 알 수 없습니다).

atomic_t counter;

...
atomic_inc(&counter);

이건 원자이고, 기억장벽도 없어요.필요한 경우 추가해야 합니다.volatile을 추가하는 것은 아마도 도움이 되지 않을 것입니다. 왜냐하면 그것은 근접 코드에 대한 액세스와 관련이 없기 때문입니다(예를 들어 카운터가 카운트하고 있는 목록에 요소를 추가하는 것과 관련이 없습니다).물론 프로그램 외부에서 카운터가 증가하는 것을 볼 필요는 없으며 최적화는 여전히 바람직합니다.

atomic_inc(&counter);
atomic_inc(&counter);

여전히 최적화할 수 있다

atomically {
  counter+=2;
}

최적기가 충분히 스마트한 경우(코드의 의미는 변경되지 않음).

comp.programming.스레드 FAQ는 Dave Butenhof의 고전적인 설명입니다.

Q56: 공유 변수를 VOLATIVE라고 선언할 필요가 없는 이유는 무엇입니까?

다만, 컴파일러와 스레드 라이브러리가 각각 사양에 맞는 경우가 우려됩니다.적합한 C 컴파일러는 일부 공유(비휘발성) 변수를 레지스터에 글로벌하게 할당할 수 있으며, 레지스터는 CPU가 스레드에서 스레드로 전달될 때 저장 및 복원됩니다.각 스레드에는 이 공유 변수에 대한 자체 개인 값이 있지만 공유 변수에서 원하는 값이 아닙니다.

In some sense this is true, if the compiler knows enough about the respective scopes of the variable and the pthread_cond_wait (or pthread_mutex_lock) functions. In practice, most compilers will not try to keep register copies of global data across a call to an external function, because it's too hard to know whether the routine might somehow have access to the address of the data.

So yes, it's true that a compiler that conforms strictly (but very aggressively) to ANSI C might not work with multiple threads without volatile. But someone had better fix it. Because any SYSTEM (that is, pragmatically, a combination of kernel, libraries, and C compiler) that does not provide the POSIX memory coherency guarantees does not CONFORM to the POSIX standard. Period. The system CANNOT require you to use volatile on shared variables for correct behavior, because POSIX requires only that the POSIX synchronization functions are necessary.

So if your program breaks because you didn't use volatile, that's a BUG. It may not be a bug in C, or a bug in the threads library, or a bug in the kernel. But it's a SYSTEM bug, and one or more of those components will have to work to fix it.

You don't want to use volatile, because, on any system where it makes any difference, it will be vastly more expensive than a proper nonvolatile variable. (ANSI C requires "sequence points" for volatile variables at each expression, whereas POSIX requires them only at synchronization operations -- a compute-intensive threaded application will see substantially more memory activity using volatile, and, after all, it's the memory activity that really slows you down.)

/---[ Dave Butenhof ]-----------------------[ butenhof@zko.dec.com ]---\
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3/Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
-----------------[ Better Living Through Concurrency ]----------------/

Mr Butenhof covers much of the same ground in this usenet post:

The use of "volatile" is not sufficient to ensure proper memory visibility or synchronization between threads. The use of a mutex is sufficient, and, except by resorting to various non-portable machine code alternatives, (or more subtle implications of the POSIX memory rules that are much more difficult to apply generally, as explained in my previous post), a mutex is NECESSARY.

따라서, Bryan이 설명한 것처럼 휘발성의 사용은 컴파일러가 유용하고 바람직한 최적화를 하지 못하도록 하는 것 외에는 아무 것도 달성하지 못하며, 코드를 "스레드 안전"하게 만드는 데 아무런 도움도 제공하지 않습니다.물론 원하는 모든 것을 "휘발성"으로 선언할 수 있습니다. 어쨌든 이것은 합법적인 ANSI C 스토리지 속성입니다.스레드 동기화 문제를 해결할 수 있을 것으로 기대하지 마십시오.

모두 C++에 동일하게 적용됩니다.

이전 C 표준에 따르면 "휘발성 수식 유형을 가진 객체에 대한 액세스를 구성하는 것은 구현 정의"입니다.따라서 C 컴파일러 라이터는 "휘발성"을 "다중 프로세스 환경에서 스레드 안전 액세스"를 의미하도록 선택할 수 있습니다.하지만 그들을 그렇지 못 했다.

대신 멀티코어 멀티프로세스 공유 메모리 환경에서 중요한 섹션 스레드를 안전하게 만들기 위해 필요한 작업이 새로운 구현 정의 기능으로 추가되었습니다.그리고, "휘발성"이 멀티 프로세스 환경에서 원자적 접근과 접근 순서를 제공한다는 요구사항으로부터 자유로워진 컴파일러 라이터는 과거의 구현 의존적인 "휘발성" 의미론보다 코드 감소를 우선시했다.

즉, 중요한 코드 섹션 주변의 "휘발성" 세마포와 같은 것은 새로운 컴파일러를 사용하는 새로운 하드웨어에서는 동작하지 않는 경우가 있었습니다.또한 오래된 예는 잘못된 것이 아니라 오래된 것일 수도 있습니다.

동시 환경에서 데이터를 일관되게 유지하려면 다음 두 가지 조건을 적용해야 합니다.

1) 원자성, 즉 메모리에 데이터를 읽거나 쓰는 경우 해당 데이터는 한 번에 읽기/쓰기되며 컨텍스트 스위치 등의 이유로 중단되거나 경합될 수 없습니다.

2) 일관성(읽기/쓰기 조작 순서는 스레드, 머신 등 여러 환경에서 동일해야 함)

volatile은 위의 어느 것에도 적합하지 않습니다.특히 휘발성의 동작 방법에 관한 c 또는 c++ 규격에는 위의 어느 것도 포함되어 있지 않습니다.

일부 컴파일러(인텔 Itanium 컴파일러 등)는 (메모리 펜스를 확보함으로써) 동시 액세스 안전 동작의 요소를 구현하려고 하지만 컴파일러 구현 간에 일관성이 없고 표준에서는 이 구현이 처음부터 필요하지 않기 때문에 더욱 심각합니다.

Marking a variable as volatile will just mean that you are forcing the value to be flushed to and from memory each time which in many cases just slows down your code as you've basically blown your cache performance.

c# and java AFAIK do redress this by making volatile adhere to 1) and 2) however the same cannot be said for c/c++ compilers so basically do with it as you see fit.

For some more in depth ( though not unbiased ) discussion on the subject read this

ReferenceURL : https://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming

반응형