가이드라인: 동시성
이 가이드라인은 개발자가 소프트웨어 시스템에서 동시성에 대한 요구를 만족시키는 최상의 방법을 선택하는 데 도움을 줍니다.
관계
기본 설명

소개

좋은 디자인 기술은 요구사항 세트를 만족시키는 "최상"의 방법을 선택하는 것입니다. 좋은 동시성 시스템 디자인 기술은 종종 동시성 요구를 만족시키는 가장 단순한 방법을 찾는 것입니다. 디자이너에게 우선되는 규칙 중 하나로 바퀴를 다시 발명하지 말라는 것이 있습니다. 대부분의 문제점을 해결하기 위해 좋은 디자인 패턴 및 디자인 관용 표현이 이미 개발되었습니다. 동시성 시스템의 복잡성을 고려할 때, 잘 입증된 솔루션을 사용하고 디자인의 단순성을 위해 노력하는 것이 가장 바람직합니다.

동시성 접근 방식

모두 컴퓨터에서 발생하는 동시성 타스크를 실행 스레드라고 합니다. 모든 동시성 타스크와 마찬가지로 실행 스레드는 시간에 맞추어 발생하는 추상 개념입니다. 실제로 실행 스레드를 캡처하기 위해 할 수 있는 최선의 방법은 특정 시간 순간에 해당 상태를 표시하는 것입니다.

컴퓨터를 사용하여 동시성 타스크를 표시하는 가장 직접적인 방법은 각 타스크에 별도의 컴퓨터를 전용으로 사용하는 것입니다. 그러나 일반적으로 이 방법은 비용이 너무 많이 들고 분석 충돌에 항상 도움이 되는 것은 아닙니다. 따라서 동일한 실제 프로세서에서 멀티태스킹이라는 양식을 통해 다중 타스크를 지원하는 것이 일반적입니다. 이 경우 프로세서 및 이와 연관된 자원(예: 메모리 및 버스)이 공유됩니다. (안타깝게도 자원의 공유는 원래 문제점에서는 나타나지 않는 새로운 충돌을 발생시킬 수도 있습니다.)

멀티태스킹의 가장 일반적인 양식은 각 타스크에 "가상" 프로세서를 제공하는 것입니다. 이 가상 프로세서는 보통 프로세스 또는 타스크라고 합니다. 일반적으로 각 프로세스에는 고유한 주소 공간이 있어서 다른 가상 프로세서의 주소 공간과 논리적으로 구분이 됩니다. 그러면 프로세스가 서로의 메모리를 우연히 겹쳐쓰는 경우 충돌을 방지할 수 있습니다. 안타깝게도 실제 프로세서를 하나의 프로세스에서 다른 프로세스로 전환하는 데 필요한 오버헤드가 종종 매우 큽니다. 이는 최신 고속 프로세서에서도 수백 밀리초가 걸릴 수 있는 CPU 내의 주요 레지스터 세트 스와핑(작업 전환)과 관련이 있습니다.

이 오버헤드를 줄이기 위해 많은 운영 체제가 단일 프로세스에 여러 경량 스레드를 포함시키는 기능을 제공합니다. 한 프로세스에 있는 스레드는 해당 프로세스의 주소 공간을 공유합니다. 그러면 작업 전환과 관련된 오버헤드가 줄어듭니다. 그러나 메모리 충돌이 발생할 가능성은 높아집니다.

일부 처리량이 많은 응용프로그램에서는 경량 스레드 전환의 오버헤드도 허용할 수 없을 만큼 높은 경우가 있습니다. 이 상황에서는 응용프로그램의 일부 특수한 기능을 활용하여 경량 양식의 멀티태스킹을 확보하는 것이 일반적입니다.

시스템의 동시성 요구사항은 시스템의 아키텍처에 큰 영향을 줄 수 있습니다. 단일 프로세스 아키텍처에서 다중 프로세스 아키텍처로 기능을 이동시키도록 결정하면 많은 측면에서 시스템의 아키텍처가 크게 변경됩니다. 시스템의 아키텍처가 많이 변경된 경우 추가 메커니즘(예: 원격 프로시저 호출)을 도입해야 할 수도 있습니다.

시스템 가용성 요구사항은 물론 추가 프로세스 및 스레드를 관리하는 추가 오버헤드도 고려해야 합니다.

다른 아키텍처 결정과 마찬가지로, 프로세스 아키텍처를 변경하면 또다른 문제점 세트가 나타납니다.

접근 방식

장점

단점

단일 프로세스, 스레드 없음
  • 단순성
  • 고속 프로세스 내부 메시지 전달
  • 워크로드 밸런스 조절이 어려움
  • 다중 프로세서로 크기 조정할 수 없음
단일 프로세스, 다중 스레드
  • 고속 프로세스 내부 메시지
  • 프로세스 간 통신 없이 멀티태스킹
  • '중량' 프로세스의 오버헤드 없이 더 나은 멀티태스킹
  • 응용프로그램에 '스레드 안전성'이 있어야 함
  • 운영 체제에서 효과적으로 스레드를 관리해야 함
  • 공유 메모리 문제를 고려해야 함
다중 프로세스
  • 프로세서 추가로 크기 조정 잘됨
  • 노드에 분배하기가 비교적 쉬움
  • 프로세스 경계에 민감, 프로세스 간 통신 사용 시 큰 성능 저하
  • 스와핑 및 작업 전환의 비용이 많이 듬
  • 디자인이 더 어려워짐

일반적인 발전 경로는 단일 프로세스 아키텍처에서 시작하여 동시에 나타나야 하는 동작 그룹의 프로세스를 추가합니다. 이 폭넓은 그룹화 내에서 동시성을 늘리도록 프로세스 내부에 스레드를 추가하여 동시성에 대한 추가 요구를 고려하십시오.

첫 번째 시작점은 목적이 정해져 있는 활성 오브젝트 스케줄러를 사용하여 단일 운영 체제 타스크 또는 스레드에 많은 활성 오브젝트를 지정하는 것입니다. 이 방법을 사용하면 일반적으로 동시성에 대한 경량 시뮬레이션을 달성할 수 있습니다. 그러나 단일 운영 체제 타스크 또는 스레드를 사용하면 다중 CPU를 포함하는 시스템의 장점을 활용할 수 없습니다. 이때 차단 동작이 병목 현상을 일으키지 않기 위해 별도의 스레드에서 차단 동작을 분리하도록 결정해야 합니다. 그러면 차단 동작을 포함하는 활성 오브젝트가 운영 체제 스레드로 분리됩니다.

실시간 시스템에서 이 추론은 캡슐에 동등하게 적용됩니다. 각 캡슐에는 운영 체제 스레드, 타스크 또는 다른 캡슐을 포함하는 프로세스에서 공유할 수도 있고 공유하지 않을 수도 있는 논리적 제어 스레드가 있습니다.

문제

안타깝게도 다른 많은 아키텍처 결정과 마찬가지로 쉬운 대답은 없습니다. 올바른 솔루션에는 주의깊게 밸런스를 조정하는 접근 방식이 포함됩니다. 작은 아키텍처 프로토타입을 사용하여 특정 선택사항 세트의 함축된 내용을 조사할 수 있습니다. 프로세스 아키텍처 프로토타입 시 이론상 최대 시스템 수까지 프로세스 수를 조정하는 데 초점을 두십시오. 다음 문제를 고려하십시오.

  • 프로세스 수를 최대값까지 크기 조정할 수 있습니까? 최대값을 초과하여 시스템을 얼마나 증가시킬 수 있습니까? 잠재적인 증가를 허용할 수 있습니까?
  • 프로세스 일부를 변경하면 공유 프로세스 주소 공간에서 작동하는 경량 스레드가 어떤 영향을 받습니까?
  • 프로세스 수를 추가하면 응답 시간에 어떤 상황이 발생합니까? 프로세스 간 통신(IPC)의 크기가 증가했습니까? 성능이 크게 떨어졌습니까?
  • 프로세스를 결합하거나 재구성하여 IPC 크기를 줄일 수 있습니까? 이 변경사항으로 대규모 일체 프로세스에서 로드 밸런스가 어려워졌습니까?
  • 공유 메모리를 사용하여 IPC를 줄일 수 있습니까?
  • 시간 자원을 할당한 경우 모든 프로세스가 "동일 시간"을 확보해야 합니까? 시간 할당을 수행할 수 있습니까? 스케줄 우선순위를 변경하는 잠재적인 문제점이 있습니까?

오브젝트 간 통신

활성 오브젝트는 동기 또는 비동기 방식으로 서로 통신할 수 있습니다. 동기 통신은 엄격히 제어되는 시퀀스를 통해 복잡한 협업을 단순화할 수 있으므로 유용합니다. 즉, 활성 오브젝트가 다른 활성 오브젝트의 동기 호출과 관련된 RTC(Run-to-completion) 단계를 실행하는 동안 다른 오브젝트에서 시작된 동시성 상호 작용은 전체 시퀀스를 완료할 때까지 무시될 수 있습니다.

일부 경우 이 방법이 유용하지만 높은 우선순위를 가지는 보다 중요한 이벤트가 대기해야 할 수도 있으므로(우선순위 반전) 문제가 될 수도 있습니다. 이는 동기 호출된 오브젝트가 자체 동기 호출에 대한 응답을 대기하지 못하도록 차단할 수 있다는 가능성 때문에 악화될 수 있습니다. 이 경우 끝없는 우선순위 반전으로 이어질 수 있습니다. 가장 극단적인 경우 동기 호출의 체인이 고리 모양을 이루면 교착 상태에 빠질 수 있습니다.

비동기 호출은 제한된 응답 시간을 사용하여 이 문제점을 방지합니다. 그러나 소프트웨어 아키텍처에 따라 비동기 통신은 종종 보다 복잡한 코드로 연결됩니다. 활성 오브젝트가 언제든지 여러 비동기 이벤트(각각에 다른 활성 오브젝트와의 복잡한 비동기 상호작용 시퀀스가 동반될 수 있음)에 응답해야 할 수도 있기 때문입니다. 이 경우 구현이 매우 어렵고 오류가 발생할 수 있습니다. 

메시지 전달을 보증하는 비동기 메시지 전달 기술을 사용하면 응용프로그램 프로그래밍 타스크를 단순화할 수 있습니다. 응용프로그램은 네트워크 연결 또는 원격 응용프로그램이 사용 불가능해도 계속 오퍼레이션을 진행할 수 있습니다. 비동기 메시지 전달이 동기 모드에서의 사용을 배제하지는 않습니다. 동기 기술은 응용프로그램이 사용 가능할 때마다 연결을 사용 가능하게 해야 합니다. 연결이 있음을 알기 때문에 확약 프로세스 처리가 더 쉬워집니다.

실시간 시스템에 대한 Rational Unified Process의 접근 방식의 경우 캡슐은 특정 프로토콜에 따라 신호를 사용하여 비동기적으로 통신합니다. 그러나 각 방향에서 하나씩 신호 쌍을 사용하여 동기 통신을 달성할 수 있습니다.

실용성

활성 오브젝트의 작업 전환 오버헤드는 매우 낮지만, 일부 응용프로그램에서는 비용이 너무 많이 든다고 여길 수 있습니다. 이는 보통 많은 데이터를 높은 비율로 처리해야 하는 상황에서 발생합니다. 이 경우 다시 세마포어와 같은 전통적인(그러나 위험성이 더 높은) 동시성 관리 기법 및 수동 오브젝트를 사용해야 할 수도 있습니다.

그러나 이 고려사항은 반드시 활성 오브젝트 접근 방식도 함께 포기해야 함을 의미하지는 않습니다. 이와 같이 데이터가 집중된 응용프로그램에서도 성능에 민감한 파트는 전반적인 시스템에서 비교적 작은 부분에 해당하는 경우가 종종 있습니다. 이 경우 나머지 시스템은 여전히 활성 오브젝트 패러다임의 장점을 활용할 수 있음을 의미합니다.

일반적으로 시스템 디자인을 고려할 때 유일한 디자인 기준은 오직 성능뿐입니다. 시스템이 복잡하면 다른 기준(예: 유지보수성, 변경 용이성, 이해 가능성 등)이 똑같이 중요하게 존재합니다. 활성 오브젝트 접근 방식은 동시성 및 동시성 관리의 복잡도를 많이 숨기므로 확실한 장점을 가지고 있습니다. 동시에 하위 레벨 기술 특정 메커니즘과 반대로 디자인을 응용프로그램 특정 용어로 표현할 수 있습니다.

발견적 방법

동시성 컴포넌트 사이의 상호작용에 초점을 맞춤

상호작용이 없는 동시성 컴포넌트는 대부분 사소한 문제점입니다. 거의 모든 디자인 문제가 동시성 타스크 사이의 상호작용과 관련이 있으므로 우선 상호작용 이해에 초점을 맞춰야 합니다. 다음은 몇 가지 질문입니다.

  • 상호작용은 단방향, 양뱡향 또는 다중 방향으로 수행됩니까?
  • 클라이언트-서버 또는 마스터 슬레이브 관계가 있습니까?
  • 일부 필요한 동기화 양식이 있습니까?

상호작용을 이해하면 구현하는 방법을 검토할 수 있습니다. 구현은 시스템 성능 목적과 일치하는 가장 단순한 디자인을 생성하도록 선택되어야 합니다. 일반적으로 성능 요구사항은 외부에서 생성된 이벤트에 응답하기 위한 전반적인 처리량 및 허용 가능한 대기 시간 모두를 포함합니다.

이 문제는 종종 성능에 대한 편차 허용 범위가 적은 실시간 시스템에서 더 중요합니다(예: 응답 시간의 '불안정(jitter)' 또는 최종 기한을 맞추지 못하는 경우).

외부 인터페이스 분리 및 캡슐화

응용프로그램에 걸친 외부 인터페이스 처리량에 대한 특정 가정을 포함하는 것은 잘못된 사례이고 이벤트 대기를 차단하는 여러 제어 스레드를 포함하는 것은 매우 비효율적입니다. 대신 이벤트를 발견하는 전용 타스크에 단일 오브젝트를 지정하십시오. 이벤트가 발생하면 해당 오브젝트가 이벤트에 대해 알아야 하는 사람에게 이 사실을 알릴 수 있습니다. 이 디자인은 잘 알려지고 입증된 디자인 패턴인 "관찰자(Observer)" 패턴[GAM94]에 기반합니다. "공개자-등록자(Publisher-Subscriber) 패턴"[BUS96]에 보다 많은 유연성을 제공하도록 쉽게 확장될 수 있습니다. 이 패턴에서 공개자(publisher) 오브젝트는 이벤트 발견자와 이벤트에 관심이 있는 오브젝트("등록자(subscriber)") 사이에서 매개체 역할을 수행합니다.

차단 및 폴링 동작 분리 및 캡슐화

시스템에서 조치는 외부에서 생성된 이벤트의 발생으로 트리거될 수 있습니다. 외부에서 생성되는 매우 중요한 이벤트 중 하나는 시간의 흐름으로 표시되는 단순한 시간의 경과일 수 있습니다. 기타 외부 이벤트는 외부 하드웨어(사용자 인터페이스 장치, 프로세서 센서 및 다른 시스템과의 통신 링크 포함)에 연결된 입력 장치에서 발생합니다. 보통 외부 세계와의 연결성이 높은 실시간 시스템에서는 더욱 그렇습니다.

소프트웨어에서 이벤트를 발견하려면 인터럽트 대기를 차단하거나 이벤트 발생을 확인하도록 정기적으로 하드웨어를 확인해야 합니다. 후자의 경우 정기적인 주기는 수명이 짧은 이벤트 또는 다중 발생이 누락되지 않도록 또는 이벤트 발생 및 발견 사이의 대기 시간을 최소화하기 위해 짧아야 합니다.

이때 흥미로운 점은 이벤트의 낮은 빈도에 상관없이 일부 소프트웨어는 이벤트에 대해 대기하지 못하도록 차단하거나 이벤트 발생을 자주 확인해야 한다는 점입니다. 그러나 시스템에서 처리해야 하는 많은 이벤트(대부분이 아닌 경우)는 드물게 나타납니다. 대부분의 경우 지정된 시스템에서 중요한 내용이 발생하지 않습니다.

엘리베이터 시스템은 이에 관한 좋은 예제입니다. 엘리베이터 사용 시 중요한 이벤트로, 서비스 호출, 승객 층 선택, 승객이 손으로 문 차단, 한 층에서 다음 층으로 이동이 있습니다. 이 이벤트 중 일부는 시간에 민감한 반응이 요구됩니다. 그러나 모두 원하는 응답 시간의 시간 스케일과 비교하면 매우 드문 경우입니다.

단일 이벤트로 많은 조치를 트리거할 수 있으며 조치는 다양한 오브젝트의 상태에 종속될 수 있습니다. 또한 시스템의 다른 구성에서는 동일한 이벤트를 서로 다르게 사용할 수 있습니다. 예를 들어 엘리베이터에서 한 층을 지나는 경우 엘리베이터 안의 디스플레이를 새로 갱신해야 하며, 엘리베이터 자체가 새 호출 및 승객 층 선택에 응답하는 방법을 알 수 있도록 해당 위치를 알고 있어야 합니다. 각 층에 엘리베이터 위치 디스플레이가 있을 수도 있고, 없을 수도 있습니다.

폴링 동작보다 반응 동작 선호

폴링은 비용이 많이 듭니다. 일부 시스템 파트에서 이벤트가 발생했는지 확인하기 위해 수행 중인 작업을 정기적으로 중지해야 합니다. 이벤트에 빨리 응답해야 하는 경우 시스템은 달성할 수 있는 다른 작업의 크기도 제한하면서 이벤트 도착을 매우 자주 확인해야 합니다.

인터럽트로 활성화되는 이벤트 종속 코드를 사용하여 이벤트에 인터럽트를 할당하는 것이 훨씬 더 효과적입니다. 때때로 인터럽트에 "비용이 많이 들기"때문에 사용을 피하기도 하지만 인터럽트를 신중히 사용하면 반복되는 폴링보다 더 효과적일 수 있습니다.

이벤트 알림 메커니즘으로 인터럽트가 선호되는 경우는 이벤트 도착이 임의적이고 빈번하지 않은 경우입니다. 이 경우 대부분의 폴링 노력에서 이벤트가 발생하지 않았음을 확인합니다. 폴링이 선호되는 경우는 이벤트가 정기적이고 예측 가능한 방식으로 도달하여 대부분의 폴링 노력에서 이벤트가 발생했다는 사실을 확인하는 경우입니다. 이 중간에 폴링 또는 반응 동작이 서로 차이가 나지 않는 지점이 있습니다. 여기에서는 두 동작 모두 동일하게 잘 작동하므로 어떤 동작을 선택해도 큰 차이는 없습니다. 그러나 대부분의 경우 현실 세계에서는 이벤트가 임의로 발생하기 때문에 반응 동작이 선호됩니다.

데이터 브로드캐스트보다 이벤트 알림 선호

보통 신호를 사용하여 데이터를 브로드캐스트하면 비용이 많이 들고 낭비가 심할 수 있습니다. 소수의 오브젝트만 데이터에 관심이 있는데 모든(또는 많은) 경우 데이터를 점검하려면 정지해야 합니다. 자원을 덜 소모하는, 더 나은 접근 방식은 관심이 있는 해당 오브젝트에만 일부 이벤트 발생을 알리는 알림을 사용하는 것입니다. 브로드캐스트는 많은 오브젝트에 주의를 기울여야 하는 이벤트(보통 타이밍 또는 동기화 이벤트)로만 제한하십시오.

경량 메커니즘의 많은 사용 및 중량 메커니즘의 적은 사용

구체적으로 설명하면 다음과 같습니다.

  • 동시성이 문제가 되지 않지만 자발적인 반응이 문제가 되는 경우 동기 메소드 호출 및 수동 오브젝트를 사용하십시오.
  • 응용프로그램 레벨 동시성 개념의 대부분에서 비동기 메시지 및 활성 오브젝트를 사용하십시오.
  • 차단 요소를 분리하도록 OS 스레드를 사용하십시오. 활성 오브젝트를 OS 스레드에 맵핑할 수 있습니다.
  • 최대 분리를 위해 OS 프로세스를 사용하십시오. 프로그램을 독립적으로 시작 및 종료해야 하는 경우 및 분산되어야 하는 서브시스템에서 별도의 프로세스가 필요합니다.
  • 기본 마력 또는 실제 분배를 위해 별도의 CPU를 사용하십시오.

효과적인 동시성 응용프로그램 개발 시 가장 중요한 가이드라인은 최경량 동시성 메커니즘 사용을 최대화하는 것입니다. 하드웨어 및 운영 체제 소프트웨어는 동시성 지원 시 중요한 역할을 수행합니다. 그러나 모두 비교적 중량 메커니즘을 제공하므로 많은 응용프로그램 디자이너 작업이 필요합니다. 동시성 응용프로그램의 요구와 사용 가능한 도구 사이의 큰 간격을 중재하는 작업이 필요합니다.

활성 오브젝트를 사용하면 다음과 같은 두 개의 주요 기능을 통해 이 간격을 중재하는 데 도움이 됩니다.

  • OS 또는 CPU에서 제공하는 기본 메커니즘을 사용하여 구현할 수 있는 기본 동시성 단위(제어 스레드)를 캡슐화하여 디자인 추상을 통합합니다.
  • 활성 오브젝트가 단일 OS 스레드를 공유하는 경우 매우 효과적인 경량 동시성 메커니즘이 됩니다. 그렇지 않으면 응용프로그램에서 직접 구현해야 합니다.

또한 활성 오브젝트는 프로그래밍 언어에서 제공하는 수동 오브젝트에 적합한 이상적인 환경을 작성합니다. 프로그램 및 프로세스와 같은 프로시저 아티팩트를 사용하지 않고 전적으로 동시성 오브젝트의 기반에서 시스템을 디자인하면 디자인이 보다 이해가 쉽고 응집되며 모듈화될 수 있습니다.

일방적인 성능 주장 방지

대부분의 시스템에서는 코드의 10% 미만이 CPU 주기의 90% 이상을 사용합니다.

많은 시스템 디자이너는 코드의 모든 행이 최적화되어야 한다고 생각합니다. 대신 더 자주 실행하거나 오래 걸리는 10%의 코드를 최적화하는 데 시간을 투자하십시오. 나머지 90%는 이해 가능성, 유지보수성, 모듈화 및 구현 용이성을 강조하여 디자인하십시오.

메커니즘 선택

시스템의 아키텍처 및 비기능적 요구사항은 원격 프로시저 호출을 구현하는 데 사용하는 메커니즘 선택사항에 영향을 줍니다. 대안 사이의 절충 유형에 대한 개요가 아래 표시됩니다.  

메커니즘 용도 설명
메시지 전달 엔터프라이즈 서버에 대한 비동기 액세스 메시지 전달 미들웨어는 대기열, 제한시간 및 복구/다시 시작 조건을 처리하여 응용프로그램 프로그래밍을 단순화할 수 있습니다. 또한 메시지 전달 미들웨어를 Pseudo 동기 모드로 사용할 수 있습니다. 보통 메시지 전달 기술은 크기가 큰 메시지를 지원할 수 있습니다. 일부 RPC 접근 방식은 메시지 크기를 제한할 수 있므로 크기가 큰 메시지를 처리하려면 추가 프로그래밍이 필요할 수 있습니다.
JDBC/ODBC 데이터베이스 호출 동일하거나 다른 서버에 있는 데이터베이스를 호출하기 위해 Java Servlet 또는 응용프로그램에 대해 데이터베이스에 독립적인 인터페이스가 있습니다.
기본 인터페이스 데이터베이스 호출 많은 데이터베이스 벤더는 응용프로그램 이식성을 희생하여 벤더 고유의 데이터베이스에 기본 응용프로그램 인터페이스를 구현함으로써 ODBC에서의 성능 이점을 제공합니다.
원격 프로시저 호출 원격 서버에서 프로그램 호출 사용자를 대신하여 관리하는 응용프로그램 빌더가 있는 경우 RPC 레벨에서 프로그램을 작성하지 않아도 됩니다.
대화식 E-business 응용프로그램에서는 거의 사용하지 않음 보통 APPC 또는 소켓과 같은 프로토콜을 사용하는 하위 레벨의 프로그램 간 통신.

요약

많은 시스템에 동시성 동작 및 분산 컴포넌트가 필요합니다. 대부분의 프로그래밍 언어는 이 두 문제를 해결하는 데 거의 도움을 주지 못합니다. 응용프로그램의 동시성에 대한 요구 및 소프트웨어에서의 구현 옵션 모두를 이해하려면 바람직한 추상이 필요합니다. 또한 역설적으로 동시성 소프트웨어는 본질적으로 비동시성 소프트웨어보다 더 복잡하지만, 현실 세계에서 동시성을 다루어야 하는 시스템 디자인의 많은 부분을 단순화해줄 수도 있습니다.