>  기사  >  Java  >  Java 동시성 모델에 대한 자세한 소개

Java 동시성 모델에 대한 자세한 소개

黄舟
黄舟원래의
2017-02-28 10:18:471077검색

동시 시스템은 다양한 동시성 모델을 사용하여 구현할 수 있습니다. 동시성 모델은 스레드가 주어진 작업을 완료하기 위해 시스템에서 협력하는 방법을 지정합니다. 다양한 동시성 모델은 작업을 분할하기 위해 다양한 방법을 사용하며 스레드는 다양한 방식으로 서로 통신하고 협력할 수 있습니다. 이 동시성 모델 튜토리얼은 이 글을 쓰는 시점에서 가장 널리 사용되는 동시성 모델에 대한 심층적인 설명을 제공합니다.

동시성 모델은 분산 시스템과 유사합니다.

이 텍스트에서 언급하는 동시성 모델은 분산 시스템에서 사용되는 모델과 다릅니다. 프레임워크는 비슷합니다. 서로 다른 스레드는 동시 시스템에서 서로 통신합니다. 서로 다른 프로세스가 분산 시스템(아마도 서로 다른 컴퓨터)에서 통신합니다. 본질적으로 스레드와 프로세스는 매우 유사합니다. 이것이 서로 다른 동시성 모델이 서로 다른 분산 프레임워크와 유사해 보이는 이유입니다.

물론 분산 시스템에는 네트워크 오류, 원격 컴퓨터 및 프로세스 충돌 등과 같은 추가적인 문제도 있습니다. 그러나 대규모 서버에서 실행되는 동시 시스템에서는 CPU 오류, 네트워크 카드 오류, 디스크 오류 등의 경우 유사한 문제가 발생할 수 있습니다. 이러한 실패 가능성은 낮을 수 있지만 이론적으로는 가능합니다.

동시성 모델은 분산 시스템 프레임워크와 유사하기 때문에 서로에게서 몇 가지 아이디어를 배울 수 있는 경우가 많습니다. 예를 들어 작업자(스레드) 간에 작업을 분산하는 모델은 분산 시스템의 로드 밸런싱과 유사합니다. 로깅, 내결함성 등과 같은 오류를 처리하는 기술도 동일합니다.

병렬 작업자

최초의 동시성 모델, 우리는 이를 병렬 작업자 모델이라고 부릅니다. 들어오는 작업은 다른 작업자에게 할당됩니다. 다이어그램은 다음과 같습니다.


병렬 작업자 동시성 모델에서 에이전트는 들어오는 작업을 여러 작업자에게 배포합니다. 각 작업자는 전체 작업을 완료합니다. 전체 작업자는 병렬로 작업하며 서로 다른 스레드에서 실행되고 가능하면 서로 다른 CPU에서 실행됩니다.

자동차 공장에서 병렬 작업자 모델을 구현하면 각 자동차는 작업자가 생산하게 됩니다. 이 작업자는 빌드 지침을 받고 처음부터 끝까지 모든 것을 빌드합니다.

병렬 작업자 동시성 모델은 Java 애플리케이션에서 가장 널리 사용되는 동시성 모델입니다(변화하고 있음). Java 패키지 java.util.concurrent의 많은 동시성 유틸리티 클래스는 이 모델을 사용하도록 설계되었습니다. Java 엔터프라이즈 애플리케이션에서도 이 모델의 흔적을 볼 수 있습니다.

병렬 작업자의 장점

병렬 작업자 동시성 모델의 장점은 상대적으로 이해하기 쉽다는 것입니다. 애플리케이션의 병렬성을 높이려면 작업자를 더 추가하기만 하면 됩니다.

예를 들어 웹 크롤러 기능을 구현하는 경우 특정 개수의 페이지를 크롤링하기 위해 다양한 수의 작업자를 사용하고 어떤 작업자가 크롤링 시간을 더 짧게 걸리는지 확인합니다(성능이 더 높다는 의미). 웹 스크래핑은 IO 집약적인 작업이므로 컴퓨터의 CPU/코어당 여러 스레드가 생길 수 있습니다. CPU당 하나의 스레드는 데이터 다운로드를 기다리는 동안 오랫동안 유휴 상태이므로 너무 적습니다.

병렬 작업자의 단점

병렬 작업자 동시성 모델에는 몇 가지 단점이 숨어 있습니다. 아래 섹션에서 대부분의 단점을 설명하겠습니다.

공유 상태 획득이 복잡합니다.

실제로 병렬 작업자 동시성 모델은 위에서 설명한 것보다 더 복잡합니다. 이 공유 작업자는 메모리나 공유 데이터베이스의 일부 공유 데이터에 액세스해야 하는 경우가 많습니다. 아래 다이어그램은 병렬 작업자 동시성 모델의 복잡성을 보여줍니다.


이 공유 상태 중 일부는 작업 대기열과 같은 통신 메커니즘에 있습니다. 그러나 이러한 공유 상태 중 일부는 비즈니스 데이터, 데이터 캐시, 데이터베이스 연결 풀 등입니다.

공유 상태가 병렬 작업자 동시성 모델에 들어오자마자 복잡해지기 시작합니다. 이 스레드는 한 스레드의 변경 사항이 다른 스레드에 표시되도록 하기 위해 어떤 방식으로든 공유 데이터에 액세스해야 합니다(이 스레드를 실행하는 CPU의 CPU 캐시에 머물지 않고 메인 메모리로 푸시됨). 스레드는 경쟁 조건, 교착 상태 및 기타 여러 공유 상태 동시성 문제를 방지해야 합니다.

또한 공유 데이터 구조에 액세스할 때 스레드가 서로를 기다리면 병렬 컴퓨팅 부분이 손실됩니다. 많은 동시 데이터 구조가 막혀 있습니다. 이는 하나 또는 제한된 스레드 집합이 주어진 시간에 해당 구조에 액세스할 수 있음을 의미합니다. 이로 인해 이러한 공유 데이터 구조에 대한 경합이 발생할 수 있습니다. 높은 경합은 본질적으로 공유 데이터 구조에 액세스하는 코드 부분 실행 시 어느 정도 직렬화를 초래합니다.

최신 비차단 동시 알고리즘은 경합을 줄이고 성능을 향상시킬 수 있지만 비차단 알고리즘은 구현하기 어렵습니다.

지속적인 데이터 구조는 또 다른 옵션입니다. 영구 데이터 구조는 수정 시 항상 이전 버전을 유지합니다. 또한 여러 스레드가 동일한 영구 데이터 구조를 가리키고 스레드 중 하나가 이를 수정하는 경우 수정하는 스레드는 새 구조에 대한 참조를 얻습니다. 다른 모든 스레드는 이전 구조에 대한 참조를 유지하며 변경되지 않습니다. Scala 프로그래밍 언어에는 여러 가지 영구 데이터 구조가 포함되어 있습니다.

지속적 데이터 구조는 공유 데이터 구조의 동시 수정을 위한 우아하고 간결한 솔루션을 제공하면서 제대로 작동하지 않습니다.

예를 들어 영구 목록은 모든 새 요소를 목록의 헤드에 추가하고 새로 추가된 요소에 대한 참조를 반환합니다(그러면 목록의 나머지 부분이 실행됩니다). 다른 모든 스레드는 여전히 목록의 이전 첫 번째 요소에 대한 참조를 유지하며 목록은 다른 스레드에 변경되지 않은 채로 나타납니다. 새로 추가된 요소를 볼 수 없습니다.

이러한 영구 목록은 연결 목록으로 구현됩니다. 불행하게도 연결 목록은 최신 소프트웨어에서는 제대로 작동하지 않습니다. 목록의 각 요소는 별도의 개체이며 이러한 개체는 컴퓨터 메모리 전체에 분산될 수 있습니다. 최신 CPU는 데이터에 순차적으로 액세스하는 속도가 훨씬 빠르므로 목록 대신 배열 위에 구현하면 최신 하드웨어에서 더 높은 성능을 얻을 수 있습니다. 배열은 데이터를 순차적으로 저장합니다. 이 CPU 캐시는 한 번에 더 큰 청크를 캐시에 로드할 수 있으며, 로드된 후에는 이 CPU 캐시에서 데이터에 직접 액세스할 수 있습니다. 연결된 목록을 사용하여 구현하는 것은 불가능합니다. 연결된 목록의 요소는 모든 RAM에 분산되기 때문입니다.

상태 비저장 작업자

시스템에서 공유되는 상태는 다른 스레드에 의해 수정될 수 있습니다. 따라서 작업자는 최신 복사본에서 작업 중인지 확인하기 위해 필요할 때마다 이 상태를 다시 읽어야 합니다. 이는 공유 상태가 메모리에 있는지 외부 데이터베이스에 있는지에 관계없이 적용됩니다. 내부적으로 상태를 유지하지 않지만 매번 다시 읽어야 하는 작업자를 무국적 작업자라고 합니다.

매번 데이터를 다시 읽어야 하면 속도가 느려집니다. 특히 이 상태가 외부 데이터베이스에 저장되어 있는 경우에는 더욱 그렇습니다.

작업 순서를 정할 수 없다

병렬 작업자 모델의 또 다른 단점은 작업 실행 순서를 정할 수 없다는 점이다. 어떤 작업이 먼저 실행되고 어떤 작업이 마지막에 실행될지는 보장할 수 없습니다. 작업 A는 작업 B보다 먼저 작업자에게 주어질 수 있지만 작업 B는 작업 A보다 먼저 실행될 수 있습니다.

병렬 작업자 모델의 본질적으로 비결정적인 특성으로 인해 특정 시점의 시스템 상태를 추론하기가 어렵습니다. 또한 한 작업이 다른 작업보다 먼저 발생하는지 확인하는 것도 어렵습니다(기본적으로 불가능함).

Assembly Line

두 번째 동시성 모델, 저는 그것을 Assembly Line 동시성 모델이라고 부릅니다. 나는 단순히 "병렬 작업자"라는 비유에 더 간단하게 맞추기 위해 이 이름을 선택했습니다. 다른 개발자는 플랫폼이나 커뮤니티에 의존하기 위해 다른 이름(예: 반응형 시스템 또는 이벤트 기반 시스템)을 사용합니다. 다음은 설명하기 위한 예시 사진입니다.


이 작업자는 공장 조립 라인의 작업자와 같습니다. 각 작업자는 전체 작업의 일부만 수행합니다. 해당 부분이 완료되면 작업자는 작업을 다음 작업자에게 전달합니다.

각 작업자는 자체 스레드에서 실행되며 작업자 간에 공유 상태가 없습니다. 따라서 이는 때때로 비공유 동시성 모델로 언급됩니다.

파이프라인 동시성 모델을 사용하는 시스템은 일반적으로 비차단 IO를 사용하여 설계됩니다. 비차단 IO는 작업자가 IO 작업(예: 네트워크 연결에서 파일 또는 데이터 읽기)을 시작할 때 IO 호출이 완료될 때까지 기다리지 않음을 의미합니다. IO 작업이 너무 느리기 때문에 IO 작업이 완료되기를 기다리는 것은 CPU 낭비입니다. 이 CPU는 동시에 다른 작업도 수행할 수 있습니다. IO 작업이 끝나면 IO 작업의 결과(예: 데이터 읽기 또는 데이터 쓰기 상태)가 다른 작업자에게 전달됩니다.

비차단 IO의 경우 이 IO 작업은 워커 간의 경계 범위를 결정합니다. 작업자는 IO 작업을 시작해야 할 때까지 할 수 있는 모든 작업을 수행합니다. 그런 다음 이를 제어하는 ​​작업을 포기합니다. 이 IO 작업이 끝나면 파이프라인의 다음 작업자가 IO 작업을 시작해야 할 때까지 이 작업을 계속 수행합니다.


사실 이러한 작업이 반드시 생산 라인에서 진행되는 것은 아닙니다. 대부분의 시스템은 단지 하나의 작업을 수행하는 것 이상을 수행하기 때문에 작업자 간의 작업 흐름은 수행해야 하는 작업에 따라 달라집니다. 실제로 동시에 실행되는 다양한 가상 파이프라인이 있을 것입니다. 아래 다이어그램은 실제 파이프라인 시스템에서 작업이 어떻게 흐르는지 보여줍니다.


작업을 동시에 실행하기 위해 둘 이상의 작업자를 실행할 수도 있습니다. 예를 들어 작업은 작업 실행기와 작업 로그를 모두 가리킬 수 있습니다. 이 다이어그램은 세 파이프라인 모두가 동일한 작업자에게 작업을 지시하여 어떻게 끝나는지 보여줍니다(마지막 작업자는 중간 파이프라인에 있음).


이것보다 더 복잡합니다.

반응형 시스템, 이벤트 중심 시스템

파이프라인 동시성 모델을 사용하는 시스템을 반응형 시스템, 이벤트 중심 시스템이라고도 합니다. 이 시스템의 워커는 외부에서 수신되거나 다른 워커에 의해 방출되는 시스템 내 이벤트에 반응합니다. 이벤트의 예로는 HTTP 요청, 메모리에 로드되는 파일의 끝 등이 있습니다.

이 글을 쓰는 시점에는 흥미로운 반응형/이벤트 중심 플랫폼이 많이 있으며 앞으로 더 많은 플랫폼이 제공될 예정입니다. 보다 일반적인 것 중 일부는 다음과 같습니다:


  • Vert.x

  • Akka

  • Node.JS(JavaScript)

저는 개인적으로 Vert 🎜>

Actor VS.

액터와 채널은 파이프라인(또는 반응형 시스템/이벤트 중심) 모델의 두 가지 유사한 예입니다.

배우 모델에서는 모든 작업자를 배우라고 합니다. 배우들은 서로에게 메시지를 보낼 수 있습니다. 메시지가 전송된 후 비동기적으로 실행됩니다. 앞에서 설명한 대로 액터를 사용하여 하나 이상의 작업을 구현할 수 있습니다. 배우의 모델은 다음과 같습니다.


채널 모델에서는 작업자가 서로 소통할 수 없습니다. 대신 그들은 다양한 채널에 정보(이벤트)를 게시합니다. 다른 작업자는 이러한 채널에서 메시지를 들을 수 있지만 발신자는 누가 듣고 있는지 알 수 없습니다. 다음은 채널 모델의 그림입니다.


이 글을 쓰는 시점에서는 이 채널 모델이 더 탄력적으로 보입니다. 작업자는 파이프라인의 후속 작업자가 어떤 작업을 수행할지 알 필요가 없습니다. 이 작업에 대해 채널이 무엇을 가리키는지(또는 메시지를 보낼 위치)만 알면 됩니다. 채널의 청취자는 채널에 글을 쓰는 작업자에게 영향을 주지 않고 구독하거나 취소할 수 있습니다. 이는 작업자 간의 느슨한 결합을 허용합니다.

파이프라이닝의 장점

이 파이프라인 동시성 모델은 병렬 작업자 모델에 비해 몇 가지 장점이 있습니다. 다음 섹션에서는 가장 큰 장점을 다루겠습니다.

공유 상태 없음

작업자가 다른 작업자와 상태를 공유하지 않는다는 사실은 모든 동시성에 대해 걱정할 필요가 없음을 의미합니다. 달성해야 할 문제. 이렇게 하면 작업자를 더 쉽게 구현할 수 있습니다. 작업자를 구현할 때 하나의 스레드만 해당 작업을 수행하는 경우 이는 본질적으로 단일 스레드 구현입니다.

상태 저장 작업자

작업자는 다른 스레드가 데이터를 수정할 수 없다는 것을 알고 있으므로 이 작업자는 상태입니다. 상태 저장이란 작업에 필요한 데이터를 메모리에 보관하고 최종 변경 사항을 외부 스토리지 시스템에 다시 쓸 수 있음을 의미합니다. 따라서 상태 저장 작업자는 상태 없는 작업자보다 빠릅니다.

더 나은 하드웨어 통합

단일 스레드 코드에는 이러한 장점이 있으며, 기본 하드웨어에 더 잘 적응하는 경우가 많습니다. 첫째, 단일 스레드 모드에서 코드를 실행할 수 있다고 가정하면 보다 최적화된 데이터 구조와 알고리즘을 만들 수 있습니다.

위에서 언급한 것처럼 두 번째 단일 스레드 Stateful 작업자는 데이터를 메모리에 캐시할 수 있습니다. 데이터가 메모리에 캐시되면 데이터가 실행 스레드 CPU의 CPU 캐시에도 캐시될 가능성이 높습니다. 이렇게 하면 데이터에 더 빠르게 액세스할 수 있습니다.

저는 코드를 작성할 때 기본 하드웨어 작동 방식의 이점을 활용하는 방식으로 이를 하드웨어 통합이라고 부릅니다. 일부 개발자는 이것을 하드웨어 작동 방식이라고 부릅니다. 나는 컴퓨터에 기계적 부품이 거의 없기 때문에 하드웨어 통합이라는 용어를 선호합니다. 이 기사에서는 "공감"이라는 단어가 "더 잘 맞는"이라는 비유로 사용되는 반면 "순응"이라는 단어는 더 합리적이라는 단어를 표현한다고 믿습니다.

그나저나 이건 잔소리네요. 그냥 마음에 드는 단어를 사용하세요.

순차적인 작업이 가능합니다

파이프라인 동시성 모델을 기반으로 동시 시스템을 구현하여 일부에게 작업 순서를 보장하는 것이 가능합니다. 정도. 순차적 작업을 사용하면 특정 시점의 시스템 상태를 더 쉽게 추론할 수 있습니다. 또한 들어오는 모든 작업을 로그에 기록할 수 있습니다. 이 로그는 시스템의 일부가 실패할 경우 실패 지점에서 재구축하는 데 사용될 수 있습니다. 이 작업은 특정 순서로 로그에 기록될 수 있으며 이 순서는 고정된 작업 순서가 됩니다. 디자인 다이어그램은 다음과 같습니다.


고정된 작업 순서를 구현하는 것은 확실히 간단하지는 않지만 가능합니다. 가능하다면 데이터 백업, 복원, 데이터 복사 등과 같은 작업이 크게 단순화됩니다. 이 모든 작업은 로그 파일을 통해 수행할 수 있습니다.

파이프라인의 단점

파이프라인 동시성 모델의 주요 단점은 작업 실행이 종종 여러 작업자에게 분산되어 사용자를 통과한다는 점입니다. 프로젝트의 여러 클래스. 따라서 특정 작업에 대해 정확히 어떤 코드가 실행되고 있는지 확인하기가 더 어렵습니다.

코드 작성도 어려워질 수 있습니다. 작업자 코드는 콜백 함수로 작성되는 경우가 많습니다. 더 많은 중첩된 콜백과 함께 제공되는 코드로 인해 일부 개발자는 어떤 콜백을 호출할지 지루해질 수 있습니다. 콜백 지옥은 코드가 수행하는 작업을 추적하고 각 콜백이 데이터에 액세스하는 데 필요한 데이터를 결정하는 것이 더 어렵다는 것을 의미합니다.

병렬 작업자 동시성 모델을 사용하면 이 작업이 더 간단해집니다. 이 작업자 코드를 열고 실행된 코드를 거의 처음부터 끝까지 읽을 수 있습니다. 물론 병렬 작업자 코드는 여러 클래스에 분산될 수도 있지만 코드에서 실행 순서를 더 쉽게 읽을 수 있습니다.

기능적 병렬성

기능적 병렬성은 최근 많은 논의가 있었던 세 번째 동시성 모델입니다.

기능적 병렬성의 기본 아이디어는 함수 호출을 사용하여 프로그램을 구현하는 것입니다. 함수는 파이프라인 동시성 모델(AKA 반응 시스템 또는 이벤트 기반 시스템)과 마찬가지로 서로에게 메시지를 보내는 "에이전트" 또는 "액터"로 간주됩니다. 한 함수가 다른 함수를 호출하는 것은 메시지를 보내는 것과 유사합니다.

함수에 전달된 모든 매개변수는 복사되므로 수신 함수 외부의 개체는 이 데이터를 결합할 수 없습니다. 이 복사본은 공유 데이터의 정적 조건을 방지하는 데 중요합니다. 이로 인해 함수는 원자적 연산과 유사하게 수행됩니다. 각 함수 호출은 다른 함수 호출과 독립적으로 실행될 수 있습니다.

함수 호출은 개별적으로 실행될 수 있지만 각 함수 호출은 별도의 CPU에서 실행될 수 있습니다. 이는 구현된 기능적 알고리즘이 여러 CPU에서 병렬로 실행될 수 있음을 의미합니다.

Java 7에서는 기능적 병렬 처리와 유사한 작업을 수행하는 데 도움이 될 수 있는 ForkAndJoinPool이 포함된 java.util.concurrent 패키지를 얻습니다. Java 8을 사용하면 대규모 컬렉션의 반복을 병렬화하는 데 도움이 되는 병렬 스트림이 제공됩니다. ForkAndJoinPool에 만족하지 않는 개발자가 있다는 것을 기억하십시오(내 ForkAndJoinPool 튜토리얼에서 일부 비판에 대한 링크를 찾을 수 있습니다).

기능적 병렬 처리에서 가장 어려운 부분은 병렬 처리할 함수 호출을 아는 것입니다. CPU 전반에 걸쳐 함수 호출을 조정하면 오버헤드가 발생합니다. 기능에 의해 완료되는 작업 단위에는 일정량의 오버헤드가 필요합니다. 함수 호출이 매우 작은 경우 병렬화를 시도하는 것은 실제로 단일 스레드, CPU 전용 실행보다 느릴 수 있습니다.

제가 이해한 바에 따르면(물론 완벽하지는 않습니다) 리액티브 시스템과 타임 드라이브를 사용하여 알고리즘을 구현하고 작업 분해를 완료할 수 있습니다. 이는 기능적 병렬 처리와 유사합니다. 이벤트 중심 모델을 사용하면 병렬화 정도와 방법을 더 잘 제어할 수 있습니다.

또한 여러 CPU에 걸쳐 작업을 분할하면 조정 비용이 발생합니다. 이는 해당 작업이 현재 이 프로그램에서 실행되는 유일한 작업인 경우에만 의미가 있습니다. 그러나 시스템이 여러 다른 작업(예: 웹 서버, 데이터베이스 서버 및 기타 여러 시스템)을 수행하는 경우 단일 작업을 병렬화하는 것은 의미가 없습니다. 어쨌든 컴퓨터의 다른 CPU는 다른 작업을 수행하느라 바쁘기 때문에 더 느린 기능적 병렬 작업으로 인해 CPU를 방해할 이유가 없습니다. 파이프라인 동시성 모델을 사용하는 것이 가장 현명할 것입니다. 오버헤드가 적고(단일 스레드 모드에서 순차적 실행) 기본 하드웨어 요구 사항을 더 잘 따르기 때문입니다.

어떤 동시성 모델이 가장 좋은가요?

그럼 어떤 동시성 모델이 가장 좋은가요?

일반적으로 그렇습니다. 대답은 시스템이 어떻게 생겼는지에 따라 다릅니다. 작업이 자연스럽게 병렬적이고 독립적이며 공유 상태가 필요하지 않은 경우 병렬 작업 모델을 사용하여 시스템을 구현할 수 있습니다.

그러나 많은 작업은 본질적으로 병렬적이고 독립적이지 않습니다. 이러한 종류의 시스템에서는 이 파이프라인 동시성 모델이 단점보다 장점이 많고 병렬 작업자 모델보다 장점이 더 크다고 생각합니다.

파이프라인 구조의 코드를 직접 작성할 필요도 없습니다. Vert.x와 같은 최신 플랫폼은 이미 이 작업을 많이 수행하고 있습니다. 나는 개인적으로 다음 프로젝트에서 Vert.x와 같은 플랫폼 위에서 실행되는 디자인을 탐구할 것입니다. Java EE에는 아무런 장점이 없을 것 같습니다.

위 내용은 Java 동시성 모델에 대한 자세한 소개입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.