Skip to content

Chapter 01, Hello, world of concurrency in Cpp!

Chris Ohk edited this page Nov 16, 2015 · 41 revisions

Chapter 01, Hello, world of concurrency in C++!

1.1 동시성이란 무엇인가?

  • 동시성(Concurrency)이란 2가지 이상의 서로 다른 일들이 동시에 발생하는 것
    • 걸으면서 대화하는 행동
    • 한 손으로 손잡이를 잡으면서 다른 한 손으로 스마트폰을 보는 행동

1.1.1 컴퓨터 시스템에서의 동시성

  • 컴퓨터에서 동시성이란 한 시스템이 서로 다른 작업들을 동시에 수행하는 것을 의미 (연속적으로, 또는 한 작업이 끝나고 다음 작업을 처리하는 것과는 의미가 다름)
  • 예전에 사용하던 컴퓨터는 프로세서가 1개이기 때문에 한 번에 한 가지 작업만 수행 가능하지만 다른 작업으로 전환 가능
  • 작업 전환 (Task Switching) : 다른 작업으로 전환하는데 걸리는 시간이 너무 빨라 동시에 하는 것처럼 보임
  • 하드웨어 동시성 (Hardware Concurrency) : 프로세서가 여러 개이거나 하나의 프로세서 안에 여러 개의 코어가 있는 경우 하나 이상의 작업을 "진짜로" 동시에 수행할 수 있음
  • 문맥 교환 (Context Switch) : 어떤 작업에서 다른 작업으로 전환할 때 수행 (비용이 발생)
  • 동시성 프로그래밍을 할 때 가장 중요하게 고려해야 할 요소는 실제 하드웨어 스레드(Hardware Thread)의 개수!

1.1.2 동시성으로의 접근

다중 프로세서의 동시성

  • 애플리케이션을 여러 부분으로 나눠 스레드가 1개인 여러 개의 프로세스에서 실행
    • 예 : 웹 브라우저와 워드 프로세서를 동시에 실행할 수 있음
  • 각 프로세스는 IPC(Inter-Process Communication) 채널을 통해 메시지를 전달 (신호, 소켓, 파일, 파이프 등)
  • 장점
    • 프로세스 사이에 제공되는 보호 정책과 고수준 통신 메커니즘을 통해 안전한 동시성 코드를 쉽게 만들 수 있음
    • 네트워크를 통해 연결된 서로 다른 머신에서 각 프로세스를 실행할 수 있음
  • 단점
    • 프로세스 간의 통신을 설정하기 복잡하고 속도도 느림
      • 운영체제가 다른 프로세스가 소유하고 있는 데이터를 수정하지 못하도록 프로세스 사이에 수많은 보호 정책을 제공하기 때문
    • 여러 개의 스레드를 실행하면서 발생하는 오버헤드

다중 스레드의 동시성

  • 하나의 프로세스에서 여러 개의 스레드를 실행
    • 각 스레드는 다른 스레드와 독립적으로 실행, 다른 명령어 집합을 실행
  • 프로세스에 있는 모든 스레드는 같은 주소 공간을 공유 → 대부분의 데이터는 모든 스레드에서 직접 접근 가능
    • 하지만 같은 데이터더라도 서로 다른 프로세스라면 메모리 주소는 같지 않음
  • 장점
    • 주소 공간을 공유하고 스레드 사이의 데이터 보호 정책 때문에 여러 스레드를 사용할 때 발생하는 오버헤드는 여러 프로세스를 사용할 때보다 작음
  • 단점
    • 여러 스레드가 특정 데이터에 접근할 때마다, 애플리케이션 프로그래머는 각 스레드에서 데이터가 무결한지 확인해야 함

→ C++에서는 다중 스레드의 동시성을 선호 (다중 스레드에서 발생하는 실행 및 통신 관련 오버헤드는 다중 프로세스에서 발생하는 실행 및 통신 관련 오버헤드보다 낮기 때문, 또한 C++에서는 프로세스 간에 통신을 위한 지원을 제공하지 않음)

1.2 왜 동시성인가?

  • 애플리케이션에서 동시성을 사용하는 두 가지 이유
    • 작업의 분리
    • 성능 향상

1.2.1 작업의 분리

  • 프로그램을 더 쉽게 이해할 수 있음
  • 프로그램을 더 쉽게 테스트 할 수 있음 → 버그 발생할 확률 ↓

1.2.2 성능 향상

"The free lunch is over." - Herb Sutter

  • 예전에 CPU의 코어가 1개이던 시절에는 코어 클럭수가 높아지면 소프트웨어 성능도 따라서 높아졌었음 (공짜 점심)
  • 하지만 코어 클럭수에 한계가 찾아옴 → 코어 클럭수를 높이는 대신 코어의 개수를 늘리기 시작
  • 코어의 개수를 늘려도 프로그램은 1개의 코어에서만 돌아감 → 여러 개의 작업이 동시에 실행할 수 있도록 코드를 설계해야 함 (공짜 점심은 끝났다)
  • 한 작업을 여러 부분으로 나눠 동시에 실행, 총 실행 시간 ↓ → 태스크 병렬(Task Parallelism)
    • 하지만 많은 부분에서 존재하는 종속성으로 인해 작업을 나누는 일은 복잡함
  • 각 스레드가 똑같은 동작을 수행하되 서로 다른 데이터를 처리하는 방법 → 데이터 병렬(Data Parallelism)

1.2.3 동시성을 사용하지 않을 때

  • 동시성을 사용한 코드는 이해하기 힘든 경우가 많음
  • 성능이 생각보다 크게 향상하지 않는 경우가 많음
  • 스레드는 제한된 자원 → 한 번에 너무 많은 스레드를 사용하면 시스템 전체가 느려짐
  • 실행하는 스레드가 많아질수록, 더 많은 문맥 교환이 발생 → 애플리케이션 성능 저하

→ 동시성의 사용은 최적화 전략의 문제! 애플리케이션의 성능을 향상시켜 주지만, 코드가 복잡해지고 가독성이 떨어지며 더 많은 버그를 발생하게 만드는 원인이 됨

→ 애플리케이션에서 성능이 중요한 부분에만 동시성 코드를 사용하는 것이 좋음

1.3 C++의 동시성과 멀티스레딩

  • C++11의 특징 중 하나는 멀티스레딩을 통한 동시성의 표준 지원
    • 플랫폼마다 코드를 따로 작성하지 않아도 됨

1.3.1 C++ 멀티스레딩의 역사

  • C++98 표준을 제정할 당시에는...
    • 스레드의 존재를 알지 못했었음
    • 함수의 동작은 연속적으로 처리하는 머신을 기준으로 작성됨
    • 표준 메모리 모델이 정의되지 않았었음

→ 컴파일러의 확장 기능 없이는 멀티스레딩 애플리케이션을 만들 수 없었음

  • 컴파일러 벤더들은 멀티스레딩을 위한 확장 기능을 추가
    • POSIX C Standard
    • Microsoft Windows API
    • MFC
    • C++ Runtime Library
      • Boost
      • ACE

1.3.2 새 표준에서의 동시성 지원

  • C++11 표준이 등장하면서 모든 것이 바뀜
    • 스레드를 감안한 메모리 모델 도입
    • C++ 표준 라이브러리 확장
      • 스레드 관리
      • 공유 데이터 보호
      • 스레드 사이의 작업 동기화
      • 저수준 atomic 작업

1.3.3 C++ 스레드 라이브러리의 효율성

  • C++ 표준 위원회의 설계 목표
    • 저수준 API를 직접 사용했을 때와 동일한 기능을 제공하는 것
    • 궁극적인 성능을 이끌어 낼 수 있도록 저수준 기능을 충분히 제공하는 것
  • C++ 표준 라이브러리는 멀티스레딩을 지원하는 코드를 쉽게 작성하고 오류가 덜 발생하도록 고수준 추상화 및 기능을 제공
    • 고수준 추상화 및 기능을 사용하더라도 추가 비용을 발생하지 않게 하는 것이 C++ 설계의 원칙

1.3.4 플랫폼별 기능

  • C++ 스레드 라이브러리를 통해 멀티스레딩과 동시성과 관련된 많은 기능들을 제공해주지만, 때로는 특정 플랫폼에만 있는 기능을 사용하고 싶을 때가 있음
  • C++ 스레드 라이브러리를 사용했을 때의 장점을 그대로 유지하면서 이런 기능을 사용하고 싶다면, native_handle() 함수를 사용하면 됨

1.4 시작하기

  • C++11을 지원하는 컴파일러 필요

1.4.1 Hello, Concurrent World

    #include <iostream>
    #include <thread>

    void Hello()
    {
        std::cout << "Hello, Concurrent World!\n";
    }

    int main()
    {
        std::thread t(Hello);
        t.join();
    }
  • #include <thread> : 멀티스레딩 관련 클래스 및 함수를 사용하기 위해 추가해야 하는 헤더 파일
  • void Hello() { ... } : 스레드가 실행할 함수 (모든 스레드는 초기 함수(Initial Function)을 가짐)
  • std::thread t(Hello); : 새로운 스레드 선언, 인자로 초기 함수 Hello()를 전달, 선언 뒤 스레드 실행
  • t.join(); : 프로그램이 종료된 후에 Hello() 함수가 수행될 가능성이 존재, 따라서 join()을 통해 스레드의 실행이 끝날 때까지 main() 함수가 대기하도록 만듬

1.5 정리

  • 동시성과 멀티스레딩의 정의, 애플리케이션에서 동시성을 사용하는 / 사용하지 않는 이유를 설명함
  • C++98에서는 동시성과 관련된 기능을 제공하지 않아 플랫폼마다 다양한 확장 라이브러리가 존재했었음
  • C++11로 오면서 동시성과 관련된 기능을 표준으로 제공함
    • 플랫폼에 상관 없이 동일한 코드로 동시성을 지원, 최신 CPU 지원
Clone this wiki locally