교착 상태(DeadLock) : 2개 이상의 프로세스가 다른 프로세스의 작업이 끝나기만 기다리며 작업을 더 이상 진행하지 못하는 상태

Starvation은 운영체제가 잘못된 정책을 사용하여 특정 프로세스의 작업이 지연되는 문제이고, 교착 상태는 여러 프로세스가 작업을 진행하다 자연적으로 일어나는 문제

 

다중 자원 : 여러 프로세스가 하나의 자원을 동시에 사용할 수 있는 자원

 

교착 상태 필요 조건

상호 배제, 비선점, 점유와 대기, 원형 대기를 모두 충족해야 발생, 하나라도 충족하지 않으면 교착 상태가 발생하지 않음

 

상호 배제(Mutual Exclusion) : 한 프로세스가 사용되는 자원은 다른 프로세스와 공유할 수 없는 배타적인 자원이어야 함

(배타적인 자원은 임계구역으로 보호되기 때문에 다른 프로세스가 동시에 사용할 수 없음)

비선점(Nonpreemptive) : 한 프로세스가 사용 중인 자원은 다른 프로세스가 빼앗을 수 없는 비선점 자원이어야 함

 

점유와 대기(Hold And Wait) : 프로세스가 어떤 자원을 할당받은 상태에서 다른 자원을 기다리는 상태여야 함

원형 대기(Circular Wait) : 점유와 대기를 하는 프로세스 간의 관계가 원을 이루어 프로세스들이 서로 양보하지 않아야 함

 

상호 배제와 비선점 조건은 자원의 특징을 나타내고, 점유와 대기, 원형 대기 조건은 프로세스의 행위를 나타냄

 

교착 상태 해결 방법

예방

교착 상태를 유발하는 네 가지 조건이 발생하지 않도록 무력화하는 방식

실효성이 적어 잘 사용되지 않음

  • 상호 배제 예방 : 상호 배타적인 모든 자원을 없애는 방법, 현실적으로 모든 자원을 공유할 수 없으며 보호해야 하는 자원이 있으므로 어려움
  • 비선점 예방 : 모든 자원을 빼앗을 수 있도록 만드는 방법, 상호 배제를 보장할 수 없고 임계구역 보호가 불가능하고 Starvation을 초래하므로 어려움
  • 점유와 대기 예방 : 필요한 자원을 전부 할당하거나 아예 할당하지 않는 방법
    프로세스가 자신이 사용하는 모든 자원을 자세히 알기 어려움
    앞으로 사용할 자원까지 미리 선점해버리기 때문에 자원 낭비가 심함
    많은 자원을 사용하는 프로세스의 작업이 지연되는 Starvation 현상 발생
    실제로 구현하면 일괄 작업 방식으로 처리되므로 시스템의 효율이 떨어짐
  • 원형 대기 예방 : 점유와 대기를 하는 프로세스들이 원형을 이루지 못하도록 막는 방법
    모든 자원에 번호를 부여하고 번호가 큰 방향으로 자원을 할당
    프로세스 작업 진행에 유연성이 떨어짐
    자원의 번호를 어떻게 부여할지의 문제가 있음

 

회피

자원을 할당하다가 교착 상태를 유발할 가능성이 있다고 판단되면 자원 할당을 중단하고 대기하는 방법

 

자원의 총 수와 현재 할당된 자원의 수를 기준으로 시스템을 안정(safe) 상태와 불안정(unsafe) 상태로 나누고 안정 상태를 유지하도록 자원을 할당

불안정 상태에서 항상 교착 상태가 발생하는 것이 아닌 교착 상태 발생 가능성이 높아짐

 

  • 은행원 알고리즘(Banker's Algorithm)
    스레드(프로세스)가 요청하는 최대 자원 개수를 신고
    스레드가 자원을 요청했을 때 안정 상태이면 자원을 할당하고, 불안정 상태이면 다른 스레드가 끝날 때까지 대기

 

자원을 얼마나 할당해야 교착 상태가 발생하지 않는다는 보장이 없기 때문에 실효성이 적음

  • 프로세스가 자신이 사용할 모든 자원을 미리 선언해야 하는데 어려움
  • 시스템의 전체 자원 수가 고정적이어야 하는데, 시스템의 자원 수는 유동적임
  • 모든 불안정 상태가 교착 상태가 되는 것은 아니므로 최대 자원을 사용할 수 없어 자원이 낭비됨

 

검출

어떤 제약을 가하지 않고 자원 할당 그래프를 모니터링하면서 교착 상태가 발생하는지 살펴보는 방식

  • 타임아웃을 이용한 교착 상태 검출 : 일정 시간 동안 작업이 진행되지 않은 프로세스를 교착 상태가 발생한 것으로 간주하여 처리
    • 타임아웃 시간 동안 작업이 진행되지 않은 모든 프로세스가 교착 상태 때문에 작업이 이루어지지 않은 것은 아니므로 엉뚱한 프로세스가 강제 종료될 수 있음
    • 윈도우의 '프로그램이 응답이 없어 종료합니다' 메시지
  • 자원 할당 그래프를 이용하여 교착 상태 검출
    • 운영체제는 자원을 요청하거나 할당할 때마다 자원 할당 그래프를 갱신하는데, 사이클이 발생하면 교착 상태가 검출된 것으로 판단 
    • 작업이 너무 많아 구현하기 어려워 자주 발생하지 않는 교착 상태를 찾기 위해 자원 할당 그래프를 유지하면서 모든 자원을 감시하기에 어려움

 

회복

교착 상태를 검출한 후 교착 상태를 유발한 프로세스를 강제 종료하여 교착 상태를 푸는 후속 작업

  • 교착 상태를 일으킨 모든 프로세스를 종료 : 종료된 프로세스가 동시에 작업을 시작하면 다시 교착 상태를 일으킬 가능성이 크기 때문에 다시 실행할 때는 순차적으로 실행해야 함
  • 교착 상태를 일으킨 프로세스 중 하나를 골라 순서대로 종료

프로세스를 강제 종료하는 일뿐 아니라 강제 종료된 프로세스가 실행되기 전에 시스템을 복구하는 일도 해야 함

명령어가 실행될 때마다 체크포인트를 만드는 일은 작업량이 상당하여 시스템에 부하를 주므로 선택적으로 사용해야 함

메모리 오버헤드

객체 크기 증가 : 각 객체는 vptr(보통 64비트 시스템에서 8바이트)를 추가로 저장합니다.

vtable 저장 : 각 클래스마다 하나의 vtable이 생성되어 런타임 파일 크기 증가

 

성능 오버헤드

간접 함수 호출

  • 일반 함수 호출은 컴파일 시점에 해결되어 직접 점프 명령으로 변환
  • 가상 함수 호출은 추가적인 메모리 접근이 필요
    1. 객체에서 vptr 로드
    2. vptr을 통해 vtable 접근
    3. vtable에서 함수 포인터 로드
    4. 로드된 주소로 점프

CPU 파이프라인 최적화 방해 : CPU는 명령어를 미리 가져와 실행하는 분기 예측(branch prediction)을 수행하는데, 가상 함수 호출은 런타임에 결정되므로 분기 예측이 어려워져 파이프라인 스톨(stall)이 발생할 수 있음

  • 파이프라인 스톨 : CPU가 한 사이클 휴식하는 것

 

최적화 기회 감소

인라인화 방해 : 가상 함수는 런타임에 대상이 결정되므로 일반적으로 인라인화가 불가능(예외는 있음)

전체 프로그램 최적화 제한 : 컴파일러가 호출 대상을 정확히 알 수 없으므로 상수 전파, 데드 코드 제거 등의 최적화가 제한됨

 

간접 참조로 인한 캐시 미스

  1. 객체에서 vptr(가상 함수 테이블 포인터)를 로드
  2. vptr를 통해 vtable(가상 함수 테이블)에 접근
  3. vtable에서 함수 포인터를 로드
  4. 해당 함수 포인터로 점프 : 코드 세그먼트

이 각 단계는 서로 다른 메모리 위치에 접근하므로 데이터 지역성이 저하되어 여러 캐시 미스의 원인이 됨

(일반 함수 호출은 컴파일 시점에 주소가 결정되어 직접 점프하므로 이런 추가적인 메모리 접근이 없음)

  1. 객체 데이터: 객체의 멤버 변수들은 보통 서로 인접해 있습니다.
  2. vtable: 프로그램의 읽기 전용 데이터 영역에 각 클래스마다 하나씩 존재
  3. 함수 코드: 또 다른 메모리 영역(코드 세그먼트)에 있습니다.

선형 변환

수학 함수 r(v) = r(x, y, z) = (x', y', z')

아래의 조건들을 만족하면 r을 가리켜 선형 변환(linear transformation)이라 부름

  • r(u + v) = r(u) + r(v)
  • r(ku) = kr(u)

 

u = (x, y, z) = xi + yj + zk = x(1, 0, 0) + y(0, 1, 0) + z(0, 0, 1)

i, j, k는 좌표계의 축들과 같은 방향인 단위벡터이고 이들을 표준기저벡터라고 부름

r(u) = r(xi + yj + zk) = xr(i) + y(j) + z(k)

위 식은 하나의 선형결합(일차결합)이므로 하나의 벡터 행렬 곱셈 형태로 표기할 수 있음

uA = [xA11 + yA21 + zA31, xA12 + yA22 + zA32, xA13 + yA23 + zA33]

     = [xA11, xA12, xA13] + [yA21 + yA22 + yA32] + [zA31 + zA32 + zA33]

     = xA1,* + yA2,* + zA3,*

r(i) = (A11, A12, A13), r(j) = (A21, A22, A23), r(k) = (A31, A32, A33)

이러한 행렬 A를 선형변환 r의 행렬 표현이라고 부름

 

비례

비례 변환은 다음과 같이 정의됨

S는 하나의 선형변환이므로 행렬 표현이 존재함

S(i) = (sx, 0, 0)

S(j) = (0, sy, 0)

S(k) = (0, 0, sz)

위 행렬을 비례 행렬이라고 함

 

회전

회전각은 축 n을 내려다 볼 때(머리에서 꼬리 쪽으로) 시계 방향으로 측정, ||n|| = 1이라고 가정

회전하려는 벡터 v를 n에 평행한 벡터 projn(v) = (nov)n와 n에 수직인 벡터 vㅗ = perpn(v) = v - projn(v)로 분할

n에 평행한 projn(v)는 축 n과 평행하므로 벡터가 회전해도 변하지 않기 때문에 수직인 부분을 회전하는 방법만 알면 됨

즉 회전된 벡터 Rn(v) = projn(v) + Rn(vㅗ)를 구하면 됨

|| n x v || = ||n||||v|| sina = ||v||sina = ||vㅗ||

vㅗ를 하나의 기준 벡터로 사용하고 n x v를 둘째 기준 벡터로 사용

(vㅗ는 v의 n에 수직인 성분이므로, n x v는 vㅗ에도 수직)

두 기준 벡터는 크기가 같고 회전의 원에 놓여 있음

 

Rn(v) = projn(v) + Rn(vㅗ)

 

회전 행렬 Rn

(c = cos, s = sin)

 

회전행렬의 각 행벡터는 단위길이이고, 서로 직교이므로 행벡터들은 정규직교

직교행렬에는 역행렬이 자신의 전치행렬과 같은 속성이 있음

특히 회전축이 x축, y축, z축인 경우 회전행렬이 아주 간단해짐

 

아핀변환

동차좌표

아핀변환은 선형변환에 이동이 결합된 것

벡터는 위치와 무관하게 방향과 크기만 서술하는 것이므로 이동(위치벡터)는 점에만 적용되어야 하는데, 동차좌표를 이용하면 점과 벡터를 동일한 방식으로 다룰 수 있음

동차좌표는 3차원 벡터에 w 성분을 추가한 4원소쌍의 형태인데, 벡터의 경우 0이고 점의 경우 1

w = 1로 설정하면 이동 시 점이 정확이 이동되며 w = 0으로 설정하면 이동 시 벡터가 변하지 않음

 

정의 및 행렬 표현

아핀변환은 선형변환에 이동 벡터 b를 더한 것

a(u) = r(u) +b

 

w = 1인 동차좌표를 도입하면 간결하게 표기할 수 있음

 

이동

항등 변환 : 주어진 인수(입력)을 그대로 돌려주는 선형변환, I(u) = u

r(u) = uI + b = u + b

u를 b만큼 이동하므로 물체의 모든 점의 위치를 동일한 벡터 b로 이동

 

이동행렬

 

아핀변환 행렬의 기하학적 해석

r가 물체를 얼마나 회전할 것인지를 나타내는 회전 변환이고, b가 물체를 얼마나 이동할 것인지를 나타내는 이동 변환

a(x, y, z) = r(x, y, z) + b = xr(i) + yr(j) + zr(k) + b

r은 표준기저벡터 i, j, k만을 새로운 방향 r(i), r(j), r(k)로 회전

b는 원점으로부터의 변위를 나타내는 위치벡터

 

변환들의 합성

S가 비례행렬, R이 회전행렬, T가 이동행렬일 때 직육면체의 각 정점에 세 변환을 적용할 때 가장 직접적인 방법은 변환 행렬들을 단계별로 적용하는 것

((viS)R)T = vi(SRT)

 

여러 개의 변환들을 하나의 변환으로 결합할 수 있음

이는 성능에 영향을 미쳐 많은 변환들을 연달아 적용하면 n개의 점으로 이루어진 물체를 변환할 때 벡터 행렬 곱셈 n x 3회이지만 결합된 행렬 접근 방식은 벡터 행렬 곱셈 n + 행렬 행렬 곱셈 2회이므로 계산 횟수가 크게 줄어듦

 

좌표 변경 변환

한 좌표계의 상대적인 점 또는 벡터의 좌표를 다른 좌표계의 좌표로 변환하는 것

기하 구조 자체가 바뀌는 것이 아닌 좌표 표현이 변경되는 것

 

벡터

좌표계 A에서 B로 변환한다고 가정

Pa = xu + yv (u와 v는 x축과 y축 방향의 단위벡터)

 

벡터  u와 v의 b 기준 좌표를 알 수 있다면 다른 좌표계에 상대적인 좌표로 변환할 수 있음

Pb = xub + yvb = xub + yvb

 

점의 경우 위치가 중요하므로 벡터를 이동시키는 방식으로 점을 이동할 수는 없음

Pb = xub + yvb + Qb (Qb는 B의 원점에서 A 좌표계의 원점을 가리키는 벡터)

 

행렬 표현

벡터와 점에 대한 좌표 변경 변환을 동차좌표로 하나의 공식으로 처리할 수 있음

(x', y', z', w) = xub + yvb + zwb + wQb

w가 0이면 벡터에 대한 좌표 변경 변환을 의미하고 w가 1이면 점에 대한 좌표 변경 변환을 의미

 

좌표 변경 행렬 또는 좌표계 변환 행렬 또는 좌표 변환 행렬

 

결합 법칙과 좌표 변경 행렬

세 개의 좌표계 F, G, H가 있고 A가 F->G 좌표계 변환 행렬, B가 G->H 좌표계 변환 행렬

(PFA)B = PH

 

결합 법칙을 이용

PF(AB) = PH

C = AB는 F에서 H로 가는 좌표계 변환 행렬이라고 생각할 수 있음(함수의 합성과 비슷)

이는 성능에 영향을 미쳐 많은 변환들을 연달아 적용하면 n개의 점으로 이루어진 물체를 변환할 때 벡터 행렬 곱셈 n x 2회이지만 결합된 행렬 접근 방식은 벡터 행렬 곱셈 n + 행렬 행렬 곱셈 1회이므로 계산 횟수가 크게 줄어듦

 

역행렬과 좌표 변경 행렬

M이 좌표계 A에서 B로의 좌표 변경 행렬이고, 가역행렬(역행렬이 존재)하다고 가정

  • 이 책에서 다루는 모든 좌표계 변환에서는 항상 역행렬이 존재

Pb = PaM

pbM^(-1) = PaMM^(-1) = PaI = Pa

따라서 M^(-1)은 B에서 A로의 좌표 변경 행렬

 

DirectXMath 라이브러리 관련 함수들

XMMATRIX    XM_CALLCONV     XMMatrixScaling(float ScaleX, float ScaleY, float ScaleZ) noexcept; // 비례행렬 생성
XMMATRIX    XM_CALLCONV     XMMatrixScalingFromVector(FXMVECTOR Scale) noexcept; // 벡터의 성분들로 비례행렬 생성

XMMATRIX    XM_CALLCONV     XMMatrixRotationX(float Angle) noexcept; // x축에 대한 회전행렬 Rx 생성
XMMATRIX    XM_CALLCONV     XMMatrixRotationY(float Angle) noexcept; // y축에 대한 회전행렬 Ry 생성
XMMATRIX    XM_CALLCONV     XMMatrixRotationZ(float Angle) noexcept; // z축에 대한 회전행렬 Rz 생성
XMMATRIX    XM_CALLCONV     XMMatrixRotationAxis(FXMVECTOR Axis, float Angle) noexcept; // 임의의 축에 대한 회전행렬 Rn 생성

XMMATRIX    XM_CALLCONV     XMMatrixTranslation(float OffsetX, float OffsetY, float OffsetZ) noexcept; // 이동행렬 생성
XMMATRIX    XM_CALLCONV     XMMatrixTranslationFromVector(FXMVECTOR Offset) noexcept; // 벡터의 성분들로 이동행렬 생성

XMVECTOR    XM_CALLCONV     XMVector3Transform(FXMVECTOR V, FXMMATRIX M) noexcept; // 벡터 행렬 곱 vM 계산
XMVECTOR    XM_CALLCONV     XMVector3TransformCoord(FXMVECTOR V, FXMMATRIX M) noexcept; // 벡터 행렬 곱 vM 계산, 점 변환을 위해 Vw = 1이 적용됨
XMVECTOR    XM_CALLCONV     XMVector3TransformNormal(FXMVECTOR V, FXMMATRIX M) noexcept; // 벡터 행렬 곱 vM 계산, 벡터 변환을 위해 Vw = 0이 적용됨
// 마지막 두 함수의 w 성분을 프로그래머가 직접 설정할 필요 없음

지역 객체는 함수의 유효범위가 끝나면 자동으로 소멸되기 때문에 지역 객체를 포인터로 전달하는 것은 소멸된 객체를 전달하는 위험이 있음

대안은 힙 객체를 전달

void someFuntion()
{
    exception ex;
    
    throw &ex;
}

// 힙 객체 전달
void someFuntion()
{
    throw make_unique();
}

C++에서 기본적으로 제공되는 표준 예외는 bad_alloc(메모리 할당을 실패했을 때), bad_cast(dynamic_cast가 실패했을 때), bad_typeid(dynamic_cast를 nullptr에 적용했을 때), bad_exception(바라지 않은 예외가 발생했을 때)인데, 이 표준 예외는 객체에 대한 포인터가 아니라 모두 객체

따라서 참조자나 값에 의한 전달을 사용하지 않으면 위 예외들을 사용할 수 없음

 

값에 의한 전달은 객체가 두 번씩 복사되고, 슬라이스 문제가 있는 단점이 있음

 

참조자에 의한 전달은 포인터에 의한 전달과 달리 C++ 표준 예외를 처리할 수 있고, 값에 의한 전달과 달리 슬라이스 문제가 없고 한 번만 복사됨

class exception {};
class Validation_error : public exception {};

void someFuntion()
{
    throw Validation_error();
}

void doSomething()
{
    try
    {
        someFuntion();
    }
    catch ( exception& ex)
    {
        cerr << ex.what();
    }
}

프로세스 간 통신(IPC)

동시에 실행되는 프로세스끼리 데이터를 주고받는 작업

프로세스 간 통신에는 같은 컴퓨터에 있는 프로세스 뿐만 아니라 네트워크로 연결된 다른 컴퓨터에 있는 프로세스와의 통신도 포함됨

 

프로세스 내부 데이터 통신 : 하나의 프로세스 내에 2개 이상의 스레드가 존재하는 경우의 통신, 전역 변수나 파일을 이용하여 데이터를 주고 받음

프로세스 간 데이터 통신 : 같은 컴퓨터에 있는 여러 프로세스끼리 통신하는 경우로, 공용 파일이나 메모리 또는 운영체제가 제공하는 파이프를 사용하여 통신

네트워크를 이용한 데이터 통신 : 여러 컴퓨터가 네트워크로 연결되어 있을 때 프로세스가 소켓을 이용하여 데이터를 주고 받음

 

양방향 통신 : 데이터를 동시에 양쪽 방향으로 전송할 수 있는 구조, 소켓 통신

반양방향 통신 : 데이터를 양쪽 방향으로 전송할 수 있지만 동시 전송은 불가능하고 특정 시점에 한쪽 방향으로만 전송할 수 있는 구조, 무전기

단방향 통신 : 한쪽 방향으로만 데이터를 전송할 수 있는 구조, 전역 변수와 파이프

 

전역 변수를 사용하는 통신 방식의 가장 큰 문제는 언제 데이터를 보내는지를 데이터를 받는 쪽에서는 모른다는 것

따라서 상태 변화를 살펴보기 위해 반복문을 무한 실행하며 기다리는 바쁜 대기(busy waiting)는 좋지 않음

busy waiting을 해결하기 위해서는 데이터가 도착했음을 알려주는 동기화(synchronization)을 사용

 

대기가 있는 통신 : 동기화를 지원하는 통신 방식, 데이터를 받는 쪽은 데이터가 도착할 때까지 자동으로 대기 상태, 파이프, 소켓

대기가 없는 통신 : 동기화를 지원하지 않는 통신 방식, 데이터를 받는 쪽은 바쁜 대기를 사용, 전역 변수, 파일

 

공유 메모리

데이터를 공유하는 프로세스가 공통적으로 사용할 메모리 영역을 두는 방식

프로세스가 공유 메모리 영역을 확보하는 시스템 콜을 기반으로 수행하거나 변수/파일을 활용하는 방법

데이터를 공유할 때 커널 영역을 거의 사용하지 않아 속도가 빠름

각각의 프로세스가 공유 메모리 영역을 동시에 읽고 쓸 경우 race condition, 동기화 문제 가능

 

메시지 전달(메시지 큐)

프로세스 간에 공유하는 데이터를 메시지의 형태로 커널을 거쳐 송수신되는 방식

메시지를 보내는 send() 시스템 콜과 메시지를 받는 recv() 시스템 콜을 사용하여 송수신

커널의 도움을 받아 race condition, 동기화 문제는 적지만 통신 속도는 느림

 

공유 자원과 임계 구역

동기화

실행 순서 제어와 상호 배제 조건을 충족하여 실행하는 것을 의미

  • 실행 순서 제어 : 프로세스 및 스레드를 올바른 순서로 실행
  • 상호 배제 : 동시에 접근해서는 안되는 자원에 하나의 프로세스 및 스레드만 접근

 

공유 자원

공유 자원은 여러 프로세스가 공동으로 이용하는 변수, 메모리, 파일 등을 말함

누가 언제 데이터를 읽거나 쓰냐에 따라 결과가 달라질 수 있으므로 프로세스들의 공유 자원 접근 순서를 정하여 문제가 발생하지 않도록 해야 함

 

두 프로세스가 예금 10만 원을 동시에 읽은 후 다른 프로세스의 작업을 무시하고 덮어쓰는 바람에 총액이 맞지 않는 문제가 발생

이처럼 2개 이상의 프로세스가 공유 자원을 병행적으로 읽거나 쓰는 상황을 경쟁 조건(race condition)이 발생했다고 함

 

임계 구역(critical section)

공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역

임계 구역에서는 프로세스들이 동시에 작업하면 안됨

 

생산자-소비자 문제

produce()
{
    input(buf);
    sum += 1;
}

consumer()
{
    output(buf);
    sum -= 1;
}

sum += 1;과 sum -= 1;이 거의 동시에 실행되면 sum 값은 2 또는 4가 될 수 있음

 

임계 구역 해결 조건

상호 배제 : 한 프로세스가 임계구역에 들어가면 다른 프로세스는 임계구역에 들어갈 수 없음

한정 대기 : 특정 프로세스가 임계구역에 진입하지 못하면 안됨

진행의 융통성 : 한 프로세스가 다른 프로세스의 진행을 방해해서는 안됨

 

임계 구역 해결 방법

진행의 융통성 문제

// process A
while(lock == 2);

{
    // 임계 구역
}

lock = 2;

// process B
while(lock == 1);

{
    // 임계 구역
}

lock = 1;

한 프로세스가 다른 프로세스로 인해 방해받아 두 번 연달아 임계 구역에 진입할 수 없는 현상을 경직된 동기화(lookstep synchronization)이라고 함

 

하드웨어적 해결방법

while(testandset(&lock) == true);

{ 
    // 임계 구역
}

lock = false;

testandset은 while(lock == true); 문과 lock = true; 문을 한 번에 실행

바쁜 대기를 사용하여 검사하기 때문에 자원 낭비

 

피터슨 알고리즘

// process A
lock1 = true;
turn = 2;

while(lock2 && turn == 2);

{
    // 임계 구역
}

lock1 = false;

// process B
lock2 = true;
turn = 1;

while(lock1 && turn == 1);

{
    // 임계 구역
}

lock2 = false;

turn은 두 프로세스가 동시에 락을 설정하여 임계 구역에 못 들어가는 상황을 대비하기 위한 장치

프로세스를 추가하려면 변수가 늘어나고 알고리즘이 복잡해지는 단점이 있음

 

데커 알고리즘

// process A
lock1 = true;

while(lock2)
{
    if(turn == 2)
    {
        lock1 = false;
        while(turn == 2);
        lock1 = true;
    }
}

{
    // 임계 구역
}

turn = 2;
lock1 = false;

// process B
lock2 = true;

while(lock1)
{
    if(turn == 1)
    {
        lock2 = false;
        while(turn == 1);
        lock2 = true;
    }
}

{
    // 임계 구역
}

turn = 1;
lock2 = false;

프로세스를 추가하려면 변수가 늘어나고 알고리즘이 복잡해지는 단점이 있음

 

Mutex Lock

동시 접근이 불가능하도록 상호 배제를 보장하는 동기화 도구

임계 구역에 접근하려면 반드시 락을 획득(Acquire)하고, 임계 구역을 벗어나면 락을 해제(Release)해야 함

RAII 기법을 사용하여 객체의 생성자와 소멸자에서 락을 획득 및 소멸하는 방법으로도 구현 가능

락을 획득한 상태에서 다른 프로세스 및 스레드가 락을 획득하려고 하면 락을 해제할 때까지 대기

mutex threadMutex;

void increment()
{
	for (int i = 0; i < threadCount; ++i)
	{
		std::lock_guard<std::mutex> lock(threadMutex);
		++value;
	}
}

void decrement()
{
	for (int i = 0; i < threadCount; ++i)
	{
		std::lock_guard<std::mutex> lock(threadMutex);
		--value;
	}
}

 

세마포어

// 주석은 해당 함수의 내부 코드
Semaphore(n); // Rs = n; // n은 공유 자원의 개수를 의미
P(); // if(RS > 0) RS -= 1;
     // else block();

{
    // 임계 구역
}

V(); // RS += 1;
     // wake_up();

세마포어에서 잠금이 해제되기를 기다리는 프로세스는 세마포어 큐에 저장되어 있다가 wake_up 신호를 받으면 큐에서 나와 임계구역에 진입하기 때문에 바쁜 대기를 하지 않음

P와 V의 내부 코드가 실행되는 도중에 다른 코드가 실행되면 상호 배제와 한정 대기 조건을 보장하지 못하므로 내부 코드는 검사와 지정(testandset)을 사용하여 완전히 실행되게 해야 함

 

피터슨 알고리즘이나 데커 알고리즘보다 단순하고 사용하기 편리

임계구역 전후로 P와 S를 정확히 호출하지 않으면 임계구역이 보호받지 못할 수 있다는 문제가 있음

 

모니터

공유 자원을 내부로 숨기고 공유 자원에 접근하기 위한 인터페이스만 제공함으로써 자원을 보호하고 프로세스 간에 동기화를 시킴

사용자 입장에서는 복잡한 코드를 실행하지 않아서 좋음

 

임계구역으로 지정된 변수나 자원에 접근하고자 하는 프로세스는 모니터에 작업을 요청

모니터는 요청받은 작업을 모니터 큐에 저장한 후 순서대로 처리하고 결과만 해당 프로세스에 전달

 

class BankAccount {
public:
    void deposit(double amount) {
        std::unique_lock<std::mutex> lock(mtx);

        cv.wait(lock, [this]() { return !isProcessing; });
        
        isProcessing = true;

        // 입금 처리
        if (amount > 0) {
            balance += amount;
        }

        isProcessing = false;
        
        cv.notify_one();
    }
    
private:
    double balance;
    std::mutex mtx;
    std::condition_variable cv;
    bool isProcessing;
};

wait : 모니터 큐에서 자신의 차례가 올 때 까지 기다림, 세마포어의 P()에 해당

notify_one : 모니터 큐에서 기다리는 다음 프로세스에 순서를 넘겨줌, 세마포어의 V에 해당

함수 매개변수를 선언하는 문법과 catch 문이 거의 유사함

void f1(Widget w);

catch(Widget w) ...

 

매개변수와 예외는 값에 의한 전달, 참조에 의한 전달, 포인터에 의한 전달이 모두 가능

 

함수로의 매개변수 전달과 catch 문으로의 예외 전달의 차이점

함수를 호출했을 때는 함수를 호출한 부분으로 되돌아가지만, 예외를 발생시켰을 때에는 throw를 호출한 부분으로 되돌아가지 않음

 

istream operator>>(istream& s, Widget& w);

void passAndThrowWidget()
{
    Widget localWidget;
    cin >> localWidget;
    
    throw localWidget;    
}

localWidget이 operator>>로 전달될 때에는 참조에 의한 전달로 객체의 사본이 생성되지 않음

예외로서 발생될 때에는 값에 의한 전달이든, 참조에 의한 전달이든, 포인터에 의한 전달이든 상관 없이 객체의 사본(포인터는 포인터 객체)이 생성되어 전달됨

함수의 유효 범위를 떠난 후에는 localWidget이 소멸되기 때문에 catch문으로 전달되는 것은 쓰레기 Widget이기 때문에 사본을 만듦

(포인터에 의한 전달은 원본 객체가 소멸될 위험이 있으므로 주의)

(소멸의 위험이 없어도 사본이 생성됨)

따라서 객체의 사본을 생성하므로 속도가 느림

 

예외에 값에 의한 전달은 객체의 사본을 만들고 사본을 함수 매개변수에 저장하므로 두 개의 사본이 만들어짐

(함수는 한 번 복사됨)

 

예외 발생을 위해 객체가 복사될 때 복사 생성자는 객체의 정적 타입에 대응하는 클래스에 정의된 것이지, 동적 타입에 의해 정의된 것이 아님(다형성 지원 X)

class Widget {};
class SpecialWidget : public Widget {};

void passAndThrowWidget()
{
    SpecialWidget localSpecialWidget;
    Widget& rw = localSpecialWidget;
    
    throw rw; // 예외의 타입은 Widget
}

 

catch (Widget& w)
{
    throw;
}

catch (Widget& w)
{
    throw w;
}

첫 번째 catch문은 기존 예외를 중계(rethrow)하고, 두 번째 catch문은 기존 예외의 사본을 발생

예외가 중계(rethrow)될 때에는 객체의 복사가 일어나지 않기 때문에 전달받은 타입 그대로 전달됨

 

예외와 catch 문에는 암시적 타입 변환 메커니즘이 적용되지 않음

void f(int value)
{
    try
    {
        throw value;
    }
    catch (double d)
    {
        ...
    }
}

try 블록 안에서 발생된 int 예외는 catch 문에서 처리되지 않는데, 타입 변환이 전혀 일어나지 않기 때문에 이 catch 문은 꼭 double 타입인 것만 잡아냄

따라서 int 예외가 처리되게 하려면 int를 취하는 catch 문을 추가해야 함

그렇지만 상속 기반의 타입 변환과 타입이 있는 포인터로부터 타입이 없는 포인터(void*)로의 타입 변환은 가능

 

catch 문은 작성된 순서에 따라 사용됨

따라서 파생 클래스 타입을 받는 catch 문이 있다고 해도 순서가 제대로 되어 있지 않으면 기본 클래스의 타입 catch 문이 실행될 수 있음

(가상 함수를 찾는 동작 방식은 가장 적합한(best fit) 것을 선택하는 방식이고, 예외 처리 매커니즘은 첫 번째(first fit) 것을 선택하는 방식)

+ Recent posts

목차