이 스레드가 단일 코어 CPU에서 실행된다고 가정하십시오. CPU는 한 번에 하나의 명령 만 실행합니다. 즉, CPU 리소스를 공유한다고 생각했습니다. 그러나 컴퓨터는 한 번에 하나의 명령을 보장합니다. 따라서 다중 스레딩에 잠금이 필요하지 않습니까?
이 스레드가 단일 코어 CPU에서 실행된다고 가정하십시오. CPU는 한 번에 하나의 명령 만 실행합니다. 즉, CPU 리소스를 공유한다고 생각했습니다. 그러나 컴퓨터는 한 번에 하나의 명령을 보장합니다. 따라서 다중 스레딩에 잠금이 필요하지 않습니까?
답변:
이것은 예제와 함께 가장 잘 설명됩니다.
여러 번 병렬로 수행하려는 간단한 작업이 있고 작업이 수행 된 횟수 (예 : 웹 페이지의 적중 수 계산)를 전체적으로 추적하려고한다고 가정합니다.
각 스레드가 카운트를 증가시키는 지점에 도달하면 실행은 다음과 같습니다.
이 프로세스의 모든 시점에서 모든 스레드가 일시 중단 될 수 있습니다. 따라서 스레드 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 단계 이후 다른 스레드에 의해 값이 변경 되었기 때문에 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 개의 스레드가 일시 중단되고 잠금 지점에 도착하는 순서대로 각 스레드를 실행합니다.
직렬화가 중요하지 않은 경우 (증가 사례에서와 같이) 숫자 업데이트에 실패 할 경우 손실되는 계산이 최소 인 경우 비교 및 스왑 작업을 사용하면 얻을 수있는 이점이 있습니다. 잠금보다 저렴합니다.
이 인용문을 고려하십시오.
어떤 사람들은 문제에 직면했을 때,“나도 알아 볼게요.”라고 생각하고 나서 두 사람은 poblesms
한 번에 하나의 명령이 CPU에서 실행 되더라도 컴퓨터 프로그램은 단순한 원자 조립 명령 이상의 기능을 수행합니다. 예를 들어, 콘솔 (또는 파일)에 쓰려면 원하는대로 작동하도록하려면 잠 가야합니다.
많은 답변이 잠금을 설명하려고 시도한 것 같지만 OP에 필요한 것은 실제로 멀티 태스킹이 무엇인지에 대한 설명이라고 생각합니다.
하나의 CPU로도 시스템에서 두 개 이상의 스레드를 실행하는 경우 이러한 스레드의 예약 방법을 결정하는 두 가지 주요 방법이 있습니다 (예 : 단일 코어 CPU로 실행).
문제는 개별 작업과 관련이 없지만 작업이 수행하는 더 큰 작업입니다.
많은 알고리즘은 작동하는 상태를 완전히 제어 할 수 있다는 가정하에 작성되었습니다. 설명하는 것과 같이 인터리브 순서가 지정된 실행 모델을 사용하면 작업이 임의로 인터리브 될 수 있으며 상태를 공유하는 경우 상태가 일치하지 않을 위험이 있습니다.
불변을 일시적으로 중단 시켜서 수행하는 작업을 수행 할 수있는 함수와 비교할 수 있습니다. 중개 상태가 외부에서 관찰되지 않는 한, 그들은 임무를 달성하기 위해 원하는 모든 것을 할 수 있습니다.
동시 코드를 작성할 때는 독점 액세스 권한이 없으면 컨 텐션 상태가 안전하지 않은 것으로 간주되어야합니다. 독점적 액세스를 달성하는 일반적인 방법은 잠금을 유지하는 것과 같은 동기화 기본에서 동기화하는 것입니다.
동기화 프리미티브가 일부 플랫폼에서 발생하는 또 다른 것은 메모리 장벽을 방출하여 메모리의 CPU 간 일관성을 보장한다는 것입니다.
'bool'을 설정하는 것을 제외하고는 변수를 읽거나 쓰는 데 단 하나의 명령 만 필요하다는 것을 보증하지 않습니다 (적어도 c에서는).
bool
속성 만 가질 수 있습니까? 그리고 메모리에서로드, 변경 및 메모리로 푸시에 대해 이야기하고 있습니까, 아니면 레지스터 수준에서 이야기하고 있습니까? 레지스터에 대한 모든 읽기 / 쓰기는 중단되지 않지만 mem로드 및 mem 저장은 수행되지 않습니다 (그 자체만으로는 2 개의 명령어이므로 값을 변경하려면 적어도 1 개 더 있음).
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
은 실제로 답변에 추가되어야합니다.
CPU는 한 번에 하나의 명령을 실행하지만 둘 이상의 CPU가 있으면 어떻게합니까?
주어진 명령어에서 실행이 중단되지 않고 다른 프로세서의 간섭이없는 명령어 인 원자 명령어를 이용하도록 프로그램을 작성할 수 있다면 잠금이 필요하지 않다는 것이 맞습니다.
여러 명령어가 간섭으로부터 보호되어야하고 동등한 원자 명령어가없는 경우 잠금이 필요합니다.
예를 들어, 이중 연결 목록에 노드를 삽입하려면 여러 메모리 위치를 업데이트해야합니다. 삽입 전과 삽입 후, 특정 불변 은리스트의 구조를 유지합니다. 그러나 삽입하는 동안 이러한 불변은 일시적으로 손상됩니다. 목록이 "구성 중"상태입니다.
고정되지 않은 상태에서 다른 스레드가 목록을 통해 행진하거나 이러한 상태 일 때이를 수정하려고하면 데이터 구조가 손상되어 예상치 못한 동작이 발생합니다. 소프트웨어가 중단되거나 잘못된 결과가 계속 될 수 있습니다. 따라서 스레드가 목록이 업데이트 될 때 서로의 방식을 벗어나는 데 동의해야합니다.
원자 적 명령으로 적절히 설계된 목록을 조작 할 수 있으므로 잠금이 필요하지 않습니다. 이를위한 알고리즘을 "자유 잠금"이라고합니다. 그러나 원자 명령어는 실제로 잠금 형식입니다. 그것들은 특별히 하드웨어로 구현되며 프로세서 간의 통신을 통해 작동합니다. 원자 적이 지 않은 유사한 명령어보다 비쌉니다.
고급 원자 명령어가없는 멀티 프로세서에서는 상호 배제를위한 프리미티브가 간단한 메모리 액세스 및 폴링 루프로 구성되어야합니다. 이러한 문제는 Edsger Dijkstra 및 Leslie Lamport와 같은 사람들이 작업했습니다.