[프로그래밍] 프로세스의 스레드, 동기/비동기 호출, 그리고 스레드 블로킹/논블로킹

스레드

프로세스(프로그램 또는 애플리케이션) 내에서 스레드(thread)는 작업의 요청 흐름을 의미한다. 어떤 작업을 위한 순차적인 코드 실행이 바로 스레드라고 볼 수 있다. 코드가 실행되는 것을 스레드가 작업을 수행한다고도 표현한다. 스레드가 어떤 작업을 순차적으로 수행할지는 코드 구현 방법에 달려있다.

서버에 요청이 들어오면 스레드(작업 스레드)가 해당 요청에 대한 작업을 수행한다. 운영체제는 프로세스에 할당된 CPU 자원을 사용하여 스레드를 실행(코드를 실행)한다. 물리적으로 CPU 코어 당 하나의 스레드를 실행할 수 있으므로 싱글 코어 머신에서 여러 스레드를 물리적으로 동시에 실행하는 것은 불가능하지만 시분할 기법(스레드 단위의 컨텍스트 스위칭)을 통해 하나의 스레드가 CPU 자원을 점유하지 않도록 함으로써 동시성 구현이 가능하다. 멀티 코어 CPU인 경우 물리적 동시 처리(병렬성)가 가능하며 하이퍼스레딩 기술에 의한 CPU의 멀티스레딩이 가능한 경우 경우 CPU 스레드(프로세스의 스레드와는 상이한 개념)가 물리적 동시 처리를 보다 효율적으로 처리한다.

작업 처리 성능 향상을 위해 동일한 요청에 대한 작업을 여러 스레드가 처리할 경우(프로세스의 멀티스레딩) 객체 잠금 및 동기화 시 컨텍스트 스위칭으로 인한 오버헤드가 발생할 수 있으므로 리소스 비용 측면에서 트레이드오프 고려가 필요하다. 스레드 리소스 사용을 효율적으로 하기 위해 네트워크 또는 파일 입출력과 같이 IO 작업을 위한 스레드를 별도로 두어 일반적인 CPU 연산 작업과 IO 작업을 서로 다른 스레드가 처리할 수 있다. 서버가 스레드풀에 존재하는, 제한된 수의 스레드를 얼마나 그리고 어떻게 사용하여 클라이언트의 요청들을 처리할지에 따라 제한된 하드웨어 리소스 사용의 확장성과 효율성이 달라지게 된다.

스레드는 값비싼 자원이므로 제한된 리소스를 좀더 효율적으로 사용하고자 하는 요구사항으로 인해 리액티브 프로그래밍(reactive programming) 패러다임이 등장하게 되었다. 리액티브 프로그래밍은 비동기(asynchronous), 논블로킹(non-blocking) 방식으로 프로그래밍을 구현하여 시스템의 한정된 리소스 내에서 스레드 사용 효율을 극대화시킨다. 하지만 제한된 스레드로 많은 요청을 처리하기 위한 과정에서 여러가지 오버헤드가 발생할 수 있기 때문에 리액티브 프로그래밍이 항상 빠른 것은 아니며 작업의 종류와 처리량에 따라 애플리케이션의 적절한 스레드 사용 전략을 세워야 한다.


동기/비동기 호출과 스레드 블로킹/논블로킹

동기 및 비동기 요청은 요청자의 관점에서 요청한 작업의 수행 결과에 대한 응답을 그다음 작업 수행을 위해 받아야 하는지, 그렇지 않은지에 따라 구분할 수 있다.

  • 요청을 동기적으로 처리한다는 것은 작업의 요청자가 작업 수행 결과에 대한 응답을 기다리는 것을 의미한다.
    • 요청에 대한 응답의 결과(정상 또는 실패)를 받은 후 그 다음 작업을 수행하기를 기대한다.
    • 선 작업의 결과가 후 작업에 영향을 미치는 경우(후 작업이 선 작업에 의존적인 경우) 요청은 동기적으로 처리되어야 한다.
  • 반면 요청을 비동기적으로 처리한다는 것은 작업의 요청자가 작업 수행 결과에 대한 응답을 기다리지 않고 또다른 작업을 수행하는 것을 의미한다.
    • 요청한 작업의 수행 결과는 작업이 완료되었을 때 언제든 받기를 기대한다.
    • 선 작업의 결과가 후 작업에 영향을 미치지 않는 경우(후 작업이 선 작업에 의존적이지 않은 경우) 요청은 비동기적으로 처리될 수 있다.


작업 요청을 위한 함수(메서드) 호출은 그 특성에 따라 크게 두 가지로 구분할 수 있다.

  1. 동기 호출 (synchronous call): 일련의 두 작업이 순차적으로 수행되어야 할 때, 요청자(호출자)는 첫 번째 작업을 요청한 후 해당 작업이 완료되었을 때 그 다음 두 번째 작업을 수행한다.
    • 요청자는 요청한 첫 번째 작업이 완료될 때까지 두 번째 작업을 수행하지 못한 상태로 기다리거나(블로킹) 다른 작업(일련의 두 작업과 관련이 없는 다른 작업)을 수행하면서 기다릴 수 있다(논블로킹).
    • 동기 호출 시 작업 요청 스레드가 논블로킹 되더라도 일련의 두 작업들이 동기적으로 수행된다는 사실은 변함이 없다.
  2. 비동기 호출 (asynchronous call): 요청자는 첫 번째 작업을 요청한 후 해당 작업의 완료 여부에 관계 없이 그 다음 두 번째 작업을 수행한다.
    • 요청자는 요청한 첫 번째 작업이 완료될 때까지 두 번째 작업을 수행하지 못한 상태로 기다리거나(블로킹) 요청한 첫 번째 작업이 수행되는 도중에도 두 번째 작업이나 다른 작업(일련의 두 작업과 관련이 없는 다른 작업)을 수행할 수 있다(논블로킹).
    • 비동기 호출 시 작업 요청 스레드가 블로킹 되더라도 일련의 두 작업들이 비동기적으로 수행된다는 사실은 변함이 없다.
    • 요청한 첫 번째 작업이 완료될 때까지 두 번째 작업을 수행하지 못하는 상태로 기다린다면(블로킹) 제한된 스레드 환경에서 시스템의 성능 저하가 발생할 수 있다. 이는 리소스를 비효율적으로 활용하는 것이며 이를 개선하기 위해서는 요청 스레드를 논블로킹하는 방식의 프로그래밍 구현이 필요하다.


작업의 요청 흐름(스레드)은 크게 두 가지로 구분할 수 있다.

  1. 블로킹 (차단, blocking): A 작업의 흐름이 다른 B 작업 흐름에 의해 차단되면 A 작업을 수행하는 A 스레드는 블로킹되어 동시에 다른 작업을 수행할 수 없다.
    • 요청을 보낸 스레드가 요청을 받은 스레드의 작업 수행이 완료될 때까지 블로킹된다면 다른 작업을 수행할 수 없게 된다.
    • 동기적 작업 요청은 응답이 올 때까지 스레드를 블로킹할 수도 있고, 블로킹하지 않을 수도 있다.
    • 논블로킹 방식의 동기적 요청은 제한된 스레드 환경에서 스레드 자원을 낭비하지 않고 더 효율적으로 사용할 수 있게 만든다.
    • 블로킹 방식의 비동기적 요청은 제한된 스레드 환경에서 스레드 자원을 낭비하고 시스템의 심각한 성능 저하를 유발할 수 있으므로 리액티브 애플리케이션은 이러한 사항을 염두에 두고 설계되어야 한다.
  2. 논블로킹 (비차단, non-blocking): A 작업의 흐름이 다른 B 작업 흐름에 의해 차단되지 않으면 A 작업을 수행하는 A 스레드는 블로킹되지 않으며 동시에 다른 작업을 수행할 수 있다.
    • 요청을 보낸 스레드가 요청을 받은 스레드의 작업 수행이 완료될 때까지 블로킹되지 않는다면 다른 작업을 수행할 수 있다.
    • 비동기적 작업 요청은 응답에 관계 없이 스레드를 블로킹하지 않을 수도 있고, 블로킹할 수도 있다.
    • 프로그래밍 언어에서 제공하는 비동기 프로그래밍 관련 기능은 스레드를 차단하지 않는 비동기 호출을 통해 논블로킹 방식의 구현 방법을 제공한다.


작업의 동기/비동기 및 블로킹/논블로킹 방식 처리 관계를 정리해 보면 다음과 같다.

  • 요청자가 작업 요청 후 그다음 작업을 위한 응답을 기다리는 동안(동기) 요청 스레드가 차단되어 다른 작업을 동시에 수행할 수 없음(블로킹): 동기 + 블로킹
  • 요청자가 작업 요청 후 그다음 작업을 위한 응답을 기다리는 동안(동기) 요청 스레드가 차단되지 않아 다른 작업을 동시에 수행할 수 있음(논블로킹): 동기 + 논블로킹
  • 요청자가 작업 요청 후 그다음 작업을 위한 응답을 기다리지 않으며(비동기) 요청 스레드가 차단되어 다른 작업을 동시에 수행할 수 없음(블로킹): 비동기 + 블로킹
  • 요청자가 작업 요청 후 그다음 작업을 위한 응답을 기다리지 않으며(비동기) 요청 스레드가 차단되지 않아 다른 작업을 동시에 수행할 수 있음(논블로킹): 비동기 + 논블로킹
  • 작업 수행 결과에 대한 응답을 받을 때까지 기다려야 하며 다른 작업을 수행하지 못한 상태로 대기: 동기 + 블로킹
  • 작업 수행 결과에 대한 응답을 받을 때까지 기다려야 하지만 그 동안 다른 작업을 수행하며 대기: 동기 + 논블로킹
  • 작업 수행 결과에 대한 응답을 받을 때까지 기다릴 필요가 없으며 바로 다른 작업을 수행: 비동기 + 논블로킹
  • 작업 수행 결과에 대한 응답을 받을 때까지 기다릴 필요가 없지만 바로 다른 작업을 수행하지 못한 상태로 대기: 비동기 + 블로킹


사례에 따른 비교

  1. 동기 + 블로킹
    • 프로그램 실행을 담당하는 메인 스레드는 다양한 작업을 수행하기 위해 별도의 작업 스레드에게 작업을 요청한다.
    • 선행 작업이 블로킹 IO 작업이고 후행 작업은 IO 처리 결과를 사용하는 것이라면 두 작업은 동기적으로 수행된다.
    • 선행 작업이 블로킹이므로 선행 작업이 완료될 때까지 메인 스레드가 차단된다. 메인 스레드가 차단되어 프로그램 실행이 중단된다. 선행 작업이 완료되어야 프로그램 실행이 재개되며 후행 작업이 진행된다.
  2. 동기 + 논블로킹
    • 선행 작업이 논블로킹 IO 작업이고 후행 작업은 IO 처리 결과를 사용하는 것이라면 두 작업은 동기적으로 수행된다.
    • 선행 작업이 논블로킹이므로 선행 작업이 완료될 때까지 메인 스레드가 차단되지 않는다. 메인 스레드가 차단되지 않으므로 프로그램 실행은 중단되지 않는다. 선행 작업이 완료되어야 후행 작업이 가능하다.
  3. 비동기 + 블로킹
    • 선행 작업이 블로킹 IO 작업이고 후행 작업은 IO 처리 작업과 관계 없는 독립적인 작업이면 두 작업은 비동기적으로 수행된다.
    • 선행 작업이 블로킹이므로 선행 작업이 완료될 때까지 메인 스레드가 차단된다. 메인 스레드가 차단되어 프로그램 실행이 중단된다. 선행 작업이 완료되지 않아도 후행 작업이 가능하지만 메인 스레드가 차단된 상황이다. 즉, 프로그램 실행이 중단된 상태이므로 후행 작업을 진행할 수 없다.
  4. 비동기 + 논블로킹
    • 선행 작업이 논블로킹 IO 작업이고 후행 작업은 IO 처리 작업과 관계 없는 독립적인 작업이면 두 작업은 비동기적으로 수행된다.
    • 선행 작업이 논블로킹이므로 선행 작업이 완료될 때까지 메인 스레드가 차단되지 않는다. 메인 스레드가 차단되지 않으므로 프로그램 실행은 중단되지 않는다. 선행 작업이 완료되지 않아도 후행 작업이 가능하며 메인 스레드가 차단되지 않은 상황이다. 즉, 프로그램 실행이 중단되지 않은 상태이므로 후행 작업을 진행할 수 있다.


비동기 호출과 비동기 콜백

비동기 호출은 요청자가 요청한 작업이 완료되지 않아도 다른 작업을 수행할 수 있다. 비동기 호출을 요청 받은 작업자는 요청자에게 작업 수행 결과에 대한 응답이 아닌 별도의 응답을 즉시 반환할 수 있다. 요청을 받은 작업자가 요청한 작업을 수행할 수 없는 상태임을 응답으로 알려주거나 요청한 작업 실행을 수락하였음을 응답으로 알려주는 등의 시나리오가 존재한다. 하지만 즉각적인 응답 외에도 요청자는 요청 작업의 수행 결과를 받기를 기대할 수 있으므로 요청한 작업이 완료되었을 때 그 결과를 받아 처리할 수 있는 방법이 필요한다.

프로그래밍에서 콜백(callback) 또는 콜백 함수(callback function)란 다른 코드(함수)의 인자로 넘겨주는 실행 가능한 코드를 말하며 다양한 목적을 위해 사용될 수 있다. 비동기 호출에 의해 수행되는 작업이 완료되었을 때 특정 코드를 실행하기 위해 사용되는 콜백을 비동기 콜백(asynchronous callback)이라고 한다.

비동기적으로 호출된 작업은 다른 작업과의 의존성이 없으므로 다른 작업과 비동기 콜백 함수의 실행 순서는 상황에 따라 달라질 수 있다.

Comments