programing

i++는 스레드가 안전하지 않다고 들었는데, ++i 스레드는 안전한가요?

kingscode 2022. 9. 2. 00:09
반응형

i++는 스레드가 안전하지 않다고 들었는데, ++i 스레드는 안전한가요?

i++는 스레드 세이프 스테이트먼트가 아니라고 들었습니다.어셈블리에서는 원래 값을 temp로 저장하여 증가시킨 후 콘텍스트 스위치에 의해 중단될 수 있기 때문입니다.

하지만 ++i가 궁금해요.내가 알기로는, 이것은 'add r1, r1, 1'과 같은 단일 어셈블리 명령으로 축소되며, 이는 하나의 명령이기 때문에 컨텍스트 스위치로 중단되지 않습니다.

누구 설명 좀 해주시겠어요?x86 플랫폼이 사용되고 있다고 가정합니다.

못못들들들들들들 것이다"i++"는 특정 컴파일러 및 특정 프로세서 아키텍처에 대해 스레드 세이프이지만 표준에서는 전혀 의무화되어 있지 않습니다.사실 멀티스레딩은 ISO C 또는 C++ 규격의 일부가 아니기 때문에 컴파일 내용에 따라 스레드 세이프하다고는 할 수 없습니다.

것 같다++i는, 할 수 .

load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory

메모리 증설 지시가 없는 CPU에서는 스레드 세이프가 되지 않습니다.또는 다음과 같이 컴파일할 수 있습니다.

lock         ; disable task switching (interrupts)
load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
unlock       ; enable task switching (interrupts)

서 ''는lock 및 "disable" 입니다.unlock이치노다만, 이 에도, 「」 「」 「」 「」 「」 「」 「」)를 가지는 에서는, 않는 경우가 .lock는, 1 개의 에 만 인터럽트를 무효로 할 수 ), 1 의 CPU 를 무효로 합니다.

언어 자체(또는 언어에 내장되어 있지 않은 경우 라이브러리)는 스레드 세이프 구조를 제공합니다.또, 생성되는 머신 코드의 이해(혹은 오해)에 의존하지 말고, 그것들을 사용할 필요가 있습니다.

synchronized ★★★★★★★★★★★★★★★★★」pthread_mutex_lock()(일부 operating system에서는 C/C++에서 이용 가능)이 필요한 부분입니다.


(a) 이 질문은 C11 및 C++11 표준이 완성되기 전에 실시되었습니다.이러한 반복으로 원자 데이터 유형을 포함한 언어 사양에 스레드 지원이 도입되었습니다(단, 일반적으로 스레드 및 스레드는 최소한 C에서는 옵션입니다).

둘 다 안전하지 않다.

CPU는 메모리로 직접 연산할 수 없습니다.메모리로부터 값을 로드해, CPU 레지스터를 사용해 계산을 실시해, 간접적으로 이것을 실시합니다.

i++

register int a1, a2;

a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1; 
*(&i) = a1; 
return a2; // 4 cpu instructions

++i

register int a1;

a1 = *(&i) ; 
a1 += 1; 
*(&i) = a1; 
return a1; // 3 cpu instructions

두 경우 모두 예측 불가능한 i 값을 초래하는 레이스 조건이 있습니다.

예를 들어 각각 레지스터 a1, b1을 사용하는 두 개의 ++i 스레드가 동시에 존재한다고 가정합니다.컨텍스트 스위칭은 다음과 같이 실행됩니다.

register int a1, b1;

a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;

결과적으로, 나는 i+2가 아니라 i+1이 되는데, 이것은 틀렸다.

이 문제를 해결하기 위해 콘텍스트스위칭이 디세블로 되어 있는 동안 모뎀 CPU는 일종의 LOCK, UNLOCK CPU 명령을 제공합니다.

Win32의 경우 인터락을 사용합니다.스레드 안전을 위해 i++를 수행합니다.뮤텍스에 의존하는 것보다 훨씬 빠릅니다.

단일 어셈블리 명령으로 축소되어 메모리 내에서 직접 값을 증가시켜도 스레드 세이프가 되지 않습니다.

메모리내의 값을 증가시키면, 하드웨어는 「읽기-수정-쓰기」 조작을 실시합니다.즉, 메모리로부터 값을 읽어, 증가시킨 후, 메모리에 다시 씁니다.x86 하드웨어는 메모리 상에서 직접 증가시키는 방법이 없습니다.RAM(및 캐시)은 값을 읽고 저장할 수 있을 뿐 수정하지 않습니다.

이제 별도의 소켓 또는 단일 소켓(공유 캐시 유무)을 공유하는 두 개의 개별 코어가 있다고 가정합니다.첫 번째 프로세서가 값을 읽고 업데이트된 값을 다시 쓰기 전에 두 번째 프로세서가 값을 읽습니다.두 프로세서가 값을 다시 쓴 후에는 값이 두 번이 아니라 한 번만 증가합니다.

이 문제를 회피하는 방법이 있습니다.x86 프로세서(및 대부분의 멀티코어 프로세서)는 하드웨어에서 이러한 경합을 검출하여 시퀀스를 실행할 수 있기 때문에 읽기-수정-쓰기 시퀀스 전체가 원자적으로 표시됩니다. 이것은 이 많이 들기 됩니다.LOCK얻을 수를 들어 로드 링크 및 아토믹 및 스왑에도 이 아키텍처가 ).다른 아키텍처에서도 이와 유사한 결과를 얻을 수 있습니다.예를 들어 로드 링크/스토어 조건부 및 아토믹 비교/스왑(최근의 x86 프로세서에도 이 마지막 아키텍처가 있습니다).

「 」를 사용하고 있는 에 주의해 .volatile여기서 도움이 되지 않습니다.변수가 외부에서 변경되었을 가능성이 있으며 해당 변수에 대한 읽기를 레지스터에 캐시하거나 최적화해서는 안 된다는 것만 컴파일러에 알립니다.원자 원초

가장 좋은 방법은 atomic primitive(컴파일러 또는 라이브러리가 있는 경우)를 사용하거나 어셈블리에서 직접 증분(올바른 atomic 명령을 사용하여)하는 것입니다.

C 경우 라이브러리(C++ 라이브러리)를할 수 .std::atomicTBB를 사용하다

한때 GNU 코딩 가이드라인은 한 단어로 데이터 타입을 업데이트하는 것이 "보통 안전하다"고 말했지만 SMP 머신에 대해서는 잘못된 조언이었다. 일부 아키텍처에는 적합하지 않습니다. Optimizing 컴파일러를 사용할 경우 오류가 발생합니다.


"한 단어로 된 데이터 유형 업데이트" 코멘트를 명확히 하려면:

SMP 머신상의 2개의 CPU가 같은 사이클로 같은 메모리 위치에 기입해, 그 변경을 다른 CPU와 캐시에 전파할 수 있습니다.데이터를 한 단어만 쓰기 때문에 쓰기가 완료되는 데 한 사이클만 소요되더라도 동시에 발생하므로 어떤 쓰기가 성공하는지 보장할 수 없습니다.부분적으로 업데이트된 데이터는 수신되지 않지만, 이 문제를 처리할 다른 방법이 없기 때문에 한 개의 쓰기가 사라집니다.

비교 및 스왑은 여러 CPU 간에 적절하게 조정되지만 단일 단어 데이터 유형의 모든 변수 할당이 비교 및 스왑을 사용할 것이라고 믿을 이유는 없습니다.

또한 최적화 컴파일러는 로드/스토어의 컴파일 방법에 영향을 주지 않지만 로드/스토어가 발생했을 때 변경될 수 있으며, 읽기 및 쓰기가 소스 코드에 표시되는 것과 같은 순서로 발생할 것으로 예상할 경우 심각한 문제를 일으킬 수 있습니다(가장 유명한 것은 더블 체크된 잠금 기능은 바닐라 C++에서는 작동하지 않습니다).

주의: 원래 답변에서는 인텔 64비트 아키텍처가 64비트 데이터 처리에서 고장났다고 했습니다.그렇지 않기 때문에 답변을 편집했는데 편집한 내용이 Power라고 되어 있습니다.PC칩이 고장났습니다.이는 레지스터에 즉시 값(즉, 상수)을 읽을 때 해당됩니다(목록 2와 목록 4 아래에 있는 "포인터 로드"라는 두 섹션을 참조). 단, 메모리로부터 데이터를 한 사이클로 로드하는 명령이 있습니다( ).lmw답변의 그 부분은 삭제했습니다.

++i와 i++ 둘 다에 대해 포괄적으로 진술할 수 없습니다. 왜요?32비트 시스템에서 64비트 정수를 늘리는 것을 고려해 보십시오.기본 머신에 "load, increment, store" 명령어가 4개 포함되어 있지 않는 한 이 값을 증가시키려면 여러 명령이 필요하며 스레드 컨텍스트 스위치에 의해 중단될 수 있습니다.

외에 '있다'도 있어요.++i이렇게 하다'라고 합니다.C와 같은 언어에서는 포인터를 늘리면 포인터의 크기가 커집니다. 「」의 는 「」입니다.i입니다.++i32번입니다.거의 모든 플랫폼이 원자적인 "메모리 주소의 값 증가" 명령을 가지고 있지만, 모든 플랫폼이 원자적인 "메모리 주소의 값에 임의 값을 추가" 명령을 가지고 있는 것은 아닙니다.

당신의 프로그래밍 언어가 스레드에 대해 아무런 언급도 하지 않고 멀티스레드 플랫폼에서 실행된다면, 어떻게 언어 구조가 스레드 안전할 수 있을까요?

다른 사람들이 지적한 바와 같이 플랫폼 고유의 호출에 의해 변수에 대한 멀티스레드 액세스를 보호해야 합니다.

플랫폼 특수성을 추상화하는 라이브러리가 있으며, 향후 C++ 규격은 스레드에 대응하기 위해 메모리 모델을 채택했습니다(따라서 스레드 안전성을 보장할 수 있습니다).

x86에 관한 이 어셈블리 레슨에 따르면 레지스터를 아토믹하게 메모리 위치에 추가할 수 있으므로 잠재적으로 당신의 코드는 '+i' 또는 'i++'를 아토믹하게 실행할 수 있습니다.그러나 다른 게시물에서 언급했듯이 Cansi는 '+' 연산에는 원자성을 적용하지 않기 때문에 컴파일러가 무엇을 생성할지 확신할 수 없습니다.

스레드 로컬 스토리지에 i를 넣습니다.원자는 아니지만 상관없습니다.

에 따라서, AFIK, C++ 의 판독합니다.int자입니니다다

그러나 데이터 경쟁과 관련된 정의되지 않은 동작을 제거하는 것이 전부입니다.

두 가 모두 증가하려고 됩니다.i.

다음과 같은 시나리오를 상정합니다.

let let 렛츠고i = 0★★★★★★★★★★★★★★★★★★:

스레드 A는 메모리에서 값을 읽고 자체 캐시에 저장합니다.스레드 A는 값을 1씩 증가시킵니다.

스레드 B는 메모리에서 값을 읽어 자체 캐시에 저장합니다.스레드 B는 값을 1씩 증가시킵니다.

의 스레드일 , 「」가 .i = 2기억 속에.

단,각가 그 는 "A"를 씁니다.i = 1「B」라고 써넣습니다.i = 1기억하기 위해서.

명확하게 정의되어 있습니다.부분적인 파괴나 건설이나 물체의 찢김 같은 것은 없지만, 여전히 데이터 경쟁입니다.

으로 증가시키기 i쓸 수 요.

std::atomic<int>::fetch_add(1, std::memory_order_relaxed)

이 작업은 어디서 수행되든 상관없이 증분 작업이 원자적이므로 여유로운 순서를 사용할 수 있습니다.

"단 하나의 명령일 뿐 콘텍스트 스위치로 중단되지 않습니다."라고 말합니다.-그것은 단일 CPU로 충분합니다만, 듀얼 코어 CPU는 어떻습니까?그러면 컨텍스트스위치 없이2개의 스레드가 같은 변수에 동시에 액세스 할 수 있습니다.

언어를 모르면, 그 답을 시험해 보는 것이다.

멀티코어 환경에서 스레드 간에 int를 공유하는 경우 적절한 메모리 장벽이 필요합니다.즉, 인터락된 명령을 사용할 수 있습니다(인터락된 참조).예를 들어 win32가 증가하거나 특정 스레드 세이프 보증을 하는 언어(또는 컴파일러)를 사용합니다.CPU 레벨의 명령 순서 변경이나 캐시등의 문제에 대해서는, 이러한 보증이 없는 한, 스레드간에 공유되는 어떠한 것도 안전하다고는 생각하지 말아 주세요.

편집: 대부분의 아키텍처에서 한 가지 가정할 수 있는 것은 올바르게 정렬된 단일 단어를 다룰 경우 두 가지 가치의 조합이 포함된 단일 단어를 사용할 수 없다는 것입니다.두 개의 쓰기가 서로 겹쳐지면 하나는 이기고 다른 하나는 폐기됩니다.주의하면 이 점을 이용하여 ++i 또는 i++ 중 하나가 싱글 라이터/멀티 리더 상황에서 스레드 세이프함을 알 수 있습니다.

C/C++ 의 x86/Windows 에서는, 스레드 세이프인 것을 상정하지 말아 주세요.Interlocked를 사용해야 합니다.원자 연산이 필요한 경우 Increment() 및 InterlockedDecrement().

증가분이 원자 연산으로 컴파일된다고 가정하지 마십시오.연동 사용타겟 플랫폼에 존재하는 증가 또는 유사한 기능.

편집: 방금 이 질문을 검색했습니다.X86은 싱글 프로세서 시스템에서는 원자적으로 증가하지만 멀티 프로세서 시스템에서는 증가하지 않습니다.잠금 접두사를 사용하면 원자적으로 만들 수 있지만 인터락을 사용하는 것만으로 훨씬 휴대성이 향상됩니다.인크리먼트

1998년 C++ 규격은 스레드에 대해 할 말이 없지만, 다음 표준(올해 또는 다음)에는 할 말이 없습니다.따라서 실장을 언급하지 않고서는 운용의 스레드 안전성에 대해 지적할 수 없습니다.프로세서뿐만 아니라 컴파일러, OS, 스레드 모델의 조합도 중요합니다.

반대로 문서가 없다면, 특히 멀티코어 프로세서(또는 멀티프로세서 시스템)에서는 어떠한 액션도 스레드 세이프하다고는 생각하지 않습니다.스레드 동기 문제는 우연히 발생할 가능성이 높기 때문에 테스트도 신뢰하지 않습니다.

사용 중인 특정 시스템용이라는 설명서가 없으면 스레드 세이프가 되지 않습니다.

i++라는 표현만이 문장에 있는 경우, ++i에 해당하며, 컴파일러는 시간적인 값을 유지할 수 없을 정도로 스마트하다고 생각합니다.따라서 서로 바꿔서 사용할 수 있다면(그렇지 않으면 어떤 것을 사용할지 묻지 않아도 됩니다), 어느 것을 사용하든 거의 비슷하기 때문에(미학 이외에는) 상관없습니다.

어쨌든 increment 연산자가 atomic인 경우에도 올바른 잠금을 사용하지 않으면 나머지 계산이 일관된다는 보장은 없습니다.

직접 실험하고 싶다면 N개의 스레드가 각각 공유 변수를 M배로 동시에 증가시키는 프로그램을 작성하십시오.값이 N*M보다 작을 경우 일부 증분이 덮어쓰기되었습니다.프리 인크리먼트와 포스트 인크리먼트 양쪽에서 시험해 보고 말해 주세요;-)

카운터에는 잠금 기능과 스레드 세이프 기능이 모두 있는 비교 및 스왑 기능을 사용하는 것이 좋습니다.

다음은 자바어입니다.

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}

언급URL : https://stackoverflow.com/questions/680097/ive-heard-i-isnt-thread-safe-is-i-thread-safe

반응형