[운영체제] 뭐만 하면 Asynchronous(비동기)를 붙이는데.. 그래서 비동기가 정확하게 어떤 의미일까? (feat. multi-threads, non-block I/O)

프로그래밍의 관점에서 동기와 비동기란?

Synchronous(동기) programming

여러 작업(task)들을 순차적으로 실행하도록 개발하는 방법

순차적으로 실행되기에 순서를 보장한다.

Asynchronous(비동기) programming

여러 작업(task)들을 독립적으로 실행하도록 개발하는 방법

독립적으로 실행되기에 순서를 보장하지 않는다.

asynchronus programming의 한 종류가 multithreading이다.

asynchronus programming은 여러 작업을 동시에 실행하는 프로그래밍 방법론이고,

multithreading은 aynchronus programming의 한 종류다. 

 

asynchronus programming을 가능하게 하는 것은 2가지가 있다.

  • multi-threads
  • non-block I/O

이제부터 각각 자세히 알아보자!

multi-threads

여러가지 작업들을 멀티 스레드가 나눠가져서 여러 가지 일을 동시에 실행한다.

이렇게 하면 멀티코어를 활용할 수 있는 장점이 있지만,

스레드를 너무 많이 만들게 되면 컨텍스트 스위칭에 비용을 많이 쓰게 되는 단점이 있고,

레이스 컨디션이 발생할 수 있기 때문에 이를 발생하지 않도록 처리해줘야 한다. 

 

레이스 컨디션

두 개 이상의 프로세스 혹은 스레드가 공유 자원을 서로 사용하려고 경합(Race)하는 현상을 의미한다

 

non-block I/O 

하나의 싱글 스레드가 I/O 작업을 blocking하지 않고 실행시킨뒤, (물론, 또 다른 I/O 작업도 블로킹하지 않고 실행시킨다면 상관없다.)

더불어 CPU를 사용하는 여러 작업도 동시에 처리하게끔 할 수 있다.

 

중요한 것은 CPU 작업과 I/O 작업은 동시에 할 수 있는 작업이라는 점이다.

그래서 I/O 작업을 non-block으로 할 경우, 싱글 스레드여도 여러 가지 일을 동시에 처리할 수 있다.

 

이제부터는 multi-threads + non-block I/O를 적절히 활용하여 전체 처리량을 늘리는 방향으로 프로그래밍을 하자!

multi-threads와 non-block I/O를 적절하게 활용하면 적은 스레드로 좋은 성능을 낼 수 있는 프로그래밍을 할 수 있다.

요즘 백엔드 프로그래밍의 추세는 스레드를 적게 쓰면서 non-block I/O를 통해 전체 처리량을 늘리는 방향으로 발전 중에 있다고 한다.

 

I/O 관점에서 동기와 비동기란?

경우에 따라 다르기에 문맥을 보고 이해하는 것이 중요하다. 다만, 몇 가지 정리를 해본다면?!

 

case 1 ( 단순하게 생각하는 경우)

synchronous I/O = block I/Oasynchronous I/O = non-block I/O보통은 이렇게 이해할 수 있다.

 

case 2 ( I/O 작업을 내가 챙길지 아니면 noti, callback으로 처리할 지)

synchronous I/O : I/O 작업을 요청한 요청자가 I/O 작업의 결과(완료)까지 챙겨야하는 경우

asynchronous I/O : 요청자가 I/O 작업을 요청하면서 I/O 작업의 결과(완료)를 noti 알려주거나, callback으로 처리할 경우

 

case 3 ([관점에 따라] 비동기인지 동기인지 헷갈리는 이유)

asynchronous I/O : block I/O를 다른 스레드에서 실행할 때 

주의! 이 경우는 생각에 따라 동기일수도 있고 비동기일 수 있다.

스레드 A가 실행하는데 중간에 block I/O를 실행하면 스레드 A는 block이 된다.

 

근데 스레드 A를 block으로 만들고 싶지 않아서,

새로운 스레드 B를 만들고 스레드 B에다가  block I/O 작업을 요청하면 

스레드 B는 block I/O 작업을 하는 동안 block이 되지만!

스레드 A는 block 되지 않아 계속 처리를 할 수 있게 된다. 

 

그 후 스레드 B 작업이 완료되면 결과를 스레드 A에서 합쳐서 그에 대한 처리를 하는 경우도

asynchronous I/O라고 할 수 있다. (내가 내린 결론 -> 이유는 동시에 여러 작업을 처리하기 때문에)

 

하지만 B의 결과를 취합을 하는 과정에서 A는 어느 시점에서 동작을 멈추고 B가 완료되길 기다려야 하기에 동기(프로그래밍)이라고도 할 수 있다고 생각한다.

이는 프로그래밍 관점에서의 비동기 프로그래밍의 의미와  I/O 작업을 함께 사용되어서 그런게 아닐까..라는 생각이다.

 

중요한 건. 해당 경우가 왜 동기라고 생각할 수도 있고, 비동기라고 생각할 수 있는지 

왜 헷갈릴 수 있는지를 알고 있는 것이라고 생각했기에 해당 경우를 적어보았다.

 

 

 

백엔드 아키텍처 관점에서 동기와 비동기란?

하나의 서비스를 구현하게 되면 내부적으로 기능과 역할에 따라서 

여러 개의 마이크로 서비스로 구성된다. (이를 마이크로 서비스 아키텍쳐[MSA] 라고 한다.)

이들 사이에는 빈번하게 커뮤니케이션이 발생하게 된다.

 

이 커뮤니케이션을 어떻게 하는지에 따라서 

Synchronous communication라 할 수도 있고,

Asynchronous communication라고도 할 수도 있다.

 

Synchronous communication

A, B, C를 각각을 마이크로 서비스로 가정해보자

A에서 어떤 코드를 수행하다가 B가 관심있는 event가 발생을 해서

B에서 API를 만들어서 API로 event를 보내줘! 약속을 했다.

 

그러면 이제 A에서 B가 관심있는 event가 발생해서 

API call을 통해 B에게 event를 전달한다.

 

B 또한 C가 관심있는 event 발생해서 A, B처럼 처리하기로 약속해서 처리했다.

 

그렇게 해서 A는 B에게 API call로 event 보내고 응답이 올 때까지 대기하고

B는 C에게 API call로 event 보내고 응답이 올 때까지 대기하다가

C가 응답하면 B가 처리하고 A에게 응답하면

드디어 A는 작업을 이어서 처리한다. 

 

이 때 어떤 문제가 발생할 수 있을까?

C가 만약 장애가 나서 응답 불능이 되었다고 가정했을 때,

자칫 잘못할 경우 그 영향이 B도 응답 불능되고 그에 따라 A도 응답 불능이 될 수 있다.

그렇게 되면 전체 서비스 장애로 이어질 수 있는 것이다.

 

이런 문제를 해결하기 위해서 Asynchronous communication을 할 수 있도록 구조를 짜는 것이 중요하다.(다만, 항상 그런건 절대 Never 아니다!)

 

 

Asynchronous communication

비동기 커뮤니케이션에서의 핵심은 Message Q를 사용한다는 점이다.

 

A가 작업을 하다가 B가 관심 있는 작업(event)을 API call로 바로 evnet를 전달하지 않고 Message Q에 넣고, A는 작업을 계속 한다. 

 

그러면 B는 Message Q에 새로운 작업을 consume 하면서 기다리고 있다가

새로운 작업을 발견하면 consume해서 관련 작업을 진행한다.

(B도 C에게 API call 대신 Message Q에 넣는다.)

 

이런 식으로 비동기 커뮤니케이션을 하면 장점은 

C에서 문제가 생겨도 해당 영향이 B에게 전파되지 않는다.

왜나하면?

중간에 Message Q 라는 Buffer를 두었기에, B는 Message Q만 바라보고 있어서

C에서 문제가 생겨도 해당 장애는 C에서만 머물기에

일부 서비스는 장애가 나더라도 전체 서비스는 지킬 수 있다. 

 

즉, 한쪽으로만 일방적으로 event를 전달하는 경우에는 메시지 큐를 두는 게 더 안정적일 수 있는 서버 아키텍처가 될 수 있다.

 

근데 또 이게 항상 좋은 커뮤니케이션은 아니다!

A에서 B가 제공하는 데이터를 필요로 했을 때,

B에서 API를 만들고 A가 API call을 하게끔 해야한다.(Synchronous communication)

 

물론 비동기 커뮤니케이션으로도 구현은 할 수 있지만!!

빠르게 B로부터 데이터를 전달받아서 처리해야하는 경우에는

상대적으로 Message Q가 처리가 느릴 수 있기 때문에 

API call를 쓰는게 더 좋을 수 있다. 

 

이 때 A에서는 B가 장애가 나서 제대로 기능이 동작하지 않더라도

A가 최대한 영향을 받지 않도록 개발하면 된다.