티스토리 뷰

1. 동기화란

[예]하나의 객체를 두개의 Thread가 접근할 때 생기는 일

귤 두박스가 있을때, 한 박스당 하나의 스레드가 담당하도록 설정하고 상한 귤을 골라 개수를 계산한다고 할때,

badCounter는 두 박스가 공유함.

heap 메모리 안에 badCounter 객체가 있고, cpu는 single core일때, Thread1과 Thread2에는 context switching이 일어나면서 실행이 될 것이다.

t1이 담당하는 박스에 상한 귤이 2개이고, t2가 담당하는 박스에 상한 귤이 5면 state값이 7개 여야하는데, 그게 보장이 안될 수 있다.

increment 메서드 안에 state++ 코드가 cpu레벨에서 어떻게 실행되는지가 중요하다.

프로그래밍언어를 cpu가 이해할 수 있는 명령어로 변환되어야 하는데, 다음과 같이 변환이 된다.

LOAD state to R1
R1 = R1 + 1
STORE R1 to state

 CPU에 있는 레지스터에 로드 한 뒤에 1을 더해 레지스터에 저장하고,  메모리에 있는 state 변수에 저장하라는 세가지 명령어로 되어 있다.

이 중간에 context switching이 일어난다면, state 변수에 대해서 기대값과 다른 값이 발생한다. (이상한 현상) 

 

race condition(경쟁 조건) : 이렇게 여러 프로세스/스레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황

Synchronization(동기화) : 여러 프로세스/스레드를 동시에 실행해도 race condition없이 공유 데이터의 일관성을 유지하는 것

 

그러면 어떻게 동기화 시킬 것인가?

프로그래밍 언어 레벨에서의 명령문이 CPU에서 실행될 수 있는 명령어 레벨에서 봤을때에는 단일 명령문가 아닌 복합 명령문의 조합이었고, 실행중 context switching이 발생하였기 때문에 문제가 발생했기에 이 영역을 실행할 때에는 context switching이 일어나지 않도록 하면 되겠다고 생각할 수 있음

-> single core에서는 가능하나 multi-core에서는 불가능함. 

두 스레드가 같이 실행하기 때문에..

-> increment()를 실행할 때, 한번에 이 메서드를 한 스레드만 실행할 수 있도록 하는 방법으로 해결할 수 있음.

 

Critical Section(임계 영역) : 공유데이터의 일관성을 보장하기 위해서 하나의 프로세스/스레드만 진입해서 실행 가능한 영역

* critical section 문제의 해결책이 되기 위한 조건

1. mutual exclusion (상호 배제)

 - 하나의 한번의 프로세스/스레드만 실행할 수 있다.

2. progress (진행)

 -  만약 critical section이 비어 있고, 어떤 프로세스/스레드들이 이 영역에 들어가길 원한다면 그 중 하나는 실행될 수 있도록 해야 한다.

3. bounded waiting (한정된 대기)

 - 어떤 프로세스/스레드가 무한정 임계영역에 들어가지 못하고 있으면 안된다.

 

* 프로그래밍 언어레벨 뿐만 아니라 thread-safe 하지 않은 클래스 들이 있다.

- SimpleDateFormat 같은 것들이 동기화 되지 않아 여러 스레드가 공유하면 안된다. 여러 스레드가 동시에 접근하면, 외부에서 동기화 해줘야한다.

 

2. Block I/O 와 Non-Block I/O의 차이

I/O : 데이터의 입출력을 의미함.

I/O의 종류 : 

- Network (socket) I/O

- File I/O

- Pipe I/O (프로세스 간 통신 할때 사용되는 개념)

- Device I/O

 

* Network (socket) I/O :

네트워크 통신은 socket을 통해서 데이터가 입출력 된다.

네트워크를 통해 서로 다른 컴퓨터의 각 프로세스가 통신을 하기위해서는 socket이 필요하다. socket을 통해서 데이터를 주고 받는다.

backend server는 네트워크 상의 client들과 각각 socket을 열고 통신을 한다.

(1) Block I/O

I/O 작업을 요청한 프로세스/스레드는 요청이 완료될 때까지 Block 되는 I/O

I/O 작업이 호출되면 제어권을 가져가서 어플리케이션이 멈춘다. (제어권이 넘어감)

Socket에서 block I/O

Socket마다 두 개의 buffer가 있는데, send_buffer와 receive_buffer가 있다.

위 그림에서 소켓마다 Socket S에서 A로 데이터를 보내야 하는데, 

read(socket A) 시스템 콜을 호출하면, receive buffer에 데이터가 들어올 때 까지 read 시스템 콜을 호출한 thread는 block이 된다. (receive buffer에 데이터가 들어 올 때 까지)

Socket S 입장에서는 Socket S에 write를 하게 되면, send_buffer에 전송하려 하는 data를 쓰게 되는데,

send_buffer가 가득 차면 data를 밀어 넣을 수 없기 때문에 block이 된다.

 

(2) Non-block I/O

: 프로세스/스레드를 블락시키지 않고 요청에 대한 현재 상태를 즉시 리턴한다.

 자신이 호출되었을 떼, 제어권을 자신을 호출한 쪽으로 바로 넘긴다. (Block과의 차이 : 제어권)

Non-block I/O

- 리눅스 기준으로 read I/O 보내고 -1이라는 값과 EAGAIN / EWOULDBLOCK 이라는 에러코드도 함께 리턴한다.

- 이처럼 non-block I/O는 블락되지 않고 즉시 리턴하기 때문에 스레드가 다른 작업을 이어서 수행할 수 있다.

Socket에서 non-block I/O

non-block I/O를 socket에서 보면, receive buffer에 data가 있는지 read로 확인한다. 

block-I/O 경우 데이터가 아직 없으면 read 시스템콜을 호출한 thread가 block 되지만, 여기서는 non-block I/O이기 때문에 receive buffer에 데이터가 없다면 없다고 알려주고 read 시스템콜에 대한 호출은 바로 종료된다.

Socket S에서도 send_buffer가 가득 차 있더라도, write 시스템 콜을 호출한 thread를 block 시키지 않고 적절한 에러코드와 함께 write 시스템 콜은 반환이 된다. 

 

그럼 non-block I/O 작업 완료를 어떻게 확인할 것인가?

3. non-block I/O 결과 처리 방식

(1) 완료되었는지 반복적으로 확인 

 - 문제 : 완료된 시간과 완료를 확인한 시간 사이의 갭으로 인해 처리 속도가 느려질 수 있음. (Time gap 발생 -> 처리가 delay됨)

 - 완료되었는지 반복적으로 확인하는것은 CPU의 낭비가 발생한다. (서버 입장에서 여러개의 소켓 연결이 있을 경우 비효율적이고 CPU를 낭비한다) 데이터를 기다리는 쪽에서는 계속 기다리고 주기적으로 확인을 해야한다. 

-> blocking 하면서 계속 기다린다고 하더라도 

(2) I/O multiplexing (다중 입출력 사용)

: 관심있는 I/O 작업들을 동시에 모니터링하고 그 중에 완료된 I/O 작업들을 한번에 알려줌

- 2개의 소켓에 대해서 non-blocking 모드로 읽으니 새로운 data가 있는지 알려달라고 하는것.

- block으로 실행하거나 non-block으로 또 다른 코드를 실행하게 만들 수도 있다.

- 위 그림은 blocking 모드이다.

- I/O multiplexing 이란, 간략히 말해서 한번의 시스템콜로 여러 event들에 대해서 여러 socket들로 불터 event가 발생하면 알려 달라는 요청을 한번에 처리하는 방식이다.

- epoll로 예시를 들자면, 8개의 socket에 대해서 하나라도 read event 발생 시 (client에서 요청 보냈을 때) 알려달라고 등록한다

그리고 만약 주황, 빨강, 보라 클라이언트가 요청을 보냈다 했을때,  epoll을 호출한 thread는 깨어나면서 동시에 3개의 socket에 data가 있다고 알려준다. 

- 그 정보를 바탕으로 3개의 socket에 대해서만 정보를 읽어 처리해 주면 된다.

- thread pool 사용시, 여러  thread 만들어 놓고, 3개의 소켓에 들어왔던 data 요청들을 thread pool에 넣어서 thread pool에 있는 thread들이 각각의 요청들을 할당받아 동시에 처리할 수 있게 해준다.

 

1)  I/O multiplexing의 종류

a. select

b. poll (성능면으로 좋지 않아 select와 poll 방식은 잘 쓰지 않는다.)

c. epoll (Linux에서 사용하는 방식)

d. kqueue (MacOS에서 사용하는 방식)

e. IOCP(I/O completion port) (Window/Solaris에서 사용하는 방식)

(epoll, kqueue, IOCP 방식을 많이 쓰고 유사하다.)

 

2)  I/O Multiplexing의 분류에 대한 논란

I/O Multiplexing 작업을 요청한 요청자가 결과 / 완료를 계속 챙겨야 하기에 Synchronous가 맞다. 

그리고 Blocking모드가 될수도, Non-Blocking 모드가 될 수도 어떻게 쓰는지에 따라 다르다.

하지만 실제 I/O 작업에 read/write 작업을 Non-Blocking으로 동작하기 때문에 Non-Blocking으로 분류되는 것이 맞다.

 

(3) Callback / Signal 사용

Callback / Signal을 통한 non-block I/O

* 위 그림에서 Signal 경우 위와 같이 동작하지만, callback 함수의 경우 data moved from kernal space to user space시 OS가 별도의 thread를 만들어서 그 thread에서 callback 코드를 실행한다.

 

1) Callback 함수란?

  javascript에서의 Callback 함수 경우, 다른 함수의 매개변수로 함수를 전달하고, 어떠한 이벤트가 발생한 후 매개변수로 전달한 함수가 다시 호출되는 것을 의미한다.

다른 함수가 실행을 끝난 뒤 실행되는 함수인 것이다.

[예시]

function doHomework(subject, callback){
	alert(`Starting my ${subject} homework`);
   callback();
}

function alertFinished(){
	alert('Finished my homework');
}

doHomework('math',alertFinished);

[결과값]:

Starting my math homework

Finished my homework

 

2) Callback / Signal의 종류

- POSIX AIO 

:  여러 운영체제 표준화되어 사용할 수 있는 명세서를 POSIX라고 하며, 안에 정의되어 있는 AIO(Asynchronous IO)를 말한다.

- LINUX AIO

: Linux 커널 자체의 AIO를 통해서 사용하는 것이다.

 

4. Synchronous vs Asynchronous

(1) 프로그래밍 관점

동기(Synchronous): 차례 차례 task를 실행하는 것.

→ synchronous programming: 여러 작업들을 순차적으로 실행하도록 개발하는 것이다.

비동기(Asynchronous): 순서대로 task를 실행하지 않고 동시에 실행하 는 것.

→ Asynchronous programming : 여러 작업들을 독립적으로 실행하도록 개발하는 방법

Synchronous
Asynchronous

  * asynchronous programming은 multithreading은 같지 않다.

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

multithreading은 asynchronous programming의 한 종류를 말한다.

 

(2) Asynchronous programming을 가능하게 하는 것

1) multi-thread

- 동시에 실행. 장점은 multi core를 활용. thread를 너무 많이 만들게 되면 context switching의 비용이 많이 발생하게 되고, race condition 관리 또한 해줘야 한다. 

 

2) non-block I/O

- 하나의 single thread가 I/O 작업을 할 때 blocking 되는 것이 아닌 CPU 작업과 I/O 작업을 동시에 진행한다. 따라서 이 두가지를 잘 조합하면 적은 스레드로도 좋은 성능을 낼 수 있다.

 

→ 따라서 Backend programming의 추세는, thread를 적게 쓰면서도 non-block I/O를 통해 전체 처리량을 늘리는 방향으로 발전중이다.

 

(3) I/O관점에서 Synchronous / Asynchronous 의 차이점

Case 1) 

Synchronous I/O = block I/O

Asynchronous I/O = non-block I/O

 

Case 2) 

Synchronous I/O = 요청자가 I/O 완료, 결과 까지 챙겨야 할 때

Asynchronous I/O = 요청자가 I/O작업을 요청하면서  I/O 작업의  완료, 결과를 직접 챙기지 않아도 결과를 noti로 알려주거나 callback으로 처리

 

Case 3)

Asynchronous I/O = block I/O를 다른 thread에서 실행

→ Thread B를 만들고 thread B에 block I/O를 요청해서 Thread A의 Block을 막고, Thread A는 계속 실행한다.

 

(4) 백엔드 아키텍처 관점에서의 Synchronous Communication vs Asynchronous Communication

하나의 서비스 구현은 내부적으로 기능과 역할에 따라서 여러개의 micro service로 구성되고,

이들 사이에서는 빈번하게 커뮤니케이션이 발생한다. 이 커뮤니케이션을 어떻게 하는지에 따라 분류한다.

1) Synchronous Communication

- 문제는 C에서 응답 불능 상태 빠지면 B,A에 순차적으로 영향을 주게 된다. (서비스 전체에 장애 확대)

 

2) Asynchronous Communication

- Message Q를 사용한 위 문제 해결.

- event 발생 시 Message Q에 넣고 할 일을 쭉 진행. B는 Message Q에 새로운 메세지 없나 계속 consume하면서 기다리고 있다가 새로운 message 도착하면 consume 하면서 계속 진행함.

-> C에서 문제가 생겨도 A나 B까지 타격을 주지 않는다.

일부 서비스에 문제가 생겨도 전체 서비스에 문제는 생기지 않는다.

하지만 A에서 신속하게 B에서 제공하는 데이터를 필요하게 된다면, A에서 요청하고 B를 통해서 정보를 가지고 올 수있게 해야한다. (Message Q를 쓰면 느리기에 이때는 API 쓰는게 더 나을수도 있음) 그리고 A에서는 B에서 이상한 상태 발생해서 제대로 기능 발휘하지 못할때에도 A 영향 최대한 받지 않게 개발하면 된다. 

하지만 위와 같이 단순히 event를 한쪽으로 전달만 하면 된다면 message Q를 사용하는것이 도움이 된다.

 

 

 

[출처]  

유튜브 쉬운코드

- https://www.youtube.com/watch?v=EJNBLD3X2yg&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=14 

- https://www.youtube.com/watch?v=vp0Gckz3z64&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=4

콜백함수

- https://velog.io/@janeshin059/JavaScript-Callback-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-Callback-%EC%A7%80%EC%98%A5%EC%9D%B4%EB%9E%80

 

 

 

 

'Programming > 잡다한것' 카테고리의 다른 글

헥사고날 아키텍처  (0) 2023.06.25
Spring MVC와 Spring Webflux의 비교  (0) 2023.02.26
github repository name convention  (0) 2022.09.02
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday