단일 코어 CPU에서 여러 스레드에 잠금이 필요한 이유를 설명 할 수 있습니까?


18

이 스레드가 단일 코어 CPU에서 실행된다고 가정하십시오. CPU는 한 번에 하나의 명령 만 실행합니다. 즉, CPU 리소스를 공유한다고 생각했습니다. 그러나 컴퓨터는 한 번에 하나의 명령을 보장합니다. 따라서 다중 스레딩에 잠금이 필요하지 않습니까?


소프트웨어 트랜잭션 메모리가 아직 주류가 아니기 때문입니다.
dan_waterworth

@dan_waterworth 소프트웨어 트랜잭션 메모리는 사소한 복잡성 수준에서 심각하게 실패하지 않기 때문에 의미합니까? ;)
Mason Wheeler

리치 히키가 그 말에 동의하지 않을 것입니다.
Robert Harvey

@MasonWheeler, 사소한 잠금은 놀랍도록 잘 작동하며 추적하기 어려운 미묘한 버그의 소스가 된 적이 있습니까? STM은 사소한 복잡성 수준에서는 잘 작동하지만 경합이있을 때는 문제가됩니다. 이러한 경우, 같은 STM의 더 제한적인 형태이며, 더 나은입니다. Btw, 제목 변경으로, 내가 왜 그렇게 댓글을 달았는지 알아내는 데 시간이 걸렸습니다.
dan_waterworth

답변:


32

이것은 예제와 함께 가장 잘 설명됩니다.

여러 번 병렬로 수행하려는 간단한 작업이 있고 작업이 수행 된 횟수 (예 : 웹 페이지의 적중 수 계산)를 전체적으로 추적하려고한다고 가정합니다.

각 스레드가 카운트를 증가시키는 지점에 도달하면 실행은 다음과 같습니다.

  1. 메모리에서 프로세서 레지스터로 적중 횟수 읽기
  2. 그 숫자를 늘리십시오.
  3. 그 숫자를 다시 메모리에 쓰기

이 프로세스의 모든 시점에서 모든 스레드가 일시 중단 될 수 있습니다. 따라서 스레드 A가 1 단계를 수행 한 다음 스레드 B가 3 단계를 모두 수행하여 일시 중단되면 스레드 A가 재개 될 때 레지스터의 히트 수가 잘못됩니다. 레지스터가 복원되면 이전 수를 행복하게 증가시킵니다 조회수를 늘리고 증가 된 수를 저장합니다.

또한 스레드 A가 일시 중단 된 시간 동안 다른 스레드 수에 관계없이 실행될 수 있으므로 끝에 스레드 A 쓰기 수가 올바른 수보다 훨씬 적을 수 있습니다.

따라서 스레드가 1 단계를 수행하는 경우 다른 스레드가 1 단계를 수행하기 전에 3 단계를 수행해야합니다. 이는 모든 스레드가이 프로세스를 시작하기 전에 단일 잠금을 얻기 위해 대기 할 수 있도록합니다. , 프로세스가 완료된 후에 만 ​​잠금을 해제하여이 코드의 "중요한 부분"을 잘못 인터리브 할 수 없어 잘못된 카운트가됩니다.

그러나 작업이 원자 적이라면 어떨까요?

그렇습니다. 증가 연산이 원자적인 마법의 유니콘과 무지개의 땅에서는 위의 예에서는 잠금이 필요하지 않습니다.

그러나 우리는 마법의 유니콘과 무지개의 세계에서 시간이 거의 없다는 것을 인식하는 것이 중요합니다. 거의 모든 프로그래밍 언어에서 증분 연산은 위의 세 단계로 나뉩니다. 프로세서가 원자 단위 증분 연산을 지원하더라도 그 연산은 훨씬 더 비쌉니다. 메모리에서 읽고 숫자를 수정 한 후 다시 메모리에 써야합니다. 일반적으로 원자 증분 연산은 실패 할 수 있습니다. 즉, 위의 간단한 시퀀스를 루프로 교체해야합니다 (아래에서 볼 수 있음).

다중 스레드 코드에서도 많은 변수가 단일 스레드에 대해 로컬로 유지되므로 각 변수가 단일 스레드에 대해 로컬이라고 가정하고 프로그래머가 스레드간에 공유 상태를 보호 할 수 있도록하면 프로그램이 훨씬 효율적입니다. 특히 원자 작업은 일반적으로 나중에 볼 수 있듯이 스레딩 문제를 해결하기에 충분하지 않습니다.

휘발성 변수

이 특정 문제에 대한 잠금을 피하려면 먼저 첫 번째 예제에 묘사 된 단계가 실제로 현대 컴파일 된 코드에서 발생하는 것이 아님을 알아야합니다. 컴파일러는 하나의 스레드 만 변수를 수정한다고 가정하기 때문에 프로세서 레지스터가 다른 것에 필요할 때까지 각 스레드는 자체 캐시 된 변수 사본을 유지합니다. 캐시 된 사본이있는 한 메모리로 돌아가서 다시 읽을 필요가 없다고 가정합니다 (비용이 많이 듭니다). 또한 레지스터에 유지되는 한 변수를 메모리에 다시 쓰지 않습니다.

변수를 volatile 로 표시하여 첫 번째 예제 (위에서 식별 한 것과 동일한 스레딩 문제와 함께)에서 제공 한 상황으로 돌아갈 수 있습니다. 이 변수는 컴파일러에이 변수가 다른 변수에 의해 수정되고 있으므로 읽어야합니다. 액세스하거나 수정할 때마다 메모리에 기록됩니다.

휘발성으로 표시된 변수는 원자 증가 연산의 땅으로 우리를 데려 가지 않을 것입니다. 우리가 이미 생각했던 것처럼 우리를 가깝게 만듭니다.

증분 원자 만들기

휘발성 변수를 사용하면 대부분의 최신 CPU가 지원하는 낮은 수준의 조건부 설정 작업 (종종 비교 및 ​​설정 또는 비교 및 스왑 이라고 함)을 사용하여 증분 작업을 원 자성으로 만들 수 있습니다 . 이 접근법은 예를 들어 Java의 AtomicInteger 클래스에서 수행됩니다.

197       /**
198        * Atomically increments by one the current value.
199        *
200        * @return the updated value
201        */
202       public final int incrementAndGet() {
203           for (;;) {
204               int current = get();
205               int next = current + 1;
206               if (compareAndSet(current, next))
207                   return next;
208           }
209       }

위의 루프는 3 단계가 완료 될 때까지 다음 단계를 반복적으로 수행합니다.

  1. 휘발성 변수의 값을 메모리에서 직접 읽습니다.
  2. 그 가치를 높이십시오.
  3. 주 메모리의 현재 값이 특수 원자 연산을 사용하여 처음 읽은 값과 동일한 경우에만 주 메모리의 값을 변경하십시오.

1 단계 이후 다른 스레드에 의해 값이 변경 되었기 때문에 3 단계가 실패하면 다시 주 메모리에서 변수를 다시 읽고 다시 시도합니다.

비교 및 스왑 작업은 비용이 많이 들지만 1 단계 이후에 스레드가 일시 중단되면 1 단계에 도달 한 다른 스레드는 첫 번째 스레드를 차단하고 기다릴 필요가 없기 때문에이 경우 잠금을 사용하는 것보다 약간 낫습니다. 값 비싼 컨텍스트 전환을 방지 할 수 있습니다. 첫 번째 스레드가 재개되면 변수를 처음으로 작성하는 데 실패하지만 변수를 다시 읽음으로써 계속할 수 있습니다. 이는 다시 잠금에 필요한 컨텍스트 스위치보다 비용이 적게 듭니다.

따라서 비교 및 ​​스왑을 통해 실제 잠금을 사용하지 않고도 원자 단위 증가 (또는 단일 변수의 다른 작업)에 도달 할 수 있습니다.

그렇다면 언제 잠금이 꼭 필요한가요?

원자 연산에서 둘 이상의 변수를 수정해야하는 경우 잠금이 필요하며, 이에 대한 특별한 프로세서 명령어를 찾을 수 없습니다.

그러나 단일 변수로 작업하고 있고 실패하고 변수를 읽고 다시 시작하기 위해 수행 한 모든 작업에 대한 준비가되어 있으면 비교 및 ​​스왑이 충분합니다.

각 스레드가 먼저 변수 X에 2를 더한 다음 X에 2를 곱하는 예를 살펴 보겠습니다.

X가 처음에 1이고 2 개의 스레드가 실행되면 결과는 (((1 + 2) * 2) + 2) * 2 = 16이됩니다.

그러나 스레드가 인터리빙되면 모든 연산이 원자 적 인 경우에도 두 개의 덧셈이 먼저 발생하고 곱셈이 수행되어 (1 + 2 + 2) * 2 * 2 = 20이됩니다.

곱셈과 덧셈은 정식 연산이 아니기 때문에 발생합니다.

따라서 원자 자체 인 작업 자체로는 충분하지 않으므로 작업 조합을 원 자성으로 만들어야합니다.

잠금을 사용하여 프로세스를 직렬화하거나 계산을 시작할 때 하나의 로컬 변수를 사용하여 X 값을 저장하고 중간 단계의 두 번째 로컬 변수를 비교 한 다음 비교 및 ​​스왑을 사용하여 X의 현재 값이 X의 원래 값과 동일한 경우에만 새 값을 설정하십시오. 실패하면 X를 읽고 계산을 다시 수행하여 다시 시작해야합니다.

계산이 길어질수록 실행중인 스레드가 중단 될 가능성이 높아지고 다시 시작하기 전에 다른 스레드에 의해 값이 수정되므로 실패 가능성이 훨씬 높아져 낭비가 발생합니다. 프로세서 시간. 매우 긴 계산을 수행하는 많은 수의 스레드의 극단적 인 경우 100 개의 스레드가 변수를 읽고 계산에 참여할 수 있습니다.이 경우 첫 번째 완료 만 새 값을 쓰는 데 성공하고 나머지 99는 여전히 계산을 완료하지만 완료되면 값을 업데이트 할 수 없음을 발견합니다. 어느 시점에서 그들은 각각 값을 읽고 계산을 시작합니다. 나머지 99 개의 스레드가 동일한 문제를 반복하여 엄청난 양의 프로세서 시간을 낭비했을 것입니다.

잠금을 통한 중요 섹션의 전체 직렬화는 해당 상황에서 훨씬 더 좋습니다. 잠금을 얻지 못하면 99 개의 스레드가 일시 중단되고 잠금 지점에 도착하는 순서대로 각 스레드를 실행합니다.

직렬화가 중요하지 않은 경우 (증가 사례에서와 같이) 숫자 업데이트에 실패 할 경우 손실되는 계산이 최소 인 경우 비교 및 ​​스왑 작업을 사용하면 얻을 수있는 이점이 있습니다. 잠금보다 저렴합니다.


그러나 카운터 무취 제가 원 자성이라면 자물쇠가 필요합니까?
pythonee

@ pythonee : 카운터 증가가 원자 적이라면 가능하지 않습니다. 그러나 합리적인 크기의 멀티 스레드 프로그램에서는 공유 자원에 대해 원자가 아닌 작업을 수행해야합니다.
Doc Brown

1
증분 원자를 만들기 위해 내장 컴파일러를 사용하지 않는 한 아마도 그렇지 않습니다.
Mike Larsen

예, 읽기 / 수정 (증가) / 쓰기가 원자 적이면 해당 작업에 대해 잠금이 필요하지 않습니다. DEC-10 AOSE (하나를 추가하고 result == 0 인 경우 건너 뛰기) 명령어는 구체적으로 원 자성으로 만들어져 테스트 및 설정 세마포어로 사용될 수 있습니다. 매뉴얼은 36 비트 레지스터를 완전히 롤링하는 데 며칠 동안 기계를 계산해야하기 때문에 충분하다고 언급했습니다. 그러나 지금 당신이하는 모든 것이 "메모리에 하나를 추가"하는 것은 아닙니다.
John R. Strohm

이러한 우려를 해결하기 위해 내 대답을 업데이트했습니다. 네, 당신은 작업을 원자 적으로 만들 수는 있지만, 그것을 지원하는 아키텍처에서도 기본적으로 원자가 아니며 원 자성이 그렇지 않은 상황이 있습니다. 충분하고 완전한 직렬화가 필요합니다. 잠금은 전체 직렬화를 달성하기 위해 알고있는 유일한 메커니즘입니다.
Theodore Murdock

4

이 인용문을 고려하십시오.

어떤 사람들은 문제에 직면했을 때,“나도 알아 볼게요.”라고 생각하고 나서 두 사람은 poblesms

한 번에 하나의 명령이 CPU에서 실행 되더라도 컴퓨터 프로그램은 단순한 원자 조립 명령 이상의 기능을 수행합니다. 예를 들어, 콘솔 (또는 파일)에 쓰려면 원하는대로 작동하도록하려면 잠 가야합니다.


나는 견적이 스레드가 아닌 정규 표현식이라고 생각 했습니까?
user16764

3
인용문은 나에게 스레드에 훨씬 더 적합하게 보입니다 (스레딩 문제로 인해 단어 / 문자가 순서대로 인쇄되지 않음). 그러나 현재 출력에 여분의 "s"가 있으므로 코드에 세 가지 문제가 있음을 나타냅니다.
시어 도어 머독

1
부작용입니다. 아주 가끔 당신은 1 추가 플러스 1과 4294967295 :) 얻을 수
gbjbaanb

3

많은 답변이 잠금을 설명하려고 시도한 것 같지만 OP에 필요한 것은 실제로 멀티 태스킹이 무엇인지에 대한 설명이라고 생각합니다.

하나의 CPU로도 시스템에서 두 개 이상의 스레드를 실행하는 경우 이러한 스레드의 예약 방법을 결정하는 두 가지 주요 방법이 있습니다 (예 : 단일 코어 CPU로 실행).

  • 협업 멀티 태스킹-Win9x 에서 사용되는 각 응용 프로그램은 명시 적으로 제어권을 포기해야했습니다. 이 경우 스레드 A가 일부 알고리즘을 실행하는 한 잠금에 대해 걱정할 필요가 없으므로 중단되지 않습니다.
  • 선점 형 멀티 태스킹 -대부분의 최신 OS (Win2k 이상)에서 사용됩니다. 이것은 타임 슬라이스를 사용하며 여전히 작업을 수행하더라도 스레드를 중단시킵니다. 단일 스레드가 전체 시스템을 정지시킬 수 없기 때문에 훨씬 강력합니다. 이는 멀티 태스킹의 협력 가능성이었습니다. 반면에 이제는 주어진 시간에 스레드 중 하나가 중단 (선점)되고 OS가 다른 스레드가 실행되도록 예약 할 수 있으므로 잠금에 대해 걱정해야합니다. 이 동작으로 다중 스레드 응용 프로그램을 코딩 할 때는 모든 코드 줄 (또는 모든 명령)간에 다른 스레드가 실행될 수 있음을 고려해야합니다. 이제 단일 코어에서도 데이터의 일관성있는 상태를 유지하려면 잠금이 매우 중요합니다.

0

문제는 개별 작업과 관련이 없지만 작업이 수행하는 더 큰 작업입니다.

많은 알고리즘은 작동하는 상태를 완전히 제어 할 수 있다는 가정하에 작성되었습니다. 설명하는 것과 같이 인터리브 순서가 지정된 실행 모델을 사용하면 작업이 임의로 인터리브 될 수 있으며 상태를 공유하는 경우 상태가 일치하지 않을 위험이 있습니다.

불변을 일시적으로 중단 시켜서 수행하는 작업을 수행 할 수있는 함수와 비교할 수 있습니다. 중개 상태가 외부에서 관찰되지 않는 한, 그들은 임무를 달성하기 위해 원하는 모든 것을 할 수 있습니다.

동시 코드를 작성할 때는 독점 액세스 권한이 없으면 컨 텐션 상태가 안전하지 않은 것으로 간주되어야합니다. 독점적 액세스를 달성하는 일반적인 방법은 잠금을 유지하는 것과 같은 동기화 기본에서 동기화하는 것입니다.

동기화 프리미티브가 일부 플랫폼에서 발생하는 또 다른 것은 메모리 장벽을 방출하여 메모리의 CPU 간 일관성을 보장한다는 것입니다.


0

'bool'을 설정하는 것을 제외하고는 변수를 읽거나 쓰는 데 단 하나의 명령 만 필요하다는 것을 보증하지 않습니다 (적어도 c에서는).


32 비트 정수를 설정하는 데 몇 개의 명령어가 필요합니까?
DXM

1
첫 번째 진술을 조금 확장 할 수 있습니까? 부울 만 원자 적으로 읽거나 쓸 수는 있지만 의미가 없습니다. "bool"은 실제로 하드웨어에 존재하지 않습니다. 일반적으로 바이트 또는 단어로 구현되므로 어떻게이 bool속성 만 가질 수 있습니까? 그리고 메모리에서로드, 변경 및 메모리로 푸시에 대해 이야기하고 있습니까, 아니면 레지스터 수준에서 이야기하고 있습니까? 레지스터에 대한 모든 읽기 / 쓰기는 중단되지 않지만 mem로드 및 mem 저장은 수행되지 않습니다 (그 자체만으로는 2 개의 명령어이므로 값을 변경하려면 적어도 1 개 더 있음).
코빈

1
하이퍼 레딩 / 멀티 코어 / 분기 예측 / 멀티 캐시 CPU에서 단일 명령의 개념은 약간 까다 롭지 만 표준은 읽기 / 쓰기 중간에 컨텍스트 스위치에 대해 'bool'만 안전해야한다고 말합니다. 단일 변수의. 다른 유형을 둘러싼 뮤텍스를 감싸는 boost :: Atomic이 있으며 c ++ 11에 스레딩 보증이 더 있다고 생각합니다.
Martin Beckett

설명 the standard says that only 'bool' needs to be safe against a context switch in the middle of a read/write of a single variable은 실제로 답변에 추가되어야합니다.
Wolf

0

공유 메모리

그것은 스레드 의 정의입니다 : 공유 메모리를 가진 많은 동시 프로세스.

공유 메모리가없는 경우 일반적으로 old-school-UNIX 프로세스 라고 합니다.
그러나 공유 파일에 액세스 할 때 잠금이 필요할 수 있습니다.

(유닉스 계열 커널의 공유 메모리는 실제로 공유 메모리 주소를 나타내는 가짜 파일 디스크립터를 사용하여 구현되었습니다)


0

CPU는 한 번에 하나의 명령을 실행하지만 둘 이상의 CPU가 있으면 어떻게합니까?

주어진 명령어에서 실행이 중단되지 않고 다른 프로세서의 간섭이없는 명령어 인 원자 명령어를 이용하도록 프로그램을 작성할 수 있다면 잠금이 필요하지 않다는 것이 맞습니다.

여러 명령어가 간섭으로부터 보호되어야하고 동등한 원자 명령어가없는 경우 잠금이 필요합니다.

예를 들어, 이중 연결 목록에 노드를 삽입하려면 여러 메모리 위치를 업데이트해야합니다. 삽입 전과 삽입 후, 특정 불변 은리스트의 구조를 유지합니다. 그러나 삽입하는 동안 이러한 불변은 일시적으로 손상됩니다. 목록이 "구성 중"상태입니다.

고정되지 않은 상태에서 다른 스레드가 목록을 통해 행진하거나 이러한 상태 일 때이를 수정하려고하면 데이터 구조가 손상되어 예상치 못한 동작이 발생합니다. 소프트웨어가 중단되거나 잘못된 결과가 계속 될 수 있습니다. 따라서 스레드가 목록이 업데이트 될 때 서로의 방식을 벗어나는 데 동의해야합니다.

원자 적 명령으로 적절히 설계된 목록을 조작 할 수 있으므로 잠금이 필요하지 않습니다. 이를위한 알고리즘을 "자유 잠금"이라고합니다. 그러나 원자 명령어는 실제로 잠금 형식입니다. 그것들은 특별히 하드웨어로 구현되며 프로세서 간의 통신을 통해 작동합니다. 원자 적이 지 않은 유사한 명령어보다 비쌉니다.

고급 원자 명령어가없는 멀티 프로세서에서는 상호 배제를위한 프리미티브가 간단한 메모리 액세스 및 폴링 루프로 구성되어야합니다. 이러한 문제는 Edsger Dijkstra 및 Leslie Lamport와 같은 사람들이 작업했습니다.


참고로, 단일 비교 및 ​​스왑 만 사용하여 이중 연결 목록 업데이트를 처리하는 잠금없는 알고리즘을 읽었습니다. 또한 이중 비교 및 ​​스왑 (68040에서 구현되었지만 다른 68xxx 프로세서에서는 수행하지 않음)보다 하드웨어에서 훨씬 저렴할 것으로 보이는 시설에 대한 백서를 읽었습니다. -linked / store-conditional은 두 개의 링크 된로드 및 조건부 저장소를 허용하지만 두 저장소간에 발생하는 액세스는 첫 번째 롤백이 아니라는 조건이 있습니다. 이중 비교 및 ​​저장보다 구현이 훨씬 쉽습니다.
supercat

...하지만 이중 연결 목록 업데이트를 관리 할 때 비슷한 이점을 제공합니다. 내가 알 수있는 한, 이중 링크로드는 잡히지 않았지만 수요가 있으면 하드웨어 비용이 상당히 저렴 해 보일 것입니다.
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.