잠금없는 멀티 스레딩은 실제 스레딩 전문가를위한 것입니다.


87

나는 Jon Skeet 이 질문에 대한 답변 을 읽고 있었고 그는 이것을 언급했습니다.

제가 아는 한, 잠금없는 멀티 스레딩은 실제 스레딩 전문가를위한 것입니다.

처음 들어 본 것은 아니지만 잠금없는 멀티 스레딩 코드를 작성하는 방법을 배우는 데 관심이 있다면 실제로 수행하는 방법에 대해 이야기하는 사람은 거의 없습니다.

그래서 내 질문은 스레딩에 대해 가능한 모든 것을 배우는 것 외에도 잠금이없는 멀티 스레딩 코드를 구체적으로 작성하는 방법과 좋은 리소스가 무엇인지 배우기 시작하는 곳입니다.

건배


저는 gcc, linux 및 X86 / X68 플랫폼을 사용합니다. Lock-free는 소리를내는 것만 큼 어렵지 않습니다! gcc 원자 내장 기능은 인텔에 메모리 장벽이 있지만 실제 생활에서는 중요하지 않습니다. 중요한 것은 메모리가 원자 적으로 수정된다는 것입니다. "잠금없는"데이터 구조를 디자인 할 때 다른 스레드가 변경 사항을 볼 때 문제가되지 않습니다. 단일 연결 목록, 건너 뛰기 목록, 해시 테이블, 무료 목록 등은 모두 잠금 해제가 매우 쉽습니다. 잠금 해제가 모든 것을위한 것은 아닙니다. 특정 상황에 적합한 또 다른 도구입니다.
johnnycrash


자원 추천으로 종결하도록 투표하거나 요청하는 내용을 명확하지 않습니다.
치로 틸리는郝海东冠状病六四事件法轮功

답변:


101

현재의 "잠금없는"구현은 대부분 동일한 패턴을 따릅니다.

  • * 주를 읽고 사본을 만드십시오 **
  • * 사본 수정 **
  • 연동 작업을하다
  • 실패하면 재시도

(* 선택 사항 : 데이터 구조 / 알고리즘에 따라 다름)

마지막 비트는 스핀 락과 매우 유사합니다. 사실 기본 스핀 락 입니다. :)
저는 이것에 대해 @nobugz에 동의합니다. 잠금없는 멀티 스레딩에서 사용되는 연동 작업의 비용 은 수행해야하는 캐시 및 메모리 일관성 작업에 의해 좌우됩니다 .

그러나 "잠금이없는"데이터 구조로 얻을 수있는 것은 "잠금"이 매우 세밀하다는 것 입니다. 이렇게하면 두 개의 동시 스레드가 동일한 "잠금"(메모리 위치)에 액세스 할 가능성이 줄어 듭니다.

대부분의 경우 트릭은 전용 잠금이 없다는 것입니다. 대신 배열의 모든 요소 또는 연결 목록의 모든 노드를 "스핀 잠금"으로 취급합니다. 마지막으로 읽은 이후 업데이트가 없으면 읽고 수정하고 업데이트를 시도합니다. 있는 경우 다시 시도하십시오.
이렇게하면 추가 메모리 또는 리소스 요구 사항을 도입하지 않고도 "잠금"(오, 죄송합니다, 비 잠금 :)이 매우 세밀 해집니다.
더 세밀하게 만들면 대기 확률이 감소합니다. 추가 리소스 요구 사항을 도입하지 않고 가능한 한 세분화하면 좋을 것 같지 않습니까?

그러나 대부분의 재미는 올바른 적재 / 점포 주문보장하는 데서 올 수 있습니다 .
직관과 달리 CPU는 메모리 읽기 / 쓰기 순서를 자유롭게 변경할 수 있습니다. 그런데 매우 똑똑합니다. 단일 스레드에서이를 관찰하는 데 어려움을 겪을 것입니다. 그러나 다중 코어에서 다중 스레딩을 시작하면 문제가 발생합니다. 당신의 직관은 무너질 것입니다. 단지 명령이 당신의 코드에서 더 일찍 나온다고해서 그것이 실제로 더 일찍 일어날 것이라는 것을 의미하지는 않습니다. CPU는 명령을 순서대로 처리 할 수 ​​있습니다. 특히 메모리 액세스가있는 명령에이 작업을 수행하여 주 메모리 대기 시간을 숨기고 캐시를 더 잘 활용하는 것을 좋아합니다.

이제 코드 시퀀스가 ​​"하향식"으로 흐르지 않고 마치 시퀀스가 ​​전혀없는 것처럼 실행되며 "악마의 놀이터"라고 불릴 수 있다는 것은 직감에 반합니다. 로드 / 스토어 재주문이 발생하는 것에 대해 정확한 답변을 제공하는 것은 불가능하다고 생각합니다. 대신에, 하나는 항상 측면에서 말하는 메이스mights 과 최악의 준비. "아, CPU 이 읽기를 쓰기 전에 오도록 재정렬 할 수 있으므로 바로 여기,이 지점에 메모리 장벽을 두는 것이 가장 좋습니다."

사항에도 이러한 사실에 의해 복잡 메이스mights는 CPU 아키텍처에 걸쳐 다를 수 있습니다. 그것은 , 예를 들면, 그 어떤 경우가 발생하지 보장 한 구조에서 발생할 수있는 또 다른에.


"잠금없는"멀티 스레딩을 제대로하려면 메모리 모델을 이해해야합니다.
그러나 메모리 모델을 얻고 올바른 것을 보장하는 것은 사소한 일이 아닙니다. 이 이야기에서 알 수MFENCE 있듯이 Intel과 AMD는 JVM 개발자들 사이에서 약간의 혼란 야기 하는 문서를 수정 했습니다 . 결과적으로 개발자가 처음부터 의존했던 문서는 처음에는 그렇게 정확하지 않았습니다.

.NET의 잠금은 암시 적 메모리 장벽을 생성하므로 안전하게 사용할 수 있습니다 (대부분의 경우 ... 예를 들어 Joe Duffy-Brad Abrams-Vance Morrison의 지연 초기화, 잠금, 휘발성 및 메모리의 위대함 을 참조하십시오. :) (해당 페이지의 링크를 따라 가십시오.)

추가 보너스로, 사이드 퀘스트에서 .NET 메모리 모델을 소개 받게됩니다 . :)

Vance Morrison의 "oldie but goldie"도 있습니다 : 모든 개발자가 멀티 스레드 앱에 대해 알아야 할 사항 .

... 물론 @Eric이 언급했듯이 Joe Duffy 는 주제에 대한 확실한 읽기입니다.

좋은 STM은 세분화 된 잠금에 가까워 질 수 있으며 아마도 손으로 만든 구현에 가깝거나 동등한 성능을 제공 할 것입니다. 그중 하나는 MS DevLabs 프로젝트STM.NET 입니다 .

.NET 전용 열광자가 아니라면 Doug Lea가 JSR-166에서 훌륭한 작업을 수행했습니다 .
Cliff Click 은 Java 및 .NET 동시 해시 테이블과 마찬가지로 잠금 스트라이핑에 의존하지 않는 해시 테이블을 흥미롭게 가지고 있으며 750 CPU로 잘 확장되는 것으로 보입니다.

Linux 영역에 도전하는 것을 두려워하지 않는다면, 다음 기사는 현재 메모리 아키텍처의 내부와 캐시 라인 공유가 성능을 어떻게 파괴 할 수 있는지에 대한 더 많은 통찰력을 제공합니다. 모든 프로그래머가 메모리에 대해 알아야하는 것 .

@Ben은 MPI에 대해 많은 의견을 남겼습니다. 저는 MPI가 일부 영역에서 빛을 발할 수 있다는 데 진심으로 동의합니다. MPI 기반 솔루션은 현명하게 시도하는 반 베이크 된 잠금 구현보다 추론하기 쉽고 구현하기 쉬우 며 오류 발생 가능성이 적습니다. (그러나-주관적으로-STM 기반 솔루션의 경우에도 마찬가지입니다.) 또한 많은 성공적인 예제에서 알 수 있듯이 예를 들어 Erlang에서 적절한 분산 응용 프로그램 을 올바르게 작성하는 것이 광년이 더 쉬울 것이라고 확신합니다 .

그러나 MPI는 단일 멀티 코어 시스템 에서 실행될 때 자체 비용과 문제가 있습니다 . 예를 들어 Erlang에서는 프로세스 스케줄링과 메시지 큐동기화와 관련하여 해결해야 할 문제가 있습니다 .
또한 MPI 시스템의 핵심은 일반적으로 "경량 프로세스"를위한 일종의 협력적인 N : M 스케줄링 을 구현합니다 . 예를 들어 이는 경량 프로세스간에 불가피한 컨텍스트 전환이 있음을 의미합니다. 이것이 "고전적인 컨텍스트 전환"이 아니라 대부분 사용자 공간 작업이고 빠르게 만들 수 있다는 것은 사실이지만 연동 작업이 걸리는 20-200 사이클 아래로 가져올 수 있는지 진심으로 의심합니다 . 사용자 모드 컨텍스트 전환은 확실히 느립니다.Intel McRT 라이브러리에서도. 경량 프로세스를 사용한 N : M 스케줄링은 새로운 것이 아닙니다. LWP는 오랫동안 솔라리스에있었습니다. 그들은 버려졌습니다. NT에는 섬유가있었습니다. 지금은 대부분 유물입니다. NetBSD에는 "활성화"가있었습니다. 그들은 버려졌습니다. 리눅스는 N : M 스레딩이라는 주제를 가지고있었습니다. 지금 쯤이면 다소 죽은 것 같습니다.
때때로 새로운 경쟁자가 있습니다. 예를 들어 Intel의 McRT 또는 Microsoft의 ConCRT 와 함께 가장 최근의 사용자 모드 스케줄링 . 가장 낮은 수준에서는 N : M MPI 스케줄러가 수행하는 작업을 수행합니다. Erlang 또는 모든 MPI 시스템은 새로운 UMS 를 활용하여 SMP 시스템에서 큰 이점을 얻을 수 있습니다 .

나는 OP의 질문이 어떤 솔루션에 대한 / 반대의 주관적인 주장의 장점에 관한 것이 아니라고 생각하지만, 대답해야한다면 작업에 달려 있다고 생각합니다. 단일 시스템 으로 많은 코어 , 중 낮은 잠금 / "잠금없는"기술 또는 STM 위의 주름이 밖으로 다림질 경우에도, 성능면에서 최상의 결과를 얻을 것입니다 아마 성능 현명한 MPI를 솔루션 언제든지 이길 것 예 : Erlang에서.
단일 시스템에서 실행되는 약간 더 복잡한 것을 빌드하려면 고전적인 거친 잠금을 선택하거나 성능이 큰 관심사 인 경우 STM을 선택합니다.
분산 시스템을 구축하기 위해 MPI 시스템은 아마도 자연스러운 선택을 할 것입니다.
가하는 것으로 MPI 구현 을위한 .NET뿐만 아니라 (그들은으로 활성화되지 것 같다하지만).


1
이 답변에는 많은 좋은 정보가 있지만 잠금없는 알고리즘과 데이터 구조가 본질적으로 매우 미세한 스핀 록의 모음 일 뿐이라는 헤드 라인 아이디어는 잘못되었습니다. 일반적으로 잠금이없는 구조에서 재시도 루프를 볼 수 있지만 동작은 매우 다릅니다. 잠금 (스핀 록 포함)은 독점적으로 일부 리소스를 획득하고 다른 스레드는 유지되는 동안 진행할 수 없습니다. 그런 의미에서 "재시도"는 단순히 독점 리소스가 해제되기를 기다리는 것입니다.
BeeOnRope

1
반면 잠금없는 알고리즘은 CAS 또는 기타 원자 적 명령어를 사용하여 독점 리소스를 획득하지 않고 일부 작업을 완료합니다. 실패하면 다른 스레드와의 일시적으로 세분화 된 경쟁으로 인해 다른 스레드가 진행 (작업 완료 )했기 때문 입니다. 스레드가 무기한으로 의심되는 경우 다른 모든 스레드는 계속 진행할 수 있습니다. 이것은 배타적 잠금과는 질적으로나 성능면에서 매우 다릅니다. "재시도"횟수는 대개 경합이 심한 경우에도 대부분의 CAS 루프에서 매우 적습니다 ...
BeeOnRope

1
... 물론 좋은 확장 성을 의미하지는 않습니다. 단일 메모리 위치에 대한 경합은 CAS 오류 수가 다음과 같더라도 코어 간 소켓 간 대기 시간으로 인해 SMP 시스템에서 항상 상당히 느려질 것입니다. 낮은.
BeeOnRope

1
@AndrasVass- "좋은"대 "나쁜"잠금없는 코드에도 의존한다고 생각합니다. 확실히 누구나 구조를 작성하고 잠금없는 상태라고 할 수 있지만 실제로는 사용자 모드 스핀 록을 사용하고 정의도 충족하지 못합니다. 또한 관심있는 독자라면 Herlihy와 Shavit 의이 문서 를 확인하여 잠금 기반 및 잠금없는 알고리즘의 다양한 범주를 공식적으로 살펴볼 것을 권장합니다 . 이 주제에 대한 Herlihy의 모든 것도 읽을 것을 권장합니다.
BeeOnRope 2017 년

1
@AndrasVass-동의하지 않습니다. 대부분의 고전적인 잠금없는 구조 (목록, 대기열, 동시 맵 등)는 공유 된 가변 구조에 대해서도 회전하지 않았으며, 예를 들어 Java에서 동일한 기존 구현은 동일한 패턴을 따릅니다 (나는 네이티브 컴파일 C 또는 C ++에서 사용할 수있는 기능에 익숙하며 가비지 수집이 없기 때문에 더 어렵습니다. 아마도 당신과 나는 회전에 대한 다른 정의를 가지고있을 것입니다. 나는 당신이 잠금없는 "회전"에서 발견되는 "CAS 재시도"를 고려하지 않습니다. IMO "회전"은 핫 대기를 의미합니다.
BeeOnRope

27

Joe Duffy의 책 :

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

그는 또한 이러한 주제에 대한 블로그를 작성합니다.

저 잠금 프로그램을 얻는 데 트릭은 바로 깊은 수준에서 이해하는 것입니다 정확하게 메모리 모델의 규칙은 하드웨어, 운영 체제 및 런타임 환경의 특정 조합에 무슨.

저는 개인적으로 InterlockedIncrement를 넘어서 올바른 로우 락 프로그래밍을 할 수있을만큼 똑똑한 사람이 아니지만, 당신이 훌륭하다면 그것을 시도하십시오. 코드에 많은 문서를 남겨 두어 실수로 메모리 모델 불변 중 하나를 깨뜨리고 찾기 불가능한 버그를 도입하지 않도록 똑똑하지 않은 사람들이 있는지 확인하십시오.


38
따라서 Eric LippertJon Skeet 모두 잠금없는 프로그래밍이 자신보다 똑똑한 사람들만을위한 것이라고 생각 한다면 , 즉시 아이디어에서 비명을 지르며 겸손하게 도망 칠 것입니다. ;-)
dodgy_coder

20

요즘 "잠금없는 스레딩"과 같은 것은 없습니다. 컴퓨터 하드웨어가 느리고 비싸던 지난 세기 말에 학계 등에 게 흥미로운 놀이터였습니다. Dekker의 알고리즘 은 항상 제가 가장 좋아하는 알고리즘 이었고, 현대적인 하드웨어는 그것을 방목했습니다. 더 이상 작동하지 않습니다.

두 가지 개발로 인해 RAM과 CPU의 속도 차이가 커지고 있습니다. 그리고 칩 제조업체가 하나의 칩에 둘 이상의 CPU 코어를 넣을 수있는 능력.

RAM 속도 문제로 인해 칩 설계자는 CPU 칩에 버퍼를 배치해야했습니다. 버퍼는 CPU 코어에서 빠르게 액세스 할 수있는 코드와 데이터를 저장합니다. 그리고 훨씬 느린 속도로 RAM에서 읽고 쓸 수 있습니다. 이 버퍼를 CPU 캐시라고하며 대부분의 CPU에는 두 개 이상이 있습니다. 1 단계 캐시는 작고 빠르며 2 단계 캐시는 크고 느립니다. CPU가 1 단계 캐시에서 데이터와 명령을 읽을 수있는 한 빠르게 실행됩니다. 캐시 미스는 비용이 많이 들고 데이터가 첫 번째 캐시에 없으면 최대 10주기 동안 CPU를 절전 모드로 전환하고 두 번째 캐시에 있지 않고 데이터를 읽어야하는 경우 최대 200주기 램.

모든 CPU 코어에는 자체 캐시가 있으며 자체 RAM "뷰"를 저장합니다. CPU가 데이터를 쓸 때 캐시에 기록한 다음 천천히 RAM으로 플러시합니다. 불가피하게, 각 코어는 이제 RAM 내용에 대해 다른보기를 갖게됩니다. 즉, 한 CPU는 RAM 쓰기주기가 완료 되고 CPU가 자체 뷰를 새로 고칠 때까지 다른 CPU가 무엇을 기록했는지 알지 못합니다 .

이는 스레딩과 극적으로 호환되지 않습니다. 당신은 항상 정말 다른 스레드에 의해 기록 된 데이터를 읽을해야하는 경우 다른 스레드의 상태가 무엇인지 관심. 이를 보장하려면 소위 메모리 장벽을 명시 적으로 프로그래밍해야합니다. 모든 CPU 캐시가 일관된 상태에 있고 RAM에 대한 최신보기를 갖도록하는 저수준 CPU 프리미티브입니다. 보류중인 모든 쓰기는 RAM으로 플러시되어야하며 캐시를 새로 고쳐야합니다.

이것은 .NET에서 사용할 수 있으며 Thread.MemoryBarrier () 메서드가 하나를 구현합니다. 이것이 lock 문이 수행하는 작업의 90 % (및 실행 시간의 95 % 이상)라는 점을 감안할 때 .NET이 제공하는 도구를 피하고 자신의 도구를 구현하려고 시도하는 것만으로는 앞서 있지 않습니다.


2
@ Davy8 : 구성은 여전히 ​​어렵습니다. 잠금없는 해시 테이블이 두 개 있고 소비자로서 두 테이블 모두에 액세스하는 경우 전체 상태의 일관성이 보장되지 않습니다. 오늘 올 수있는 가장 가까운 곳은 예를 들어 단일 atomic블록 에 두 개의 액세스를 넣을 수있는 STM 입니다. 대체로 잠금없는 구조를 사용하는 것은 많은 경우처럼 까다로울 수 있습니다.
Andras Vass

4
내가 틀렸을 수도 있지만 캐시 일관성이 작동하는 방식을 잘못 설명하신 것 같습니다. 대부분의 최신 멀티 코어 프로세서에는 일관된 캐시가 있습니다. 즉, 캐시 하드웨어가 모든 프로세스가 동일한 RAM 내용보기를 갖도록 처리합니다. 해당하는 모든 "쓰기"호출이 완료 될 때까지 "읽기"호출을 차단합니다. Thread.MemoryBarrier () 문서 ( msdn.microsoft.com/en-us/library/… )는 캐시 동작에 대해 전혀 언급하지 않습니다. 이는 단순히 프로세서가 읽기 및 쓰기 순서를 변경하지 못하도록하는 지시문 일뿐입니다.
Brooks Moses

7
"요즘"잠금없는 스레딩 "과 같은 것은 없습니다." Erlang과 Haskell 프로그래머에게 그것을 말하십시오.
Juliet

4
@HansPassant : "요즘 '잠금없는 스레딩'과 같은 것은 없습니다." F #, Erlang, Haskell, Cilk, OCaml, Microsoft의 작업 병렬 라이브러리 (TPL) 및 인텔의 스레드 구성 블록 (TBB)은 모두 잠금없는 다중 스레드 프로그래밍을 권장합니다. 요즘에는 프로덕션 코드에서 잠금을 거의 사용하지 않습니다.
JD

5
@HansPassant : "소위 메모리 장벽. 모든 CPU 캐시가 일관된 상태에 있고 RAM에 대한 최신보기를 갖도록 보장하는 저수준 CPU 기본 요소입니다. 보류중인 모든 쓰기는 RAM으로 플러시되어야합니다. 그런 다음 캐시를 새로 고쳐야합니다. " 이 컨텍스트에서 메모리 장벽은 컴파일러 또는 CPU가 메모리 명령 (로드 및 저장)을 재정렬하는 것을 방지합니다. CPU 캐시의 일관성과 관련이 없습니다.
JD


0

멀티 스레딩에 관해서는 당신이하는 일을 정확히 알아야합니다. 다중 스레드 환경에서 작업 할 때 발생할 수있는 모든 가능한 시나리오 / 사례를 탐색하십시오. 잠금없는 멀티 스레딩은 우리가 통합하는 라이브러리 나 클래스가 아니며 스레드를 여행하는 동안 얻은 지식 / 경험입니다.


잠금없는 스레딩 의미를 제공하는 수많은 라이브러리가 있습니다. STM은 특히 흥미로운 데, 그 중에서도 많은 구현이 있습니다.
Marcelo Cantos

나는 이것의 양면을 본다. 잠금없는 라이브러리에서 효과적인 성능을 얻으려면 메모리 모델에 대한 깊은 지식이 필요합니다. 그러나 그러한 지식이없는 프로그래머는 정확성의 이점을 누릴 수 있습니다.
Ben Voigt

0

.NET에서는 잠금없는 스레딩이 어려울 수 있지만 잠금이 필요한 항목을 정확히 연구하고 잠긴 섹션을 ​​최소화하여 잠금을 사용할 때 상당한 개선을 이룰 수 있습니다. 이것은 잠금 세분성 최소화라고도합니다. . 합니다.

예를 들어, 컬렉션 스레드를 안전하게 만들어야한다고 말하면됩니다. 각 항목에 대해 CPU 집약적 인 작업을 수행하는 경우 컬렉션을 반복하는 메서드 주위에 맹목적으로 잠금을 던지지 마십시오. 당신은 만 컬렉션의 단순 복사본을 만들어 주위에 잠금을 넣어해야합니다. 복사본에 대한 반복은 잠금없이 작동 할 수 있습니다. 물론 이것은 코드의 세부 사항에 크게 의존하지만 이 접근 방식으로 잠금 호송 문제 를 해결할 수있었습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.