동시성

일부에서는 병행성이라 부르기도 한다. 병렬성과의 명확한 구분을 위해 동시성이라 부르자.

병렬성과의 차이

동시성은 여러 일을 한꺼번에 다루는 것이다. 병렬성은 여러 일을 한꺼번에 해내는 것이다.

  • 롭 파이크1

동시성(concurrency)은 논리적 개념이다. 동시적인 프로그램은 병렬적으로 돌 수도 있고, 아닐 수도 있다. 액터, 스레드 등 동시성 모델을 이용하여 실현할 수 있다.

병렬성(parallelism)은 물리적 개념이다. 여러 병렬적 방법으로 동시에 계산을 해낸다. 다중코어, 분산 시스템 등을 이용하여 실현할 수 있다.

동시성이 필요한 이유

  • 실세계가 동시적이기 때문에
    • 프로그램은 좋든 싫든, 의도했든 안했든 실세계와 상호작용한다. 그 실세계가 동시적이므로 동시성이 필요하다.
    • 동시성이 없다면 파일을 다운 받으며 마우스를 움직일 수 없다.
  • 더 나은 성능을 위해
    • 한 번에 하나의 요청만 처리할 수 있는 서버는 성능이 좋지 못하다.
    • 현대 프로그램에서는 I/O에서 버리는 시간이 많다.
    • 결과를 기다리는 동안 다른 일을 할 수 있다면 좋을 것이다.
    • 성능 향상을 보장하지는 않는다. 동시성을 실현하기 위한 비용이 더 클 수도 있다.

동시성을 다루는 방법

오늘날의 운영체제는 프로그램을 프로세스 단위로 실행하며 멀티태스킹한다.

운영체제는 선점형 멀티태스킹으로 프로세스와 스레드를 스케줄한다. 선점형 멀티태스킹에서는 제어권이 바뀌는 시점을 어플리케이션이 제어할 수 없다. 이로 인하여 공유 메모리에 서로 다른 제어 주체가 의도치 않게 접근하여 예기치 못한 오류가 발생할 수 있다. 이를 경쟁 위험이라고 하며 경쟁 위험을 막기 위한 여러 동시성 모델(concurrency model)이 있다.

"코딩을 지탱하는 기술"에서는 경쟁 위험을 막기 위한 방법을 크게 세 가지로 구분한다:

  1. 공유하지 않기
  2. 변경하지 않기
  3. 끼어들지 않기

공유하지 않기

공유하지 않으면 문제될 것이 없다.

  • 운영체제는 프로세스를 동시적으로 실행한다. 각 프로세스는 서로 메모리를 공유하지 않는다.
  • 액터 모델은 프로세스와 비슷하다. 한 프로세스 내에서 실행되는 여러 액터들은 서로 메모리를 공유하지 않는다. 필요한 경우 메시지를 교환한다.
  • CSP도 비슷하다. 메모리를 공유하지 않고 채널을 만들어 메시지로 보낸다.

변경하지 않기

변경하지 않으면 공유해도 문제가 없다.

  • 여러 함수형 언어에서 할당한 값은 기본적으로 불변이다. 값이 바뀌지 않으므로 공유해도 문제가 없다. 하스켈, 엘릭서 등의 언어가 이에 해당한다.
  • 모든 값을 불변으로 다루는 것이 어렵다면, 적어도 불변을 장려한다. 러스트, 스칼라, 스위프트 등의 언어는 그 전에 만들어진 언어들보다 불변 선언이 자연스럽도록 설계되었다.

끼어들지 않기

공유하고 변경하더라도 타이밍만 잘 맞다면 문제가 없다.

  • 운영체제가 아니라 애플리케이션 수준에서 직접 작업을 스케쥴링한다. 그린 스레드, 파이버, 코루틴 등이 이에 해당한다.
  • 작업 중인 메모리에 작업 중이라고 표시하여 다른 작업이 끼어들지 않도록 한다. , 뮤텍스, 세마포어, 싱크로나이즈, STM 등이 이에 해당한다.

프로세스

액터

프로그램을 액터(actor)와 액터 사이의 의사소통으로 본다. 각 액터는 동시에 돌아가며 정보 공유는 메시지 전달을 통해 이루어진다.

채널

CSP는 순차 프로세스 통신(communicating sequential process)의 약어이다. 스레드 사이에서 메시지를 통해 값을 공유한다는 점에서 액터와 유사하다. 액터가 메시지를 주고 받는 주체인 액터에 초점을 맞춘 것에 비해, CSP는 메시지를 주고 받는 채널에 초점을 맞추었다.

고 언어에서 언어 수준에서 지원하며 많은 주목을 받았다.

Do not communicate by sharing memory; instead, share memory by communicating.

공유 메모리로 communication하지말자. communication하여 메모리를 공유하자.

-- Effective Go

함수형 프로그래밍

함수형 프로그래밍(FP; functional programming)에서는 불변 변수를 최대한 활용하고, 부수 작용을 통제하는 프로그래밍 방법론이다.

그린 스레드

스레드와 비슷하게 쓸 수 있다. 프로그래밍 모델을 바꿀 필요가 없다. 스레드보다 오버헤드가 적어 많은 양의 그린 스레드를 만들 수 있다.

스레드

  • 스레드는 이미 널리 쓰이는 개념이며 운영체제에서 지원한다.
  • 기존의 동기 코드나 라이브러리를 쉽게 이용할 수 있다.
  • 하지만 각 스레드를 서로 동기화 하는 것은 어렵고, 스레드를 생성하고 스위칭하는 비용도 비싸다.

스레드 풀을 만들어 비용을 줄일 수도 있다.

  • 커널 스레드: 운영체제 커널이 스케줄링한다.
  • 유저 스레드: 프로세스가 직접 스레드를 스케줄링한다. 커널이 볼 때에는 일반적인 프로세스로 보인다.

교착 상태(deadlock)의 위험이 있다.

교착 상태

  1. 상호 배제(mutual exclusion)
  2. 점유 상태로 대기 (Hold and wait)
  3. 선점 불가 (No preemption)
  4. 순환성 대기 (Circular wait)

STM

Software transaction memory clojure.

비동기 프로그래밍

C#, rust, JS 등.

비동기 프로그래밍은 오늘날 점점 더 많은 언어에서 지원하는 동시성 프로그래밍 모델이다. async/await 문법을 이용하여 코드가 동기적으로 보이도록 한다.

적은 CPU, 메모리 오버헤드를 소모한다. 특히 서버나 DB 같은 많은 양의 IO 작업에 유리하다. 적은 수의 스레드를 런타임으로 활용하여 많고 비용이 적은 비동기 작업을 처리할 수 있다.

이벤트 기반 설계

이벤트 기반 설계(event driven architecture) 혹은 이벤트 루프는 성능이 우수하며 스레드 수가 적어도 데이터 흐름과 오류 전파를 따라가기 힘들다.

자바스크립트에서 주로 사용한다.

observerable

RxSwift, RxJs 등

용어

공유 자원

시스템 안에서 여러 스레드나 프로세스가 함께 접근할 수 있는 자원을 공유 자원(shared resource)이라고 한다.

임계 영역

공유 자원에 접근하는 코드들을 임계 영역(critical section)이라고 한다.

상호 배제 등의 기법으로 임계 영역에 하나의 쓰레드만이 진입하도록 보장해야한다.

임계 영역이 제대로 설정되지 않으면 아래와 같은 문제가 발생할 수 있다:

  • 입출금 문제

경쟁 상태

여러 스레드가 동시에 임계 영역에 접근하여 발생하는 문제를 경쟁 상태(race condition) 혹은 경쟁 위험(race hazard)라고 한다.

상호 배제

임계 영역에 한 개의 프로세스만 진입할 수 있도록, 즉 원자적으로 실행할 수 있도록 하는 기법을 상호 배제(mutual exclution)라고 한다.

임계 영역을 잠궈서 원자적으로 다룰 수 있다. 이를 락(lock)이라고 부르며 락을 구현한 자료구조나 스마트 포인터를 흔히 mutex라고 부른다.

// 상호 배제가 가능한 변수를 선언한다.
let m = Mutex::new(5);
{
    // 변수에 접근하기 전에 먼저 임계 영역을 잠근다.
	let mut num = m.lock().unwrap();
	
	// 변수에 마음대로 접근할 수 있다.
	*num = 6;

    // 러스트에서는 블록이 끝나면 자동으로 잠금을 푼다.
}

참고

Footnotes

  1. Rob Pike, Concurrency is not Parallelism, 2013.