참고: 동시성은 모든 시스템에 적용 가능하므로 여기에서는 동시성에 대해 일반적으로 설명하고 있습니다. 그러나 동시성은 실시간으로 외부 이벤트에 반응해야 하고 종종 충족해기
어려운 최종 기한을 가지고 있는 시스템에서 특히 중요합니다. 이런 종류의 시스템 특수 요구를 처리하기 위해, RUP에는 실시간(반응) 시스템 확장이 있습니다. 이 주제에 대한 자세한 정보는
실시간 시스템을 참조하십시오.
|
동시성은 시스템에서 어떠한 사항이 동시에 발생하는 경향입니다. 동시성은 물론 자연스러운 현상입니다. 현실 세계에서는 주어진 시간에 많은 일이 동시에 발생합니다. 현실 세계 시스템을 모니터하고 제어하기 위한
소프트웨어를 디자인할 때 이러한 자연스러운 동시성을 다뤄야 합니다.
소프트웨어 시스템에서 동시성 문제에 대해 처리할 때, 일반적으로 두 가지 측면, 즉, 무작위 순서로 발생하는 외부 이벤트를 발견하여 응답할 수 있는 것과 이벤트가 최소한의 필수 간격으로 응답되도록 해야 합니다.
각각의 동시 활동이 독립적으로, 완전히 병렬 패턴으로 전개된 경우 이는 비교적 단순합니다. 별도의 프로그램을 작성하여 각 활동을 처리하면 됩니다. 대개 동시 시스템 디자인 문제는 동시 활동 사이에 발생하는
상호작용으로 인해 발생합니다. 동시 활동이 상호작용할 경우 약간의 조정이 필요합니다.
그림 1: 작업에서의 동시성 예제: 상호작용하지 않는 병렬 활동은 단순한 동시성 문제를 가지고 있습니다. 병렬 활동이 상호작용하거나 같은 자원을 공유할 때 동시성 문제가 중요하게 됩니다.
비슷한 예로 차량 통행이 있습니다. 거의 상호작용이 없는 다른 차도에서의 평행 통행은 별다른 문제점을 야기하지 않습니다. 인접하는 차선들의 평행 흐름은 안전한 상호작용을 위해 약간의 조정만 필요하지만, 교차로에서는
심각한 유형의 상호작용이 발생하므로 조정 시 주의해야 합니다.
동시성의 원동력 중 일부는 외부적입니다. 즉, 환경 요구에 의해 생깁니다. 현실 세계 시스템에서 많은 일이 동시에 발생하는데 이를 소프트웨어에서는 "실시간"으로 처리해야 합니다. 이렇게 하려면 많은 실시간
소프트웨어 시스템이 "반응적"이어야 합니다. 이 시스템은 다소 일정하지 않은 시간에, 다소 일정하지 않은 순서로 또는 두 가지 상황 모두에서 발생 가능한 외부에서 생성된 이벤트에 응답해야 합니다.
이와 같은 상황을 다루는 전통적인 프로시저 프로그램의 디자인은 아주 복잡합니다. 이는 시스템을 각각의 이벤트를 다루기 위한 동시 소프트웨어 요소로 분할하는 것과 매우 비슷할 수 있습니다. 여기에서 중요한 문구는
"할 수 있습니다"입니다. 복잡도는 또한 이벤트 사이의 상호작용 정도의 영향을 받기 때문입니다.
동시성에 대해 내부에서 오는 이유도 있을 수 있습니다([LEA97]). 병렬로
타스크를 수행하면 여러 CPU를 사용할 수 있을 경우에 시스템의 계산 작업 속도가 증가될 수 있습니다. 단일 프로세서 내에서도, 멀티태스킹은 하나의 활동이 예를 들어 입출력을 기다리는 동안 다른 활동이 차단되지
않도록 하여 극적으로 속도를 높일 수 있습니다. 이와 같은 일이 발생하는 공통적인 상황은 시스템을 시작할 때입니다. 종종 많은 컴포넌트가 있는데, 컴포넌트마다 오퍼레이션을 수행할 준비를 하는 시간이 필요합니다. 이
오퍼레이션의 순차적 수행은 아주 느릴 수 있습니다.
동시성으로 시스템의 제어 가능성도 향상될 수 있습니다. 예를 들어, 하나의 기능이 시작 또는 중지되거나 다른 동시 기능에 의해 중간 스트림에서 영향을 받을 수 있습니다(동시 컴포넌트 없이는 완료하기 어려움).
이와 같은 모든 이점에도 불구하고 항상 동시 프로그래밍을 사용하게 되지 않는 이유는 무엇일까요?
대부분의 컴퓨터 및 프로그래밍 언어는 본래 순차적입니다. 프로시저나 프로세서는 한 번에 하나의 명령어를 실행합니다. 단일 순차 프로세서 내에서는 다른 타스크의 실행 인터리빙으로 동시성 착각을 일으켜야 합니다.
어려움은 이와 같이 수행하는 기계적인 부분에 있는 것이 아니라, 서로 상호작용할 수 있는 프로그램 세그먼트를 인터리브하는 시기와 방법을 판별하는 데 있습니다.
여러 프로세서를 사용할 경우 동시성을 달성하는 것이 쉽지만 상호작용은 더 복잡하게 됩니다. 우선, 서로 다른 프로세서에서 실행 중인 타스크 사이의 통신 문제가 있습니다. 보통 몇몇 소프트웨어 계층이 연관되어
복잡도를 높이고 타이밍 오버헤드가 추가됩니다. 결정론은 다중 CPU 시스템에서 감소합니다. 시계와 타이밍이 다를 수 있고 컴포넌트가 독립적으로 실패할 수 있기 때문입니다.
결국, 동시 시스템은 명시적 글로벌 시스템 상태가 없어 이해하기가 더 어려울 수 있습니다. 동시 시스템 상태는 해당 컴포넌트 상태를 모은 집계입니다.
설명하기 위해 엘리베이터 시스템을 예로 들어보겠습니다. 더 명확하게 말하자면, 빌딩 내의 한 위치에서 엘리베이터 그룹을 제어하도록 디자인된 컴퓨터 시스템입니다. 분명히 엘리베이터 그룹 내에는 동시에 많은
엘리베이터가 작동 중이거나 하나도 작동하지 않을 수 있습니다. 특정 시점에 어떤 층에 있는 누군가가 엘리베이터를 요청하고 다른 요청은 보류 중일 수 있습니다. 엘리베이터 중 일부는 대기 상태이고 다른 엘리베이터들은
승객을 운반하고 있거나 호출에 응하기 위해 이동 중이거나 둘 다일 수 있습니다. 문은 적절한 시간에 열리고 닫혀야 합니다. 승객이 문을 가로막거나 문 열기 또는 닫기 단추를 누르거나 층을 선택하고 마음을 바꿀 수
있습니다. 엘리베이터 제어 시스템의 감시 하에 디스플레이를 갱신하고 모터를 제어하는 등의 조작이 수행되어야 합니다. 전체적으로, 이는 동시성 개념을 조사하기에 적합하고 어느 정도의 일반적인 이해와 실용적인 용어를
공유하는 좋은 모델입니다.

그림 2: 두 개의 엘리베이터와 11층에 걸쳐 분산된 다섯 명의 승객이 관련된 시나리오.
잠재적 승객은 서로 다른 시간에 시스템에 요구하며, 시스템은 엘리베이터의 현재 상태와 예상된 응답 시간을 기반으로 호출에 응답할 엘리베이터를 선택하여 최상의 서비스를 제공하려고 합니다. 예를 들어, 첫 번째 잠재적
승객 Andy는 엘리베이터에 내려오도록 호출했는데 둘 다 대기 상태이므로, 가장 가까운 엘리베이터 2가 응답합니다. 하지만 이 엘리베이터는 먼저 Andy를 향해 위로 올라가야 합니다. 한편, 잠시 후 두 번째
잠재적 승객 Bob이 엘리베이터에 위로 올라오도록 요청하면 더 멀리 있는 엘리베이터 1이 응답합니다. 엘리베이터 2가 아래에서 위로 올라오도록 하는 요청에 응답하기 전에 아직 알 수 없는 목적지를 향해 아래로
이동해야 한다는 사실이 알려져 있기 때문입니다.
엘리베이터 시스템에 단 하나의 엘리베이터가 있어서 그 엘리베이터는 한 번에 한 명의 승객에게만 서비스를 제공해야 할 경우, 일반적인 순차 프로그램으로 처리할 수 있다고 생각하기 쉽습니다. 이 "단순한" 경우에도,
프로그램은 다음 조건을 수용하기 위한 많은 분기를 필요로 합니다. 예를 들어, 승객이 전혀 타지 않고 층도 선택하지 않은 경우 엘리베이터가 다른 호출에 응답하도록 재설정할 수 있습니다.
여러 명의 잠재적 승객의 호출과 여러 승객의 요청을 처리하기 위한 일반적인 요구사항은 이전에 논의한 동시성에 대한 외부 원동력의 예입니다. 잠재적 승객은 자신의 필요에 따라 표면상 일정하지 않은 시간에 엘리베이터
상태에 관계없이 엘리베이터에 요구를 제출합니다. 지나간 결정에 따라 엘리베이터를 계속 안내하면서 언제든지 외부 이벤트에 응답할 수 있는 순차 프로그램을 디자인하는 것은 아주 어렵습니다.
동시 시스템을 효과적으로 디자인하려면 시스템에서의 동시성 역할에 대해 논리적으로 생각해야 하며 이를 위해서는 동시성의 추상이 필요합니다.
동시 시스템의 근본적인 빌딩 블록은 다소 서로 독립적으로 진행하는 "활동"입니다. 이와 같은 활동에 대해 생각할 때 유용한 그래픽 추상이 Buhr의 "타임스레드"입니다. [BUH96] 그림 3의 엘리베이터 시나리오는 실제로 타임스레드 양식을 사용했습니다. 각 활동은 활동이 이동하는 선으로 표시됩니다. 큰 점은 활동이
시작되거나 진행하기 전에 이벤트 발생을 기다리는 위치를 표시합니다. 하나의 활동이 다른 활동을 계속하도록 트리거할 수 있습니다. 이는 다른 타임스레드의 대기 위치를 건드림으로써 타임스레드 표기로 표시됩니다.
그림 3: 실행 스레드의 가시성
소프트웨어의 기본적인 빌딩 블록은 프로시저와 데이터 구조이지만, 이것만으로는 동시성에 대해 추론하기에 부적합합니다. 프로세서가 프로시저를 실행할 때 프로세서는 현재 조건에 따라 특정 경로를 따라갑니다. 이 경로는
"실행 스레드" 또는 "제어 스레드"라고 할 수 있습니다. 이 제어 스레드는 당시에 존재하는 조건에 따라 다른 분기 또는 루프를 택할 수 있으며, 실시간에서는 시스템이 지정된 시간 동안 일시정지하거나 계획된 시간이
될 때까지 기다려서 재개할 수 있습니다.
프로그램 디자이너의 관점에서 볼 때, 실행 스레드는 프로그램의 논리로 제어되고 운영 체제에 의해 계획됩니다. 소프트웨어 디자이너가 하나의 프로시저로 하여금 다른 프로시저를 호출하도록 선택하면, 실행 스레드는 하나의
프로시저에서 다른 프로시저로 점프한 후 리턴 명령문이 발견되면 그만둔 위치에서 계속하기 위해 다시 점프합니다.
CPU 관점에서 볼 때, 하드웨어 인터럽트에 응답하기 위해 실행되는 별도의 간단한 스레드에 의해 보충된 소프트웨어를 통해 만들어지는 하나의 기본 실행 스레드가 있습니다. 다른 실행 스레드는 이 모델에서 빌드되므로
디자이너가 이에 대해 알아야 합니다. 실시간 시스템의 디자이너는 다른 유형의 소프트웨어 디자이너보다 더 상세한 레벨에서 시스템이 작동하는 방법을 이해해야 합니다. 그러나 이 모델은 동시성을 아주 정밀하지 않은
세분성(CPU 세분성)으로만 표시할 수 있는 낮은 추상 레벨에 있습니다. 복잡한 시스템을 디자인하려면 다양한 추상 레벨에서 작업할 수 있도록 하는 것이 유용합니다. 물론 추상은 당면한 문제의 중요 사항에 초점을
맞출 수 있도록 불필요한 세부사항을 억제하는 보기 또는 모델의 작성입니다.
한 레벨 위로 이동할 경우, 보통 계층 관점에서 소프트웨어를 생각하게 됩니다. 가장 기본적인 레벨에서, 운영 체제(OS)는 하드웨어 및 응용프로그램 소프트웨어 사이에 놓입니다. OS는 메모리, 타이밍 및 입출력과
같은 하드웨어 기반 서비스를 응용프로그램에 제공하지만, 실제 하드웨어 구성과 독립적인 가상 시스템을 작성하기 위해 CPU를 추상화합니다.
동시성을 지원하려면, 시스템이 여러 개의 제어 스레드에 대비해야 합니다. 제어 스레드의 추상은 하드웨어나 소프트웨어에 의해 여러 방법으로 구현될 수 있습니다. 가장 공통적인 메커니즘은 다음 중 하나의
변형입니다([DEI84], [TAN86]).
-
다중 처리 - 동시에 여러 개의 CPU가 실행됨
-
멀티태스킹 - 운영 체제가 다른 타스크의 실행 인터리빙으로 단일 CPU에서
동시성을 시뮬레이트함
-
응용프로그램 기반 솔루션 - 응용프로그램 소프트웨어가 적절한 시간에 코드의 서로 다른 분기 사이에
전환되도록 하는 책임을 맡음
운영 체제가 멀티태스킹을 제공할 경우 동시성의 공통 단위는 프로세스입니다. 프로세스는 유일한 목적이 프로그램을 실행할 환경을 제공하는 것인 운영 체제가 제공, 지원 및 관리하는 엔티티입니다. 프로세스는 해당
응용프로그램의 독점 사용, 응용프로그램 실행을 위한 실행 스레드, 다른 프로세스와의 메시지 송수신 수단을 위한 메모리 공간을 제공합니다. 사실상, 프로세스는 응용프로그램의 동시 부분을 실행하기 위한 가상
CPU입니다.
프로세스마다 세 가지의 가능한 상태가 있습니다.
-
차단됨 - 입력 수신을 기다리거나 자원 제어를 얻습니다.
-
준비 - 운영 체제에서 실행할 차례가 되기를 기다립니다.
-
실행 중 - 실제로 CPU를 사용 중입니다.
또한 프로세스에는 종종 상대적 우선순위가 지정됩니다. 운영 체제 커널은 상태, 우선순위 및 스케줄 정책을 기반으로 지정된 시간에 실행할 프로세스를 판별합니다. 멀티태스킹 운영 체제는 실제로 모든 프로세스 사이에
단일 제어 스레드를 공유합니다.
참고: '타스크' 및 '프로세스' 용어는 종종 서로 바꿔서 사용할 수 있습니다. 불행히도, '멀티태스킹(multitasking)'은 일반적으로 한 번에 여러 개의 프로세스를 관리할 수 있는 능력을
의미하는 데 사용되는 반면, '다중 처리(multiprocessing)'는 여러 개의 프로세서(CPU)가 있는 시스템을 말합니다. 이 규칙이 가장 일반적으로 수용되므로 이를 따릅니다. 그러나 '타스크' 용어는
주의깊게 사용하려고 합니다. 이 경우, 수행 중인 작업 단위(타스크)와 자원 및 환경을 제공하는 엔티티(프로세스) 사이에 확실하게 구분됩니다.
이전에 설명한 것처럼, CPU 관점에서 보면 단 하나의 기본 실행 스레드가 있습니다. 응용프로그램이 서브루틴을 호출하여 하나의 프로시저에서 다른 프로시저로 점프할 수 있는 것처럼, 운영 체제는 인터럽트, 프로시저
경쟁 또는 기타 이벤트 발생 시 하나의 프로세스에서 다른 프로세스로 제어를 전송할 수 있습니다. 프로세스가 제공하는 메모리 보호로 인해, 이 "타스크 전환"은 상당한 오버헤드를 초래할 수 있습니다. 게다가, 스케줄
정책 및 프로세스 상태는 거의 응용프로그램 시점에 관계하지 않으므로, 프로세스 인터리빙은 보통 응용프로그램에 중요한 동시성 유형에 대해 너무 낮은 레벨의 추상이 됩니다.
동시성에 대해 명확하게 추론하려면, 실행 스레드 개념과 타스크 전환 개념 사이의 명확한 분리를 유지해야 합니다. 각 프로세스는 자체의 실행 스레드를 유지하는 것으로 생각할 수 있습니다. 운영 체제가 프로세스 사이에
전환할 경우, 하나의 실행 스레드가 임시로 인터럽트되고 다른 실행 스레드가 시작되거나 이전에 그만둔 위치에서 재개됩니다.
많은 운영 체제(특히 실시간 응용프로그램에 사용되는 운영 체제)는 프로세스에 대해 "경량"의 대안("스레드" 또는 "경량 스레드"라고 함)을 제공합니다.
스레드는 프로세스 내에서 동시성의 약간 더 정밀한 세분성을 달성하는 방법입니다. 각 스레드는 단일 프로세스에 속하므로, 프로세스의 모든 스레드는 단일 메모리 공간과 해당 프로세스의 제어를 받는 다른 자원을
공유합니다.
일반적으로 각 스레드에는 실행할 프로시저가 지정됩니다.
참고: '스레드' 용어는 여러 의미로 사용됩니다. '스레드' 단어를 단독으로 사용할 경우(여기에서 사용하는 것처럼) 이는 운영 체제가 제공하고 관리하는 '실제 스레드'를 가리키는 것입니다. 앞에서 말한
내용처럼 '실행 스레드', '제어 스레드' 또는 '타임스레드'를 언급하는 경우 반드시 실제 스레드와 연관되지는 않게 되는 추상을 의미합니다.
물론, 다중 프로세서는 진정한 동시 실행 기회를 제공합니다. 대개는 각 타스크가 영구적으로 특정 프로세서의 프로세스에 지정되지만, 어떤 상황에서는 다음으로 사용 가능한 프로세서에 동적으로 타스크가 지정될 수
있습니다. 이와 같이 수행하기 위한 가장 액세스 가능한 방법은 "SMP(Symmetric Multiprocessor)"를 사용하는 것입니다. 이와 같은 하드웨어 구성에서는 여러 개의 CPU가 공통 버스를 통해
메모리에 액세스할 수 있습니다.
SMP(Symmetric Multiprocessor)를 지원하는 운영 체제는 사용 가능한 CPU에 동적으로 스레드를 지정할 수 있습니다. SMP를 지원하는 운영 체제의 예로는 SUN의 Solaris와
Microsoft의 Windows NT가 있습니다.
이전에, 동시성이 소프트웨어 복잡도를 증가시키는 동시에 감소시킨다는 모순되어 보이는 주장을 한 적이 있습니다. 동시 소프트웨어는 기본적으로 동시 활동 사이에 "관심사항 분리"를 허용하므로 복잡한 문제점에 대해 더
단순한 솔루션을 제공합니다. 이에 관련하여, 동시성은 소프트웨어의 모듈화를 높이는 다른 하나의 도구일 뿐입니다. 시스템이 현저하게 독립적인 활동을 수행하거나 현저하게 독립적인 이벤트에 응답해야 할 경우, 개별적인
동시 컴포넌트에 이 활동과 이벤트를 지정하면 자연스럽게 디자인이 단순해집니다.
동시 소프트웨어에 연관된 추가 복잡도는 거의 전적으로 동시 활동이 거의 독립적이지만 완전히 독립적이지는 않은 상황에서 발생합니다. 즉, 복잡도는 상호작용에서 발생됩니다. 실제로, 비동기 활동 사이의 상호작용에는
신호 또는 정보 양식의 교환이 반드시 관련됩니다. 동시 제어 스레드 사이의 상호작용은 동시 시스템에 고유하며 시스템이 올바르게 동작하도록 보장하기 위해 처리해야 하는 일련의 문제를 초래합니다.
프로세스 간 통신(IPC)이나 스레드 간 통신 메커니즘에 대한 서로 다른 많은 특정 실현이 있지만, 결국은 모두 두 카테고리로 분류됩니다.
비동기 통신에서, 송신 활동은 수신자가 수신할 준비가 되어 있는지 여부에 관계없이 정보를 보냅니다. 이와 같은 방법으로 정보를 실행하고 나면, 송신자는 다음으로 수행해야 다른 내용으로 진행합니다.
수신자가 정보를 수신할 준비가 되어 있지 않으면, 정보는 수신자가 나중에 검색할 수 있는 대기열에 넣어집니다. 송신자와 수신자 모두는 서로 비동기식으로 작동하므로, 서로의 상태에 대해 가정할 수 없습니다. 비동기
통신은 종종 메시지 전달이라고 합니다.
동기 통신에는 정보 교환 이외에 송신자와 수신자 사이의 동기화가 포함됩니다. 정보 교환 시, 두 개의 동시 활동은 실행을 위해 서로 병합하고(사실상, 코드의 공유 세그먼트) 통신이 완료되면 다시
분할합니다. 따라서 이 간격 중에 두 활동이 서로 동기화되고 서로의 동시성 충돌 영향을 받지 않습니다. 하나의 활동(송신자 또는 수신자)이 다른 활동 이전에 통신할 준비가 완료되면, 다른 활동도 준비 완료 상태가
될 때까지 일시중단됩니다. 이와 같은 이유로, 이 통신 모드를 랑데부라고도 합니다.
동기 통신에 대해 잠재된 문제점은, 해당 피어가 준비되기를 기다리는 동안 활동이 다른 이벤트에 반응할 수 없다는 것입니다. 많은 실시간 시스템의 경우, 이는 중요한 상황에 대한 적시 응답을 보장할 수 없으므로 항상
허용 가능하지는 아닙니다. 다른 결점은 교착 상태에 빠지기 쉽다는 것입니다. 교착 상태는 두 개 이상의 활동이 서로에 대해 대기하는 악순환과 관련될 때 발생합니다.
동시 활동 사이에 상호작용이 필요할 경우, 디자이너는 동기 또는 비동기 스타일 중에서 선택해야 합니다. 동기는 두 개 이상의 동시 제어 스레드가 단일 시점에서 랑데부해야 함을 의미합니다. 이는 일반적으로, 하나의
제어 스레드는 다른 제어 스레드가 요청에 응답할 때까지 기다려야 함을 의미합니다. 가장 간단하면서도 일반적인 동기 상호작용 양식은 동시 활동 A가 A의 고유 작업을 진행하기 위해 동시 활동 B의 정보가 필요한
경우에 발생합니다.
물론 동기 상호작용은 비동시 소프트웨어 컴포넌트의 기준입니다. 일반 프로시저 호출은 동기 상호작용의 가장 좋은 예입니다. 하나의 프로시저가 다른 프로시저를 호출할 때 호출하는 프로시저는 피호출 프로시저에 즉시
제어를 전송하고 다시 제어가 전송될 때까지 "기다립니다". 그러나 동시 세계에서는 달리 독립적인 제어 스레드를 동기화하기 위한 추가 장치가 필요합니다.
비동기 상호작용에서는 시간 내의 랑데부가 필요하지 않지만, 두 제어 스레드 사이의 통신을 지원하기 위한 추가 장치는 여전히 필요합니다. 종종 이 장치는 메시지를 비동기식으로 송수신할 수 있도록 메시지 대기열을
사용하는 통신 채널 양식을 취합니다.
응답을 기다려야 하는지 아니면 메시지 수신자가 메시지를 처리하는 동안 다른 작업을 수행할 수 있도록 할 것인지 여부에 따라 단일 응용프로그램에서 동기 및 비동기 통신을 혼합할 수 있습니다.
프로세스 또는 스레드의 진정한 동시성은 동시 실행 프로세스 또는 스레드가 있는 다중 프로세서에서만 가능합니다. 단일 프로세서에서는 운영 체제 스케줄러로 스레드 또는 프로세스의 동시 실행을 흉내냅니다. 스케줄러는 몇
개의 스레드 또는 프로세스가 동시에 실행 중인 것처럼 보이도록 사용 가능한 처리 자원을 작은 덩어리로 나눕니다. 잘못된 디자인은 자주 동기식으로 통신하는 여러 개의 프로세스 또는 스레드를 작성하여 이 시간 분할이
불가능하도록 만듭니다. 결국 프로세스 또는 스레드가 많은 "시간 분할 조각"을 다른 프로세스 또는 스레드의 응답을 효과적으로 차단하고 기다리는 데 소비하게 됩니다.
동시 활동이 활동 사이에 공유해야 하는 희소 자원에 의존할 수 있습니다. 일반적인 예로 입출력 장치가 있습니다. 하나의 활동에 다른 활동이 사용 중인 자원이 필요할 경우 차례를 기다려야 합니다.
동시 시스템 디자인의 가장 근본적인 문제는 "경쟁 조건" 회피입니다. 시스템 파트가 상태 종속 함수(결과가 시스템의 현재 상태에 따라 결정되는 함수)를 수행해야 할 경우, 그 파트는 오퍼레이션 동안 상태가
고정되도록 보장되어야 합니다. 즉, 특정 오퍼레이션이 "원자식(atomic)"이어야 합니다. 두 개 이상의 제어 스레드가 동일한 상태 정보에 액세스할 수 있을 때마다, "동시성 제어" 양식은 다른 스레드가 원자인
상태 종속 오퍼레이션을 수행하는 동안 하나의 스레드가 상태를 수정하지 못하도록 보장해야 합니다. 동일한 상태 정보에 동시에 액세스하여 상태가 내부적으로 일치하지 않도록 만드는 것을 "경쟁 조건"이라고 합니다.
경쟁 조건의 일반적인 예는 엘리베이터 시스템에서 승객이 층을 선택할 때 쉽게 발생할 수 있습니다. 엘리베이터는 각각의 방향(위 및 아래)으로 이동할 때 방문할 층 목록을 사용하여 작동합니다. 엘리베이터가 층에
도착할 때마다 하나의 제어 스레드가 해당 목록에서 그 층을 제거하고 목록에서 다음 목적지를 가져옵니다. 목록이 비어 있으면, 엘리베이터는 다른 목록에 층이 있을 경우 방향을 변경하고 그렇지 않고 두 개의 목록 모두
비어 있으면 대기 상태가 됩니다. 다른 제어 스레드는 승객이 층을 선택할 때마다 적절한 목록에 층 요청을 넣어야 합니다. 각 스레드는 목록에서 본래 원자식이 아닌 오퍼레이션의 조합을 수행합니다(예: 다음으로 사용
가능한 슬롯을 확인하고 슬롯을 채움). 스레드가 해당 오퍼레이션을 인터리브할 경우 스레드는 목록의 동일 슬롯에 겹쳐쓰기 쉽습니다.
교착 상태는 두 개의 제어 스레드가 각각 차단되어 있고 각 제어 스레드가 다른 제어 스레드에서 어떤 조치를 취하기를 기다리고 있는 조건입니다. 얄궂게도, 교착 상태는 종종 동기화 메커니즘을 적용하여 경쟁 조건을
막으려고 할 경우에 발생합니다.
경쟁 조건의 엘리베이터 예는 쉽게 비교적 양호한 교착 상태 사례를 가져올 수 있습니다. 엘리베이터 제어 스레드는 목록이 비어 있다고 생각하므로 다른 층에 서지 않습니다. 층 요청 스레드는 엘리베이터가 목록을 비우고
있다고 생각하므로 엘리베이터에게 대기 상태에서 나올 것을 알릴 필요가 없다고 여깁니다.
"근본적인" 문제 외에, 동시 소프트웨어 디자인에서 명시적으로 처리해야 하는 실제적인 문제가 있습니다.
단일 CPU 내에서, 타스크 사이에 전환하여 동시성을 시뮬레이트하는 데 필요한 메커니즘에는 그렇지 않을 경우 응용프로그램 자체에 사용될 수 있는 CPU 주기가 사용됩니다. 한편, 예를 들어 소프트웨어가 입출력
장치를 기다려야 할 경우에 동시성으로 제공되는 성능 개선이 추가되는 오버헤드보다 훨씬 클 수 있습니다.
동시 소프트웨어는 순차 프로그래밍 응용프로그램에 필요하지 않은 조정 및 제어 메커니즘을 필요로 합니다. 이 메커니즘은 동시 소프트웨어를 더 복잡하게 만들고 오류 발생 기회를 증가시킵니다. 동시 시스템의 문제점은
또한 다중 제어 스레드로 인해 본래 진단하기가 더 어렵다는 것입니다. 한편, 이전에 지적한 바와 같이 외부 원동력 자체가 동시적인 경우, 서로 다른 이벤트를 독립적으로 처리하는 동시 소프트웨어는 임의 순서로
이벤트를 수용해야 하는 순차 프로그램보다 더 훨씬 단순할 수 있습니다.
많은 요소를 사용하여 동시 컴포넌트의 실행 인터리빙을 결정하므로, 동일 소프트웨어가 동일 이벤트 시퀀스에 대해 다른 순서로 응답할 수 있습니다. 디자인에 따라 이와 같이 순서를 변경하면 다른 결과를 생성할 수
있습니다.
응용프로그램 소프트웨어는 동시성 제어 구현에 포함될 수도, 포함되지 않을 수도 있습니다. 다음은 일련의 가능성을 많이 관련된 순서로 나열한 것입니다.
-
응용프로그램 타스크는 운영 체제가 언제든지 인터럽트할 수 있습니다(우선적 멀티태스킹).
-
응용프로그램 타스크는 인터럽트할 수 없는 원자 처리 단위(주요 섹션)를 정의하고 이들이 들어오고 나갈 때 운영 체제에 알릴 수 있습니다.
-
응용프로그램 타스크는 CPU 제어를 다른 타스크로 넘기는 시기를 결정할 수 있습니다(협업 멀티태스킹).
-
응용프로그램 소프트웨어는 다양한 타스크의 실행 스케줄을 작성하고 제어하기 위한 완전한 책임을 맡을 수 있습니다.
위의 가능성은 완전한 세트도 아니고 상호 배타적이지도 않습니다. 주어진 시스템에서 가능성을 조합하여 사용할 수 있습니다.
동시 시스템 디자인에서 공통적인 실수는 디자인 프로세스에서 너무 일찍 동시성에 사용할 특정 메커니즘을 선택하는 것입니다. 메커니즘마다 장단점이 있으므로 특정 상황에 맞는 "최상의" 메커니즘 선택은 종종 미묘한 절충
및 타협으로 결정됩니다. 메커니즘 선택이 빠를수록 선택의 기반이 되는 정보는 적어집니다. 메커니즘을 확실히 하면 다른 상황에 대한 디자인의 유연성 및 적응성도 감소하는 경향이 있습니다.
대부분의 복잡한 디자인 타스크와 같이, 동시성은 여러 레벨의 추상을 사용할 때 가장 잘 이해할 수 있습니다. 먼저, 시스템의 기능적 요구사항은 원하는 동작 측면에서 제대로 이해되어야 합니다. 다음은 동시성에 가능한
역할을 탐색해야 합니다. 이는 특정 구현까지 수행하지 않고 스레드 추상을 사용할 때 가장 잘 수행됩니다. 가능한 범위까지 동시성 실현을 위한 메커니즘의 최종 선택을 남겨두어, 다양한 제품 구성마다 다르게 컴포넌트를
분배할 수 있는 유연성과 성능의 미세 조정을 허용해야 합니다.
문제점 도메인(예: 엘리베이터 시스템)과 솔루션 도메인(소프트웨어 구조) 사이의 "개념적 차이"는 시스템 디자인에서 가장 큰 어려움 중 하나로 남아 있습니다. "시각적 형식주의(visual formalisms)"는
동시 동작과 같은 복잡한 개념을 이해하고 커뮤니케이션하여 개념적 차이를 좁히는 역할을 하는 데 도움이 됩니다. 그 중에서 이와 같은 문제점에 유용한 것으로 밝혀진 도구는 다음과 같습니다.
-
동시에 동작하는 컴포넌트를 계획하기 위한 모듈 다이어그램
-
동시 및 대화식 활동(컴포넌트에 대해 직교할 수 있는 활동)을 계획하기 위한 타임스레드
-
컴포넌트 사이의 상호작용을 시각화하기 위한 시퀀스 다이어그램
-
컴포넌트의 상태 및 상태 종속 동작을 정의하기 위한 상태 전이 다이어그램 차트
동시 소프트웨어 시스템을 디자인하려면 동시성의 빌딩 블록(제어 스레드)에 소프트웨어의 빌딩 블록(프로시저 및 데이터 구조)을 결합해야 합니다. 동시 활동의 개념에 대해 논의했지만 이 개념에서는 활동에서 시스템을
구성하지 않습니다. 컴포넌트에서 시스템을 구성하므로 동시 컴포넌트에서 동시 시스템을 구성한다고 볼 수 있습니다. 이 자체로 볼 때, 프로시저도, 데이터 구조도, 제어 스레드도 동시 컴포넌트의 아주 자연스러운 모델은
아니지만 필요한 모든 요소를 하나의 간결한 패키지로 결합하는데 오브젝트의 사용이 자연스러워 보입니다.
오브젝트는 고유 상태 및 동작을 사용하여 프로시저 및 데이터 구조를 결합력 있는 컴포넌트로 패키징합니다. 오브젝트는 상태 및 동작의 특정 구현을 캡슐화하고 다른 오브젝트나 소프트웨어가 상호작용할 수 있는
인터페이스를 정의합니다. 오브젝트는 일반적으로 현실 세계 엔티티 및 개념을 모델링하고 메시지를 교환하여 다른 오브젝트와 상호작용합니다. 이제 오브젝트는 복잡한 시스템을 구현하기 위한 최상의 방법으로 많이 채택되고
있습니다.
그림 4: 엘리베이터 시스템에 대한 단순한 오브젝트 세트
엘리베이터 시스템에 대한 오브젝트 모델을 고려해 보십시오. 각 층에 있는 호출 조작반 오브젝트는 해당 층에 있는 위 및 아래 호출 단추를 모니터합니다. 예상 승객이 단추를 누르면 호출 조작반 오브젝트는 엘리베이터
디스패처 오브젝트에 메시지를 송신하여 응답합니다. 엘리베이터 디스패처 오브젝트는 가장 빠르게 서비스를 제공할 것 같은 엘리베이터를 선택하고 그 엘리베이터를 디스패처하여 호출 수신을 확인합니다. 각 엘리베이터
오브젝트는 동시에, 그리고 독립적으로 실제 엘리베이터 카운터파트를 제어하여, 승객의 층 선택과 디스패처로부터의 호출에 응답합니다.
동시성은 오브젝트 모델처럼 두 가지 양식을 취합니다. 오브젝트 간 동시성은 두 개 이상의 오브젝트가 별도의 제어 스레드를 통해 독립적으로 활동을 수행할 경우에 얻어집니다. 오브젝트 내부 동시성은 여러 개의 제어
스레드가 단일 오브젝트에서 활성화될 때 발생합니다. 오늘날 대부분의 객체 지향 언어에서는 오브젝트가 "수동적"이므로 자체 제어 스레드를 가지고 있지 않습니다. 제어 스레드는 외부 환경에서 제공되어야 합니다. 가장
일반적으로, 환경은 C++ 또는 Smalltalk와 같은 언어로 작성된 객체 지향 "프로그램"을 실행하기 위해 작성되는 표준 OS 프로세스입니다. OS가 멀티스레딩을 지원할 경우, 동일한 오브젝트나 서로 다른
오브젝트에서 여러 개의 스레드가 활성화될 수 있습니다.
아래의 그림에서, 수동 오브젝트는 원 모양 요소로 표시됩니다. 각 오브젝트의 음영 처리된 내부 영역은 자체의 상태 정보이고 구분된 바깥 원은 오브젝트의 동작을 정의하는 프로시저(메소드) 세트입니다.

그림 5: 오브젝트 상호작용 설명
여러 개의 제어 스레드가 동일한 메모리 공간(이 경우 오브젝트에 캡슐화된 데이터)에 대한 액세스를 가지고 있을 경우의 경쟁 조건 가능성과 같이, 오브젝트 내부 동시성은 자체로 모든 동시 소프트웨어의 문제를
발생시킵니다. 데이터 캡슐화가 이와 같은 문제의 솔루션을 제공한다고 생각하기 쉽습니다. 문제점은 오브젝트가 제어 스레드를 캡슐화하지 않는다는 것입니다. 오브젝트 간 동시성으로 이 문제의 대부분을 피할 수 있지만,
여전히 어려운 문제점이 하나 남아 있습니다. 두 개의 동시 오브젝트가 메시지를 교환하며 상호작용할 수 있도록 하려면 최소 두 개의 제어 스레드가 메시지를 처리하고 동일 메모리 공간에 액세스하여 넘겨야 합니다.
관련된(더 어려운) 문제점은 서로 다른 프로세스나 프로세서 사이에 오브젝트를 분배하는 것입니다. 다른 프로세스에 있는 오브젝트 사이의 메시지는 프로세스 간 통신 지원을 필요로 하므로, 일반적으로 메시지는 프로세스
경계를 거쳐 전달될 수 있는 데이터로 인코드 및 디코드되어야 합니다.
물론 이 문제점들은 해결할 수 있습니다. 사실, 이전 섹션에서 지적한 바와 같이 모든 동시 시스템에서 이 문제점을 처리해야 하며 증명된 솔루션도 있습니다. 단지 "동시성 제어"가 불필요한 작업을 야기하여 추가 오류
발생 기회가 생긴다는 것이 문제입니다. 게다가 이로 인해 응용프로그램 문제점의 본질이 모호해집니다. 이런 이유로, 응용프로그램 프로그래머가 명시적으로 이 문제점을 처리해야 하는 필요성을 최소화하려고 합니다. 한
가지 방법은 동시 오브젝트(동시 제어 포함) 사이에 전달되는 메시지를 제어하여 객체 지향 환경을 빌드하고 단일 오브젝트 내에서 다중 제어 스레드를 최소화하거나 사용하지 않는 것입니다. 이렇게 하면 데이터와 함께
제어 스레드를 캡슐화합니다.
자체의 제어 스레드를 가지고 있는 오브젝트를 "활성 오브젝트"라고 합니다. 다른 활성 오브젝트와의 비동기 통신을 지원하기 위해, 메시지 대기열 또는 "편지함"이 각각의 활성 오브젝트에 제공됩니다. 오브젝트가
작성되면 환경은 고유한 제어 스레드를 오브젝트에 제공합니다. 이 제어 스레드는 중단될 때까지 오브젝트에 캡슐화됩니다. 수동 오브젝트와 같이, 활성 오브젝트는 외부에서 메시지가 도착할 때까지 대기 상태입니다.
오브젝트는 메시지를 처리하기 위해 적절한 코드를 실행합니다. 오브젝트가 바쁠 때 도착하는 메시지는 편지함 대기열에 넣어집니다. 오브젝트가 메시지 처리를 완료하면, 오브젝트는 돌아가서 편지함에서 다음 대기 중
메시지를 선택하거나 메시지가 도착하기를 기다립니다. 엘리베이터 시스템에서의 활성 오브젝트의 좋은 후보로는 엘리베이터 자체, 각 층에 있는 호출 조작반, 디스패처가 있습니다.
구현에 따라, 활성 오브젝트를 아주 효율적으로 만들 수 있습니다. 그러나 활성 오브젝트에는 수동 오브젝트보다 더한 오버헤드가 있습니다. 따라서, 모든 오퍼레이션이 동시성을 필요로 하는 것은 아니므로, 일반적으로
동일 시스템에서 활성 및 수동 오브젝트가 혼합됩니다. 서로 다른 통신 스타일로 인해 대등하게 만드는 것은 어렵지만, 활성 오브젝트는 수동 오브젝트에 이상적인 환경을 만들어서 이전에 사용한 OS 프로세스를
대체합니다. 사실, 활성 오브젝트가 모든 작업을 수동 오브젝트로 위임하면 프로세스 간 통신 기능을 가지고 있는 OS 프로세스 또는 스레드와 기본적으로 동일하게 됩니다. 그러나 더 관심있는 활성 오브젝트는 작업의
일부를 수행할 자체 동작을 가지고 있으며 다른 부분은 수동 오브젝트로 위임합니다.
그림 6: '활성' 오브젝트는 수동 클래스의 환경을 제공합니다.
활성 엘리베이터 오브젝트 내에서의 수동 오브젝트의 좋은 후보로는 엘리베이터가 위로 이동하는 동안 정지해야 하는 층의 목록과 아래로 이동할 경우의 또 다른 층 목록이 있습니다. 엘리베이터는 다음 정지 층을 알기 위해
목록을 요청하고, 목록에 새 정지 층을 추가하며, 지나간 정지 층은 제거할 수 있어야 합니다.
복잡한 시스템은 거의 항상 리프 레벨의 컴포넌트로 가기까지 몇 개의 레벨 깊이를 가지는 서브시스템으로 구현되므로, 활성 오브젝트가 다른 활성 오브젝트를 포함하도록 허용하기 위해 자연스럽게 활성 오브젝트 모델이
확장됩니다.
단일 스레드 활성 오브젝트는 진정한 오브젝트 내부 동시성을 지원하지 않지만, 많은 응용프로그램에서 작업을 포함된 활성 오브젝트로 위임하는 것이 적절한 대체안이 됩니다. 오브젝트를 기준으로 상태, 동작 및 제어
스레드를 완전히 캡슐화하면 동시성 제어 문제가 단순화되는 중요한 장점을 얻게 됩니다.
그림 7: 중첩 활성 오브젝트를 표시하는 엘리베이터 시스템
예를 들어, 위에 설명된 부분적 엘리베이터 시스템을 고려해 보십시오. 각 엘리베이터에는 문, 승강기 및 제어판이 있습니다. 이 컴포넌트 각각은 동시 활성 오브젝트로 제대로 모델링되었습니다. 문 오브젝트는 엘리베이터
문이 열리고 닫히는 것을 제어하고, 승강기 오브젝트는 감아올리는 기계 장치를 통해 엘리베이터의 위치를 제어하며, 제어판 오브젝트는 층 선택 단추와 문 열기/닫기 단추를 모니터합니다. 동시 제어 스레드를 활성
오브젝트로서 캡슐화하면 이와 같은 모든 동작을 단일 제어 스레드로 관리했을 때보다 훨씬 더 단순한 소프트웨어가 생성됩니다.
경쟁 조건을 논의할 때 설명한 바와 같이, 시스템이 올바르고 예측 가능한 방식으로 작동하도록 하려면 특정의 상태 종속 오퍼레이션이 원자식(atomic)이어야 합니다.
오브젝트가 적절하게 작동하도록 하려면, 메시지 처리 전후에 내부적으로 상태가 일치하도록 해야 합니다. 메시지를 처리하는 동안, 오브젝트의 상태는 임시 상태일 수 있고, 오퍼레이션이 부분적으로만 완료되어 불확실할
수도 있습니다.
오브젝트가 항상 하나의 메시지에 대한 응답을 완료한 후에 다른 메시지에 응답할 경우, 임시 상태는 문제가 되지 않습니다. 하나의 오브젝트를 인터럽트하여 다른 오브젝트를 실행해도 문제가 없습니다. 각 오브젝트는
엄격한 상태 캡슐화를 실행하기 때문입니다. (엄밀히 말하면, 반드시 그렇지는 않습니다. 이에 대해서는 곧 설명할 예정입니다.)
오브젝트가 메시지 처리를 인터럽트하여 다른 메시지를 처리하는 상황에서는 경쟁 조건 가능성이 발생하므로 동시 제어를 사용해야 합니다. 이에 따라 교착 상태 가능성도 발생합니다.
따라서, 오브젝트가 각 메시지를 완료한 후 다른 메시지를 받아들일 경우 동시 디자인은 일반적으로 더 단순해집니다. 이 동작은 여기에 표시된 특수 형태의 활성 오브젝트에 내재되어 있습니다.
일관된 상태 문제는 동시 시스템에서 두 가지 양식으로 나타날 수 있으며, 이는 객체 지향 동시 시스템 관점에서 이해하는 것이 더 쉽습니다. 첫 번째 양식은 이전에 논의했습니다. 단일 오브젝트(수동 또는 활성)의
상태가 두 개 이상의 제어 스레드에 액세스할 수 있으면, 기본 CPU 오퍼레이션의 자연 원자성이나 동시성 제어 메커니즘으로 원자 오퍼레이션을 보호해야 합니다.
일관된 상태 문제의 두 번째 양식은 조금 알아보기 어렵습니다. 둘 이상의 오브젝트(활성 또는 수동)에 동일한 상태 정보가 있을 경우, 불가피하게 해당 오브젝트들은 최소한의 짧은 시간 간격 동안 상태가 일치하지
않습니다. 잘못된 디자인에서는 더 오랜 기간 동안(어쩌면 영원히) 일치하지 않을 수 있습니다. 이러한 비일관된 상태는 수학적 "이중"의 다른 양식으로 간주할 수 있습니다.
예를 들어, 엘리베이터 동작 제어 시스템(승강기)은 문이 닫힌 후 엘리베이터가 움직이기 전에는 열리지 않도록 해야 합니다. 적절한 보호책이 없는 디자인에서는 엘리베이터가 이동을 시작할 때 승객이 문 열기 단추를
건드리면 이에 대한 응답으로 문이 열릴 수 있습니다.
이 문제점의 쉬운 솔루션으로 상태 정보를 하나의 오브젝트에만 상주하도록 하면 될 듯 합니다. 이 솔루션은 도움이 될 수 있지만 성능에 불리한 영향을 줄 수도 있습니다(특히 분산 시스템에서). 게다가 아주 간단한
솔루션도 아닙니다. 하나의 오브젝트만 특정 상태 정보를 포함하는 경우에도, 다른 동시 오브젝트가 특정 시점에서 이 상태를 기반으로 결정을 수행하면 상태 변경 시 다른 오브젝트의 결정이 무효화될 수 있습니다.
일관된 상태 문제점에 대한 완벽한 솔루션은 없습니다. 모든 실제 솔루션에서는 원자 오퍼레이션을 식별하고 적당히 짧은 시간 동안 동시 액세스를 차단하는 동기화 메커니즘 방식으로 이 오퍼레이션을 보호해야 합니다.
"적당히 짧은"은 상황에 따라 아주 다릅니다. CPU가 모든 바이트를 부동 소수점 숫자로 저장하는 데 소요되는 시간이나, 엘리베이터가 다음 정지 층까지 가는데 걸리는 시간일 수 있습니다.
실시간 시스템에서, RUP는 캡슐을 사용하여 활성 오브젝트를 표시할 것을 권장합니다. 캡슐에는 동시성 모델링을 단순화하는 강한 시맨틱이 있습니다.
-
이는 제대로 정의된 프로토콜을 사용하는 포트를 통한 비동기 메시지 기반 통신을 사용합니다.
-
캡슐은 메시지 처리를 위해 RTC(Run-to-completion) 시맨틱을 사용합니다.
-
캡슐은 수동 오브젝트를 캡슐화합니다(따라서 스레드 간섭이 발생할 수 없음).
|