예를 들어, 여러 스레드가 접근할 수 있는 공개 정액 카운터 필드를 포함하는 클래스가 있다고 가정해 봅시다. 이 수치는 오르거나 줄어들 뿐입니다.
이 필드를 추가할 때, 다음 스킴 중 어떤 것을 사용해야 하며, 그 이유는 무엇인가요?
- lock(this.locker) this.counter++;
- 상호 고정. 증분(참조 this.counter);
- 카운터의 접근 수정자를 공개 변동성으로 변경하세요
최악의 경우(실제로 효과가 없는 것)
카운터의 접근 수정자를 공개 변동성으로 변경하세요
이 방법은 사실 전혀 안전하지 않으며, 휘발성 모드의 핵심은 여러 CPU에서 실행되는 여러 스레드가 데이터를 버퍼링하고 실행된 명령어를 재배열한다는 점입니다.
비휘발성이라면, CPU A가 값이 증가하면 CPU B가 증가한 값을 보기 위해 일정 시간이 필요해 문제가 발생할 수 있습니다.
만약 휘발성 데이터가 있다면, 두 CPU가 동시에 같은 값을 보도록 보장합니다. 하지만 읽기와 쓰기 연산을 교차 편집하는 것을 피하지는 않습니다.
변수에 가치를 더하는 데는 실제로 세 단계가 필요합니다
1. 독서, 2. 덧셈 3. 쓰기
스레드 A가 카운터의 값을 1로 읽고 증가할 준비가 되지 않았다고 가정하자, 스레드 B도 카운터의 값을 1로 읽고, 두 스레드 모두 증분 및 쓰기 작업을 수행하기 시작한다. 최종 카운터의 가치는 2입니다. 이것은 옳지 않습니다. 두 스레드 모두 증가 연산을 수행했으며, 올바른 결과는 3이어야 합니다. 따라서 이를 변동성 있다고 분류하는 것은 단순히 안전하지 않습니다.
더 좋아
lock(this.locker) this.counter++;
이렇게 하면 안전합니다 (물론 이 카운터에 접근하려면 모든 곳을 잠그세요). 다른 스레드가 잠긴 코드를 실행하는 것을 방지합니다. 또한 앞서 언급한 다중 CPU 명령어 순서 문제도 방지합니다. 문제는 lock이 성능이 느리고, 다른 다른 곳에서 lock을 사용하면 다른 스레드가 차단될 수 있다는 점입니다.
최고의
상호 고정. 증분(참조 this.counter);
이것은 안전하고 매우 효율적입니다. 이 장치는 한 원자에서 중간에 중단되지 않고 읽기, 증가, 쓰기 세 번의 연산을 수행합니다. 다른 코드에는 영향을 주지 않기 때문에 다른 곳에서 자물쇠를 기억할 필요가 없습니다. 그리고 매우 빠릅니다(MSDN에 따르면 오늘날 CPU에서는 종종 명령어만 처리됩니다).
하지만 CPU의 명령어 순서 문제도 해결할 수 있는지, 아니면 휘발성 함수와 이 증분과 함께 사용해야 하는지 확신이 서지 않습니다.
보충: 볼라타일이 잘 해결하는 문제는 무엇인가요?
volatile이 멀티스레딩을 막을 수 없다면, 무엇을 할 수 있을까요? 좋은 예로, 두 개의 스레드가 있는데, 하나는 항상 변수(예를 들어 queneLength)에 쓰고, 다른 하나는 항상 이 변수에서 데이터를 읽습니다. 만약 queueLenght가 변동성이 아니라면, 스레드 A는 5번 읽을 수 있지만, 스레드 B는 지연된 데이터나 심지어 잘못된 순서의 데이터를 볼 수 있습니다. 한 가지 해결책은 락을 사용하는 것이지만, 이 경우에는 볼라타일도 사용할 수 있습니다. 이렇게 하면 스레드 B가 항상 스레드 A가 작성한 최신 데이터를 볼 수 있지만, 이 논리는 쓰기 시 읽지 않고, 읽을 때 쓰지 않을 때만 작동합니다. 여러 스레드가 읽기-수정-쓰기 작업을 하려 할 때는 Interlocked 또는 lock을 사용해야 합니다.
|