DOKKAEBI HILL

방문을 환영합니다!

모든 포스팅 >

이벤트 루프 [0] 2025-01-21
sync/async & blocking/non-blocking 2 [0] 2025-01-20
[실전]Asynchronous I/O 적용 [0] 2025-01-14
sync/async & blocking/non-blocking [0] 2025-01-12
CPU Bound - I/O Bound [0] 2025-01-10

sync/async & blocking/non-blocking

open_in_new

(추가) 같은 주제에 대해서 좀 더 자세히 포스팅한 글이 있습니다. 

이 글이 부족하신 분들은 이 포스팅도 읽어보세요~


글 제목을 정하거나 코딩을 하면서 변수나 함수명을 정할 때, 그 명칭만으로 의미를 알 수 있고 간결하게 칭할 수 있는 이름을 정하려고 고민합니다. "이름 정하는 게 제일 어렵다!"는 경험은 아마 다들 공감하실 거라고 생각합니다. 그런데 이게 어떤 개념을 설명하는 용어가 될 때는 어떨까요?

그 개념을 가장 잘 표현할 수 있는 명칭으로 고심하고 합의하여 정했을 거라고 생각합니다. 


그래서 저는 헷갈리는 개념이 있으면 왜 이걸 이렇게 부르지?하고 그 명명에 힌트가 있다고 생각합니다. 특히, CS 같이 해외에서 발전된 분야는 번역 과정에서 본래 의미가 흐려질 수 있고 그래서 그들의 언어적 뉘앙스를 이해하려고 노력합니다. 영어 단어를 외울 때 예문을 봐야하는 것이 같은 이유겠죠.


Synchronous와 Asynchronous

'Synchronous', '동기화'가 전 잘 와닿지 않았습니다. 그래서 어학사전을 찾았죠.


Synchronous | Syn- + Chronos

함께, 동시에를 의미하는 접두사 Syn과, 시간(time)을 의미하는 단어가 합쳐진 것이 어원입니다. 


옥스포드 사전에서 synchronization의 의미는 다음과 같습니다.

1. the fact of happening at the same time, or the act of making things happen at the same time.

2. the act of making sure that watches or clocks show exactly the same time


동시에 일어나는 것, 또는 동시에 일어나도록 하는 행위. 시간이 일치하도록 맞추는 행위.

'동시에'라는 의미에 비추어 볼 때, 복수의 어떤 것들이 동시에 일어나도록 맞춘다는 의미로 느껴집니다.


'time', '동시', 'at the same time'가 계속 등장하고 있습니다. 그 때문에 '시간'적으로 일치한다는 것으로 받아들여지는데, 그러면 우리가 일상에서 쓰는 클라우드 동기화는 뭐람? 시간이 일치? .. 직관적으로 받아들여지지 않습니다. 


결론부터 말하자면 실제로 동기화의 개념은 시간을 넘어서 데이터, 정보의 일치화까지 개념이 확장되었습니다.

또한, 공룡책의 synchronization 챕터의 race condition을 설명하는 부분에 보면,

to guard against the race condition above, we need to ensure that only one process at a time can be manipulating the variable  counter. To make such a guarantee, we require that the processes be synchronized in some way.

여러 프로세스가 동시에 같은 메모리 공간에 접근할 때, 접근 순서에 따라서 결과값이 달라질 수 있다는 걸 경고하며 공유 메모리 공간에 하나의 프로세스가 접근하도록 해야한다고 말하는 구절입니다. 여기서 synchronized를 동시에 일어나도록 한다로 해석하면 좀 이상합니다.


어원과 확장된 개념 그리고 사용되는 예시를 종합해서 생각해보면, 복수의 어떤 것들이 서로의 상태를 계속 확인하면서 조화를 이루어 차이가 없도록 하는 것 + 그런 행위 이란 뉘앙스로 이해할 수 있습니다.

( 영미권 문화가 아니란 사실은 그 언어를 쓰는 사회 문화를 이해해볼 기회를 준다는 점에서 참 좋은 것 같습니다. 럭키비키가 아닐 수가 없네요.  🤐 )


웹 통신은 엄연히 클라이언트와 서버인 remote processes 간의 통신으로 

여기서 동기적 통신이라 함은 요청자(A)가 응답자(B)에게 요청을 하고 응답을 받기까지 계속 상태를 확인하면서 서로 같은 시점과 정보를 공유하는 방식이라 할 수 있습니다. 응답 결과를 결국 두 눈으로 확인하고야 마는 방식이죠. (일종의 직거래? 중고거래를 할 때 직거래를 하면 상대방이 말하는 물건 상태와 내가 기대하는 물건 상태가 같은지 확인할 수 있고 거래를 정상적으로 마칠 수 있는(거래가 성립하든 불발이 되든) 신뢰성을 제공합니다.)


비동기 통신은 이런 조화 과정이 무시된 방식으로 A는 B에게 요청을 하고 돌아가고, B는 응답할 준비가 다 되면 callback 형태로 응답을 하게 됩니다. (택배 거래와 비슷하다고 볼 수 있겠습니다. 구매자는 판매자에게 입금하고 주소를 알려주면 판매자는 ok하고 택배를 부치고 송장번호정도만 먼저 알려줍니다. 그리고 시간이 지나서 택배를 받은 구매자는 그게 벽돌인지 아니면 아이폰인지 알 수 있겠죠. 하지만 택배가 오는 동안에 구매자는 본인의 일상을 보낼 수 있습니다.)


Blocking & Non-Blocking

Block은 무언가의 움직임, 일어나는 일, 진행을 막는 것을 의미합니다.

A가 B에게 요청을 했는데 응답하는 동안 B가 제어권을 점유하여 A의 진행이 막히는 경우를 blocking,

그렇지 않고 B가 요청을 받고 제어권을 다시 반납해서 A가 응답을 받을 때까지 기다리지 않고 A의 흐름을 진행할 수 있으면 non-blocking입니다.


sync/async 그리고 blocking/non-blocking은 비슷한 개념처럼 보이지만 

'응답에 관심이 있는지 여부' 그리고 '제어권 점유'의 관점의 차이가 있습니다. 




개념을 구분하여 굳이 매트릭스로 표현하면 위와 같습니다. 


이 두 개념을 코루틴을 이용해서 파이썬 코드로 구현해보았습니다. 

흐름 정도를 이해하는데에 참고하면 좋을 것 같습니다. 내부적인 매커니즘이 이와 같은지는 잘 모르겠습니다. (도움이 되면 좋겠습니다..!)


sync & blocking

# sync & blocking
def task():
    num = 0
    
    for i in range(5):
        num += i
        print("...열일중")
        time.sleep(2)

    return num

def main():
    start = time.time()
    num = 0
    print("작업 요청")
    result = task()
    for i in range(5):
        print("나도 열일중...")
        time.sleep(1)
        num -= i
    print("작업 끝")
    print(f"시킨 일: {result}")
    print(f"내가 한 일: {num}")
    end = time.time()
    print(end-start)

main()
    


이건 일반적인 코드 흐름과 같기에 이해하기 어렵지 않습니다. 

1. main ->  task에 작업 요청

2. task가 제어권을 가지고 있고 작업 실행

3. 제어권이 다시 main으로 왔을 때 main의 일 실행


sync & non-blocking 

# sync & non-blocking
async def task():
    num = 0
    
    for i in range(5):
        num += i
        print("...열일중")
        await asyncio.sleep(2)

    return num

async def main():
    start = time.time()
    num=0
    print("작업 요청")
    result = loop.create_task(task())
    while not result.done():
        print(f"작업 끝났어? 대답.")
        await asyncio.sleep(1)
        
    for i in range(5):
        print("나도 열일중...")
        time.sleep(1)
        num -= i
    
    print("작업 끝")
    print(f"시킨 일: {result.result()}")
    print(f"내가 한 일: {num}")
    end = time.time()
    print(end - start)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())


( 여기서 보이는 async 키워드는 단지 코루틴을 생성하기 위한 파이썬 문법이니 헷갈리지 않으시길 바랍니다! )

이벤트 루프를 통해 제어권이 반납될 수 있도록 하기 위해서 첫번째 예시와 달리 여기서는 aysncio.sleep을 사용하였습니다. 

그 덕에 while과 result.done()으로 계속 작업 완료 여부를 확인하는 연출을 할 수 있었습니다 ㅎㅎ

non-block으로 통제권이 main에게 넘어갔으나 그럼에도 synchronous 통신 성질에 따라 task가 완료된 후에 main이 자기 일을 할 수 있네요.


(경우에 따라 다르겠지만 실제 구현에서 라면 result.done()을 이용하기 보다는 await를 사용한다면 해당 태스크가 끝날 때 까지 기다린 후에 다음 명령을 진행하도록 보다 (흐름 제어를 통해)synchronously한 구현이 가능합니다!)


async & blocking

# async & blocking
async def task():
    num = 0
    
    for i in range(5):
        num += i
        print("...열일중")
        time.sleep(2)

    return num

async def main():
    num = 0
    print("작업 요청")
    result = loop.create_task(task())
    while not result.done():
        print(f"작업 끝나면 말해줘. 나도 열일중...")
        num -= 1
        await asyncio.sleep(1)
    
    print("작업 끝")
    print(f"시킨 일: {result.result()}")
    print(f"내가 한 일: {num}")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())


blocking의 제어권 점유를 연출하고자 다시 time.sleep을 썼습니다. (time.sleep은 스레드 레벨에서 pause)

여기서 주목할 부분이 '...열일중'이라고 하기 전에 while을 타면서 main은 synchronous 상황과 달리 일을 맡기고 자기 일을 하려고 돌아온 것을 볼 수 있습니다. 하지만 제어권을 task가 가지는 바람에 main은 자기 일을 제대로 하지 못했습니다.


async & non-blocking

# async & non-blocking
async def task():
    num = 0
    
    for i in range(5):
        num += i
        print("...열일중")
        await asyncio.sleep(2)

    return num

async def main():
    start = time.time()
    num = 0
    print("작업 요청")
    result = loop.create_task(task())
    while not result.done():
        print("작업이 끝나면 말해줘. 나도 열일중...")
        num -= 1
        await asyncio.sleep(1)
    
    print("작업 끝")
    print(f"시킨 일: {result.result()}")
    print(f"내가 한 일: {num}")
    end = time.time()
    print(end-start)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

마지막 케이스입니다. 두 녀석들이 사이좋게 열일을 하고 있습니다. 개발자로서 흐뭇하지 않을 수가 없습니다!

main은 task에게 일을 맡기고 돌아왔고, task도 제어권을 양보하여 각자의 자리에서 열일하는 모습이 보기 좋습니다.

걸린 시간을 보면 3번 경우와 비슷하지만 작업량 자체는 다릅니다.


다시 한번 말하면 두가지 개념을 시각화하기 위한 연출이니 재미로 봐주시기 바랍니다.

그럼에도 이번 연출로 여러가지 교훈을 우리는 배울 수 있는데!

1. 강력한 동시성을 구현하는 async & non-blocking의 힘! (작업 효율을 높인다)

2. async 처리를 과정에 blocking이 있으면 무용지물! 



게시글을 삭제하시겠습니까?

'delete'를 입력하고 '삭제' 버튼을 눌러주세요.

All rights reserved.

GUEST님 환영합니다! ^_^