본문 바로가기

CS/운영체제

[운영체제] 스레드 & Concurrency

스레드

 프로세스 하위에서 실행되는 실행 단위로, 동일 프로세스에 대한 스레드들은 전역 변수의 형태로 정보를 공유할 수 있다. 각각의 스레드마다 PC, stack 및 레지스터와 같은 실행 환경 정보를 가지고 있으며, 프로그램 코드, 데이터 등 공유할 수 있는 정보의 경우 동일 프로세스 아래에서 공유되어 사용된다는 특징이 있다.

스레드에 대한 배경

 현대의 프로그램들은 상당히 복잡하며, 다양한 기능을 동시에 제공한다. 예를 들어, 메모장이나 비디오 플레이어 같이 GUI을 이용하는 현대 프로그램들은 화면에 대한 출력 기능 이외에 데이터 처리, 입력 대기, 연산 등 다양한 기능을 추가적으로 가진다. 

 위와 같은 경우 각각의 기능을 프로세스 단위로 나누어 하나의 프로그램을 구성하는, 멀티 프로세스 프로그램을 고려할 수 있겠지만, 프로세스 사이에서 정보를 공유하기 위해서는 메시지 패싱 혹은 공유 메모리와 같은 IPC 기법이 필요하기 때문에 성능적으로 떨어진다는 문제가 있다.

 어차피 하나의 프로그램을 위해 여러가지 기능을 나누는 것이라면, 차라리 하나의 프로세스 아래에 여러개의 실행 흐름을 둬서, IPC를 통해 정보를 간접적으로 전달하는 대신 전역변수를 통해 직접 공유하도록 만드는 것은 어떨까? 스레드는이렇듯 편의성 및 효율성을 늘리기 위한 목적으로 고안되었다.

장점

  • Responsiveness
    : 프로세스의 일부분이 멈추더라도, 다른 스레드 영역은 계속 동작하므로, 반응성이 좋다.
  • Resource Sharing
    : shared memory / message passing 방식에 비해 성능 및 구현 난이도가 훨씬 쉽다.
  • Economy
    : 프로세스를 새로 생성하는 것 보다 스레드를 여러개 두는 방식이 더 비용이 적으며, context switching 상황에서도 스레드 사이에서의 경우가 오버헤드가 더 적다 ( 일부 정보는 공유한다는 특징 고려 ).
  • Scalability
    : 하나의 프로세스에 대한 각각의 스레드를 코어마다 돌릴 수 있으므로, 싱글 스레드 프로세스보다 멀티 코어의 장점을 누리기 좋다.

멀티코어 프로그래밍

멀티코어 아키텍처의 효율을 높이기 위해서는 다음과 같은 사항을 고려한다.

  • Dividing Activities : 작업을 어떻게 나눌 것인가?
  • Balance : 구분된 모든 작업은 동일한 수준이어야 Parallel로 굴릴 때 효율이 좋다.
  • Data Splitting : 데이터를 기준으로 스레드를 구분한다면, 어떤 기준으로 데이터를 쪼개야 하는가?
  • Data Dependency : 데이터 간 의존 관계가 있는 경우 synchronous 하게 ... 이들을 구분해야 한다.
  • Testing & Debugging : 다수의 조건을 함께 고려해야 하므로, 테스팅 및 디버깅이 어렵다.

Concurrency & Parallelism

  • Concurrency
    : 하나의 프로세스 혹은 코어를 이용하여 여러개의 작업을 작은 시간 단위로 쪼개 반복하여 실행하는 방식.
    프로세스들이 마치 동시에 실행되는 것 처럼 보이는 효과가 생기지만, 각각의 시간에는 하나의 작업만 한다.
  • Parallelism
    : 여러개의 작업을 여러개의 프로세서에서 동시에 수행하는 방식. 실제로 여러개의 작업이 동시에 수행된다. 
    • Data Parallelism
      : 전체 데이터를 일정한 크기로 나누어 각각의 스레드에서 연산을 수행하는 방식.
    • Task Parallelism
      : 전체 과정을 작업 단위로 쪼개서 ( GUI , I/O ... ) 각각의 스레드가 분담하게 하는 방식.

Concurrency 와 Parellelism 의 차이
Data / Task Parallelism의 차이

Amdahl's Law 

 병렬 방식으로 프로그래밍하면 성능이 좋아지기는 하지만, 무작정 좋아지는 것은 아니다. 이는 amdahl의 법칙을 통해 대략적으로 살펴볼 수 있다. 아래 수식을 살펴보자.

 암달의 법칙에 따르면, 컴퓨터의 속도는 전체 작업 중 병렬의 비율 및 코어의 수에 의해 결정된다. 예를 들어, 위 수식에서 전체 작업 중 병렬의 비율이 0.75 = (1 - S) 이고 코어의 수가 2개라면, 속도는 1.6 배가 된다.

 만약 N이 무한대로 가면, 속도의 최대치는 1/S 에 수렴한다. 이때, 기본적으로 모든 작업을 병렬 기법을 통해 처리할 수 있는 것은 아니라 반드시 S는 일정 비율이 있기 때문에, 속도가 무작정 증가할 수는 없다.


User Thread & Kernel Thread

  • User Thread
    : 유저 수준의 Thread 라이브러리를 통해 사용할 수 있는 스레드.
    POSIX Pthread, Window thread 등 ...
  • Kernel Thread
    : 시스템 콜 등 커널의 기능을 지원하기 위한 목적의 스레드. OS에서 수행하는 전반적인 기능과 관계가 있으며, 커널에 종속되므로, OS마다 상이할 수 있다.

Multithreading Model

멀티 스레딩 모델은 크게 3가지가 존재하고, 이들을 결합한 모델을 주로 사용한다.

  • many-to-one
  • one-to-one
  • many-to-many
  • Two-Level Model

Many-To-One 방식

 초기 운영체제에서 도입했던 방식으로, 다수의 유저 레벨 스레드가 하나의 커널 스레드에 매핑된다. 다수의 스레드가 공유한다는 특성상, 한번에 하나의 스레드만 커널 스레드를 사용할 수 있기 때문에 일종의 병목현상이 나타날 수 있다.

 멀티 스레드 프로세스 형식을 채택하더라도 스레드들이 특정 커널 스레드를 함께 사용해야 한다면,  병렬로 구성하더라도 동시에 여러 작업을 수행할 수 없게 되어 병렬 구성의 의미가 없을 수 있다.

 병렬 구성의 효율성이 떨어지는 구조이므로, 현재는 굳이 사용하지 않는 방식이다.

One-To-One 방식

 각각의 유저 레벨 스레드에 대응되는 커널 스레드가 할당되는 방식으로, 유저 스레드를 생성하면 자동적으로 커널 스레드가 쌍으로 생성된다.

  Many-To-One 방식과는 달리 병목 현상이 발생하지 않으므로 Concurrency/Parallelism 구성에 유리하지만, 스레드가 많아짐에 따라 커널 스레드 역시 증가하므로 자원 소비 및 오버헤드가 크게 발생할 수 있다는 단점이 있다.

Many-To-Many 방식

 다수의 유저 레벨 스레드와 커널 스레드를 매핑하는 방식으로, 커널 영역에 충분한 크기의 스레드 풀을 만들어 두고, 유저 스레드는 해당 스레드 풀에 커널 스레드를 요청하는 방식으로 구성된다.

 미리 구성된 커널 스레드 풀이 존재하므로 1 : 1로 새로 만드는 Many-To-One 보다는 오버헤드가 적으면서도 Many-To-One 방식보다는 병목 현상이 일어날 가능성이 적다는 장점이 있다. 다만 여전히 스레드 타이밍 제어에 실패하는 등의 이유로 커널 스레드를 전부 사용하는 상황에서는 다른 스레드들의 사용이 blocking 된다.

Two Level Model

 수시로 커널 스레드 제어가 필요한 유저 스레드에게는 One-To-One 모델을 적용하고, 다른 스레드들에 대해서는 Many-To-Many 모델을 적용하는 방식. 스레드 사용 효율이 좋아진다.


Pthreads

 POSIX 표준에 따른 스레드로, 유저 수준의 스레드 및 커널 수준의 스레드 둘 다 포함한다. 실제 구현이 아니라 "명세(Specification) 이므로, 운영체제마다 내부적으로 다른 방식으로 처리될 수 있다.

 Linux , Mac OS 에서 사용된다.

 

참고로, 리눅스 환경에서의 스레드는 프로세스와 크게 구분되지 않는 편이며 task 개념에 가깝다고 한다. 스레드의 생성은 clone( ) 시스템 콜을 통해 수행할 수 있으며, 필드 지정을 통해 어느 수준까지 정보를 공유할지 지정할 수 있다.


Implicit Threading

스레드를 명시적으로 사용하는 것도 좋은 방법이나, 때로는 개개 스레드를 만들어 사용하기에는 너무 복잡해지는 상황이 있다. 특히 스레드를 많이 사용하면 사용할 수록 자원 관리나 스레드 할당 및 해제 과정을 전부 수행해주기 어려워질 수 있는데, 이러한 어려움을 덜어주기 위한 목적으로 컴파일러 혹은 런타임 라이브러리를 통해 암시적으로 스레드를 사용할 수 있는 방법이 바로 Implicit Threading 이다.

  • Thread Pools
  • Fork-Join
  • 컴파일러 기반

Thread Pools

 일정 수준의 스레드를 pool 내에 생성해두고, API를 통해 이 스레드들을 가져다 사용하는 방식. 스레드를 직접 만들고 해제하는 방식보다 일반적으로 속도가 빠르며, 작업을 처리할 때 스레드의 생성 및 할당 과정을 고려하지 않는 대신 전체 로직에 집중할 수 있다는 장점이 존재한다.

 다양한 프로그래밍 언어 (Java, C# 등) 에서 스레드 풀을 구현하고 있으며, 해당 API를 가져다 쓰면 된다.

Fork-Join Parallelism

Fork 명령을 통해 하나의 스레드를 분할하여 작업을 처리한 후, join 명령을 통해 다시 합치는 방식.

컴파일러 기반 : OpenMP

 C, C++, portran 언어를 위한 컴파일러 지시어 셋으로, 병렬로 돌리고 싶은 구역에 #pragma omp parallel 을 지정하여 사용할 수 있다고 한다. 

컴파일러 기반 : Grand Central Dispatch

Mac OS, IOS 환경에서의 컴파일러 기반 병렬 처리 시스템. ^{ } 을 이용하여 병렬 구간을 지시한다고 한다.


스레딩과 관련된 이슈들

fork( ) 와 exec( ) 의 의미

 프로세스에 여러개의 스레드가 존재할 때, 하나의 프로세스를 fork 하면 대응되는 스레드들도 모두 복사하는게 맞을까? 아니면 현재 흐름에 해당하는 스레드 하나만 복사하는게 맞을까?
 현재 프로세스의 상태를 이용해야 하는 경우에는 해당 스레드들을 복사하는 것이 의미가 있지만, exec를 실행하기 위해 fork을 하는 경우에는 해당 스레드들을 복사하는 행위 자체가 오버헤드로 작용할 수 있다.

Signal Handling

 특정 시스템에서는 어떤 이벤트가 발생했을 때 시그널을 생성하고, 프로세스에게 보내서 해당 사실을 알린다. 이때 시그널은 대략 다음과 같은 조건을 만족해야 한다

  • 특정 이벤트에 의해 발생한다.
  • 프로세스에게 전달된다.
  • 전달된 시그널은 해당 프로세스에 의해 handle 되어야 한다.

멀티스레드 기반 환경에서 시그널의 전달은 다음과 같이 수행된다,

  • 자신이 받은 것은 자신이 알아서 처리하고, 나머지 스레드들은 무관하게 동작한다.
  • 모든 스레드들에게 해당 내용을 알린다. 
  • 특정 스레드에게만 해당 내용을 알린다.
  • 프로세스에게 전달되는 시그널을 특정 스레드만 수신할 수 있도록 설정해둔다.

Signal Handler 은 프로세스가 받는 시그널을 처리할 때 사용되며, 유저가 오버라이딩을 통해 구성할 수도 있다.

Thread Cancellation

 스레드가 동작하다가 외부의 개입 등에 의해 중지되는 경우가 있다 (target thread). 이때 스레드가 취할 수 있는 행동은 크게 2가지로 분류된다.

  • Asynchronous Cancellation
    : 인터럽트가 들어오는 순간 바로 중지하는 방식. clean up 과정이 없기 때문에 실행 정보를 잃을 수도 있다.
  • Deferred Cancellation
    : 수행중이었던 작업을 끝마친 후 중지하는 방식. 할일은 하고 끝나므로 큰 문제가 발생하지는 않는다.

pthread의 경우 기본 설정은 defer 이며, 시그널에 의해 관리된다.

Thread Local Storage

스레드들이 가지고 있는 데이터들을 관리하기 위한 공간

Scheduler Activations

 Lightweight process(LWP)은 유저 스레드와 커널 스레드 사이에서 두 스레드들을 연결해주는, 일종의 스케줄러 역할을 하는 자료구조이다. 커널은 응용 프로그램에 LWP(가상 프로세서) 들을 제공하고, 응용 프로그램들은 사용가능한 LWP에 유저 스레드를 예약할 수 있다.

 유저 스레드의 경우 LWP의 도움을 받아야만 커널 스레드를 예약할 수 있으므로, LWP는 커널의 상태 및 커널 스레드 정보를 아래로부터 전달받아야 한다 (어떤 스레드가 사용 가능하다든지 ) . 커널로부터 이러한 정보를 올려받는 것을 upcall이라고 하며, upcall handler 을 통해 전달받을 수 있다. LWP는 내부적으로 upcall handler을 계속 실행하여 커널의 정보를 지속적으로 얻어야 한다.

 

'CS > 운영체제' 카테고리의 다른 글

[운영체제] 동기화 문제 및 방법  (0) 2022.04.22
[운영체제] CPU 스케줄링  (0) 2022.04.11
[운영체제] 프로세스  (0) 2022.04.03
[운영체제] 운영체제  (0) 2022.03.17
[운영체제] OS 서비스/프로그램  (0) 2022.03.15