ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스레드 동기화란?
    @ 17. 1 ~ 18/C# 멀티스레드 2018. 3. 5. 00:36

    윈도우는 스레드가 스케줄링의 단위가 되는 선점형 멀티스레딩 시스템이다.

    그러므로 여러 개의 스레드가 동시에 돌아가는 솔루션에서 아무런 대책없이 스레드 사이에서 데이터를 공유할 경우 문제가 발생한다.


    두개의 스레드가 있다면 하나의 스레드 작업이 끝날때까지 다른 스레드는 기다리게 해줘야한다.

    기다리게 한다는 것은 2가지의 의미

    1. 상호배제, 즉 작원에 대한 독점적 점유를 목적으로 하나의 스레드가 해당자원을 사용하는 동안 다른 스레드들이 접근하지 못하도록 막는다는 측면

    2. 일반적 의미로는 작업의 성격상 각 스레드들의 순차적 실행을 보장해주는 측면

    결국은 막는다는 의미로 통일된다.


    동기화의 필요성

    다중 cpu가 되면서 더 중요해짐. 사실 단일 cpu일때도 엄청 중요했던건 사실임. 왜냐면 스레드 스케줄링에 의거해 스레드가 동시에

    돌아가는 효과를 만들어주었기 때문이다. 근데 다중 cpu는 실제로 동시에 여러 스레드가 돌아가기 때문에 더 중요해진것.


    1. 원자성의 문제. atomic 아토믹하다~

    a++;

    위의 연산처럼 단순하고 간단한것 까지 동기화를 해야하나? 

    실제로 기계어로 변환되면 3줄이다. 즉 3개의 명령어가 실행되어야 한다는 것.

    그래서 3개의 명령어 실행이 여러 스레드에 의해 이루어진다면 각 스레드별로 순차적으로 실행된다는 보장이 없음.

    원자성이란 하나의 연산이 값을 갱신하는 중간에 실패할 수 없으며 만약 실패 가능성이 있으면 다시 원래 상태로 되돌려야 한다.

    동기화란 결국 이 원자성을 보장해줘야한다.


    그런데 이 원자성은 연산 자체에서만 문제가 되는것이 아니다. 컴퓨터에서의 연산은 cpu의 레지스터와 메모리간의 연산이다.

    cpu가 작업을 하기 위해서는 메모리상의 데이터를 레지스터로 옮겨와 작업한 후 다시 그 결과를 메모리로 복사해야 끝나게 된다.

    결국 이 과정에서 원자성이 확보되어야 한다.

       * 64비트와 32비트에서 주의할점

    : 각각의 비트설정에 대해 원자성을 보장하기 떄문에 64비트의 값을 32비트 운영체제에서 메모리에 쓸 경우에는 기계어가 나뉘게 된다.

      원자성 보장이 안된다.


    원자성이 확보되어야 한다는 것은 여러 단계로 나눠진 명령어들의 실행을 한 스레드가 수행하는 동안 다른 스레드가

    그 중간에 끼어들지 못하도록 해줘야한 다는 것을 의미한다.

    여기서 원자성이 보장되어야 하는 블럭? 또는 코드 영역을 임계구역(크리티컬 섹션)이라고 한다.


    참고로 원자성 확보의 성공과는 별개로 코드 자체가 변경되는 경우가 있다.(결국 이 변경이 원자성 확보에 제한을 준다)

    바로 컴파일러의 옵션중에 컴파일시 최적화란 작업은 디버그 와 릴리즈일때의 어셈블리 명령어가 다르게 될 수 있다. 

    즉 명령어 재배치다. 결국 우리가 작성한 코드가 눈에 보이는 그 순서 그대로 기계어로 처리가 되지 않는다는 것.

    예를들어 cpu입장에서는 메모리로부터 값을 읽어오는 과정은 상당히 많은 시간이 걸리는 작업이다. 그래서 메모리로부터 데이터를 읽어오는부분이

    변경될 수 있다는 것이다.

    그럼 최적화 옵션을 끈다면? 별로 권장할 만한 방법은 아니다. 심지어 재배치는 컴파일러 말고 cpu에 의해서도 된다.

    아래는 코드 생성과정인데 검은색 3부분이 재배치가 일어나는 과정이다.

    컴파일러에 의한 최적화

    코드와 코드사이에 서로 읽기/쓰기 의존성이 없다면 바꿀 수 있다고 판단되어 변경된다.


    cpu에 의한 명령어 재배치

    cpu에서 명령어가 재비치된다는 것은 요즘 cpu가 명령어를 동시에 여러개를 실행하기 때문에 발생된다.


    cpu캐시효과

    cpu는 메인 메모리 의 느린 속도를 보정하기 위해 캐시를 사용. 메모리에서 읽어온 결과는 캐시에 저장되고 쓸때도 캐시에

    먼저 저장된다. 지역성에 근거해서 인접 데이터까지 미리 읽어서 캐시에 보관한다. 

    이러한 상황은 다중 cpu의 경우 동기화 문제를 야기시킨다.

    그래서 다중 cpu에선 캐시의 일관성을 제공하는데 한 cpu의 캐시값이 변경되면 다른 cpu로 하여금 변경된 부분의 일정영역을

    다시 메모리로부터 읽어오도록 한다. 근데. 이러면 효율이 떨어짐..


    결국 동기화란. 원자성 확보 뿐만이 아니라 작성한 순서대로 코드가 실행될 것을 보장해준다는 의미다.


    대안은?

    1) 해당 공유변수에는 volatile 키워드 사용

    그 키워드가 지시하는 것은 2가지다. 해당 선언된 변수에 대해서는 최적화를 수행하지 말 것과 동시에

    값을 메모리에서 읽어올 것을 지시한다.

    캐시의 일관성 문제를 미리 예방하기 위해 캐시를 거치지 않고 무조건 메모리에 읽기/쓰기를 수행하라고 지시하는 것이다.

    하지만, 단일 cpu 환경에서 컴파일러 재배치 문제는 해결해주지만 멀티cpu에 의한 재배치에 대해서는 완전한 대안을 제공하지 못하고 있다.

    그리고 변수를 읽은 후에 값을 수정하고 다시쓰는 read - modify - write를 원자적으로 수행할 수 있게 해주지도 못한다.


    2) 메모리 장벽

    앞에서 이야기한 cpu에 의한 또는 캐시 효과에 의한 명령어 재배치 문제를 해결하기 위해 대부분의 cpu는 메모리 장벽이라는 명령어를 제공한다.

    (메모리 펜스 또는 메모리 배리어라고 불림)

    메모리 장벽은 장벽 앞에 존재하는 메모리 읽기/쓰기 연산들이 장벽 뒤에 존재하는 읽기/쓰기 연산들에 앞서 메모리에 커밋되도록 보장해준다.

    cpu나 캐시에 의한 명령어 재배치의 목적은 궁극적으로 성능향상을 위한 것이다.  하지만 장벽을 사용하게 되면 이러한 장점을 이용할 수 없다.

    결론은 이러한 메모리 장벽 명령어들은 컴파일러에 의한 최적화를 위한 메모리 연산의 순서재배치를 또한 막아준다.(당연히 cpu에 의한 또는 캐시효과에 의한 명령어 재배치 문제도 해결해줌)

    VC++에서는 ReadBarrier와 WriteBarrier 그리고 ReadWriteBarrier를 제공한다.

    하지만 이것도 컴파일러에 의한 재배치만 해결해주지 더 민감한 cpu에 의한 명령여 재배치는 해결하지 못한다.

    ms는 그래서 cpu명령어에 대한 재배치 문제까지 고려해서 MemoryBarrier라는 매크로를 제공한다.


    3) 상호잠금(Interlocked) 함수의 사용

    a++ 와 같은 연산에 대해 원자성을 확보해주기 위한 많은 함수가 이 컴파일러내장함수로 제공되는데 이러한 함수들을 상호잠금 함수라고 한다.

    상호잠금 함수는 공유변수에 대한 간단한 연산에 대해 원자성을 보장해주는 동시에 컴파일러 내장 함수로 제공되기 때문에

    그 속도 또한 상당히 빠르다.



    1. 동기화의 요소

    1. 스레드를 멈추게 하는 작용 

    2. 멈출것인지 또는 진행 할 것인지에 대한 "판단 조건을 제공해주는 그 무엇"

    3. 조건을 직접 제어하는 것 즉, "판단 조건을 변경" 


    2. 동기화의 목적과 방법

    동기화를 고려한다는 것은 원자성의 확보 및 명령어 재배치 문제의 해결을 보장해줘야만 한다는 것이다.

    스레드를 움직이라고 지시하는 상태와 멈추라고 지시하는 상태의 표현을 아래와 같이 한다.

    시그널 상태 : 스레드가 움직일 수 있는 상태. ON의 경우 신호등이 초록불인 경우

    넌시그널 상태 : 스레드가 멈춰야만 하는 상태. OFF의 경우 신호등이 빨간색인 경우


    스레드는 대기 함수를 통해서 동기화 객체가 시그널 상태인지 넌 시그널 상태인지에 따라서 움직일 것인지 아니면 멈춰 대기할 것인지를

    판단한다.

      * 스레드와 프로세스도 동기화 객체로서 가능하다 그러면 이 두객체의 시그널 넌시그널 상태가 의미하는것은?

    - 시그널 : 스레드나 프로세스가 종료된 상태 

    - 넌 시그널 상태 : 스레드나 프로세스가 실행중인경우


    동기화객체는 크게 동기화를 위한 커널 객체와 유저 영역에서 제공되는 동기화 객체가 있다.

    커널 객체를 통한 동기화는 커널 영역에서 작동, 스레드를 실제로 잠들게한다.

    커널 객체를 통하면 유저 모드에서 커널 모드로 전환이 발생되며 스레드의 스케줄링 상태를 변경하게 된다. 스레드의 문맥전환이 유발됨. (오버헤드 발생)


    반면 유저 영역의 동기화 객체는 유저 주소공간에 객체의 인스턴스가 위치하며, 커널 객체 사용시의 그 오버헤드를 최소화된다.



    '@ 17. 1 ~ 18 > C# 멀티스레드' 카테고리의 다른 글

    읽기 / 쓰기 락  (0) 2018.06.27
    스레드 로컬 저장소  (0) 2018.06.21
    동시성 컬렉션 사용  (0) 2017.08.20
    async, await에 대한 설명  (0) 2017.08.19
    데드락 걸리는 async 코드  (0) 2017.08.19
Designed by Tistory.