ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 동기, 비동기, 블럭, 넌블럭 파이썬으로 알아보기
    Software Development/Python 2020. 10. 21. 16:46

    개발자라면 한 번씩 듣게되는 동기(Synchronous), 비동기(Asynchronous), 블럭(Block), 넌블럭(Non-block)을 Python코드를 통해 알아보겠습니다.

     

    우선 무엇을 의미하는지 정의하겠습니다.

     

    • Block : 호출된 함수가 자신이 할 일을 모두 마칠 때까지 제어권 계속 가지고서 호출한 함수에게 바로 돌려주지 않는 것.[2]
      • ex) 세탁기를 돌리는 동안 아무것도 하지 않고 기다린다.
    • Non-block : 호출된 함수가 자신이 할 일을 채 마치지 않았더라도 바로 제어권건네주어(return) 호출한 함수가 다른 일을 진행할 수 있도록 해주는 것.[2]
      • ex) 세탁기를 돌리는 동안 TV를 본다.
    • Synchronous : 호출된 함수의 수행 결과 및 종료를 호출한 함수가(호출된 함수뿐 아니라 호출한 함수도 함께) 신경 쓰는 것.[2]
      • ex) 세탁기가 다 돌고 건조기를 돌린다.
    • Asynchronous : 호출된 함수의 수행 결과 및 종료를 호출된 함수 혼자 직접 신경 쓰고 처리하는 것(as a callback fn.).[2]
      • ex) 세탁기를 돌리는 동안 청소기를 돌린다.

    {Synchronous, Asynchronous} 와 {Block, Non-block}을 조합하면 총 4가지가 나옵니다.

    각 Case를 예를 들어 설명하면 다음과 같습니다.

    Blocking & Synchronous

    1. 손빨래를 한다. (Block)

    2. 손빨래가 끝났다. 이제 세탁물을 널자. (Synchronous)

    Blocking & Asynchronous

    1. 세탁기에 빨래를 돌리고 TV를 본다. (Asynchronous)

    2. 빨래를 널기 위해 세탁기가 돌 때까지 기다린다.(Block)

     

    Non-blocking & Synchronous

    1. 세탁기에 빨래를 돌리고 빨래가 될 때까지 TV를 보면서 세탁기를 한 번씩 확인한다. (Non-block)

    2. 세탁이 됐는지 한 번씩 확인한 후 빨래가 돌면 빨래를 넌다. (Synchronous)

    Non-blocking & Asynchronous

    1. 빨래를 돌리고 나서 식기세척기를 돌리고 청소기를 돌린다.(non-block)

    2. 청소기를 다 돌리고 나서 식기세척기가 먼저 다 돌아서 건조기보다 그릇을 먼저 정리한다.(Asynchronous)

     

     

    위 Case를 Python 코드를 통해 구현 해보겠습니다. 예제 코드는 링크에서 보실 수 있습니다.

    함수는 크게 wash, dry, cleaner가 있습니다. 각가 세탁, 건조, 청소기를 의미합니다.

     

    Blocking & Synchronous

     

    def wash(clothes):
        cloth_type = {'a' : 2, 'b': 3,  'c': 4}
        return_clothes = []
        for cloth in clothes:
            if cloth_type.get(cloth, None) == None:
                wash_time = 5
            else:
                wash_time = cloth_type[cloth]
    
            execute_time = 0
            while True:
                print(f'Washing cloth {cloth} ... {wash_time - execute_time} time left.')
                time.sleep(1)
                execute_time += 1
                if execute_time >= wash_time:
                    break
            return_clothes.append("wash_" + cloth)
    
        print('Finish the wash!')
        return return_clothes
    
    def dry(clothes):
        for cloth in clothes:
            time.sleep(1)
            print(f'Dry cloth {cloth} ... complete.')
                
                
    def cleaner(room_nums):
        for room_idx in range(room_nums):
            time.sleep(room_idx)
            print(f'Clean room idx {room_idx} ... complete.')
            
    def main() -> None:
        st = datetime.now()
        print(f"started at {st}")
    
        clothes = ['a', 'b', 'c', 'd']
        clothes = wash(clothes)
        dry(clothes)
        room_nums = 10
        cleaner(room_nums)
    
        et = datetime.now()
        print(f"finished at {et}")
        print(f"time for task: {et-st}")
    
    if __name__ == "__main__":
        main()

    실행 결과는 다음과 같습니다.

    started at 2020-10-21 20:05:15.994880
    Washing cloth a ... 2 time left.
    Washing cloth a ... 1 time left.
    Washing cloth b ... 3 time left.
    Washing cloth b ... 2 time left.
    Washing cloth b ... 1 time left.
    Washing cloth c ... 4 time left.
    Washing cloth c ... 3 time left.
    Washing cloth c ... 2 time left.
    Washing cloth c ... 1 time left.
    Washing cloth d ... 5 time left.
    Washing cloth d ... 4 time left.
    Washing cloth d ... 3 time left.
    Washing cloth d ... 2 time left.
    Washing cloth d ... 1 time left.
    Finish the wash!
    Dry cloth wash_a ... complete.
    Dry cloth wash_b ... complete.
    Dry cloth wash_c ... complete.
    Dry cloth wash_d ... complete.
    Clean room idx 0 ... complete.
    Clean room idx 1 ... complete.
    Clean room idx 2 ... complete.
    Clean room idx 3 ... complete.
    Clean room idx 4 ... complete.
    Clean room idx 5 ... complete.
    Clean room idx 6 ... complete.
    Clean room idx 7 ... complete.
    Clean room idx 8 ... complete.
    Clean room idx 9 ... complete.
    finished at 2020-10-21 20:06:19.255739
    time for task: 0:01:03.260859

    실행결과에서 보이는 것 처럼 모든 task가 순차적으로 실행된 것을 볼 수 있습니다.

     

    Blocking & Asynchronous

    Asynchronous로 태스크를 구현하기 위해서 asyncio를 적용하였습니다. 

     

    import asyncio
    from datetime import datetime
    
    async def wash(clothes):
        cloth_type = {'a' : 2, 'b': 3,  'c': 4}
        return_clothes = []
        for cloth in clothes:
            if cloth_type.get(cloth, None) == None:
                wash_time = 5
            else:
                wash_time = cloth_type[cloth]
    
            execute_time = 0
            while True:
                print(f'Washing cloth {cloth} ... {wash_time - execute_time} time left.')
                await asyncio.sleep(1)
                execute_time += 1
                if execute_time >= wash_time:
                    break
    
            return_clothes.append("wash_" + cloth)
        print('Finish the wash!')
        return return_clothes
    
    
    async def dry(clothes):
        for cloth in clothes:
            await asyncio.sleep(1)
            print(f'Dry cloth {cloth} ... complete.')
                
    async def cleaner(room_nums):
        for room_idx in range(room_nums):
            await asyncio.sleep(1)
            print(f'Clean room idx {room_idx} ... complete.')
    
    async def main():
        clothes = ['a', 'b', 'c', 'd']
        room_nums = 10
    
        task1 = asyncio.create_task(
            wash(clothes))
    
        task3 = asyncio.create_task(
            cleaner(room_nums))
        st = datetime.now()
        print(f"started at {st}")
    
        washed_cloth = await task1
        await dry(washed_cloth)
        await task3
        et = datetime.now()
        print(f"finished at {et}")
        print(f"time for task: {et-st}")
    
    if __name__ == "__main__":
        asyncio.run(main())
    

    실행 결과는 다음과 같습니다.

    started at 2020-10-21 20:10:47.171414
    Washing cloth a ... 2 time left.
    Washing cloth a ... 1 time left.
    Clean room idx 0 ... complete.
    Washing cloth b ... 3 time left.
    Clean room idx 1 ... complete.
    Washing cloth b ... 2 time left.
    Clean room idx 2 ... complete.
    Washing cloth b ... 1 time left.
    Clean room idx 3 ... complete.
    Washing cloth c ... 4 time left.
    Clean room idx 4 ... complete.
    Washing cloth c ... 3 time left.
    Clean room idx 5 ... complete.
    Washing cloth c ... 2 time left.
    Clean room idx 6 ... complete.
    Washing cloth c ... 1 time left.
    Clean room idx 7 ... complete.
    Washing cloth d ... 5 time left.
    Clean room idx 8 ... complete.
    Washing cloth d ... 4 time left.
    Clean room idx 9 ... complete.
    Washing cloth d ... 3 time left.
    Washing cloth d ... 2 time left.
    Washing cloth d ... 1 time left.
    Finish the wash!
    Dry cloth wash_a ... complete.
    Dry cloth wash_b ... complete.
    Dry cloth wash_c ... complete.
    Dry cloth wash_d ... complete.
    finished at 2020-10-21 20:11:05.317895
    time for task: 0:00:18.146481

    실행결과를 보면 wash와 cleaner가 동시에 실행되는 것(async)을 볼 수 있습니다. 그러나 dry는 blocking 되어 있습니다. wash로 부터 parameter를 받아서 실행가능하기 때문입니다.

     

    Non-blocking & Synchronous

    Non-blocking & Synchronous를 구현하기 위해 generator를 사용하였습니다. wash는 제너레이터인데 yield를 통해 제어권을 반환합니다.

     

    반환된 값이 list type이 아니면 계속해서 nothing...이라고 출력합니다.

     

    말 그대로 아무것도 하지 않지만 wash에서 빠져나왔기 때문에 blocking 되어있진 않았습니다. 반환된 값이 list type의 객체여야 dry를 실행할 수 있으므로 Synchronous 하다고 볼 수 있습니다. 

     

    import sys
    import time
    from datetime import datetime
    from datetime import timedelta
    
    
    def wash(clothes):
        cloth_type = {'a' : 2, 'b': 3,  'c': 4}
        return_clothes = []
        value = None
        check_time = datetime.now()
        for idx, cloth in enumerate(clothes):
            if cloth_type.get(cloth, None) == None:
                wash_time = 5
            else:
                wash_time = cloth_type[cloth]
    
            execute_time = 0
            while True:
                print(f'Washing cloth {cloth} ... {wash_time - execute_time} time left.')
                time.sleep(1)
                execute_time += 1
                if execute_time >= wash_time:
                    break
            return_clothes.append("wash_" + cloth)                
    
            if check_time > datetime.now():
                continue
            else:
                recieved = yield None
                if recieved == None:
                    recieved = 0
                check_time = datetime.now() + timedelta(seconds=recieved)
        yield return_clothes
    
    def dry(clothes):
        for cloth in clothes:
            time.sleep(1)
            print(f'Dry cloth {cloth} ... complete.')
                
    def main() -> None:
        st = datetime.now()
        print(f"started at {st}")
    
        clothes = ['a', 'b', 'c', 'd']
        gen_wash = wash(clothes)
        while True:
            return_clothes = next(gen_wash)
            if type(return_clothes) == type([]):
                break  
            else:
                print('nothing...')
            gen_wash.send(3)
            
        dry(return_clothes)
    
        et = datetime.now()
        print(f"finished at {et}")
        print(f"time for task: {et-st}")
    
    if __name__ == "__main__":
        main()

     

    실행결과는 다음과 같습니다.

    started at 2020-10-21 20:13:18.483847
    Washing cloth a ... 2 time left.
    Washing cloth a ... 1 time left.
    nothing...
    Washing cloth b ... 3 time left.
    Washing cloth b ... 2 time left.
    Washing cloth b ... 1 time left.
    Washing cloth c ... 4 time left.
    Washing cloth c ... 3 time left.
    Washing cloth c ... 2 time left.
    Washing cloth c ... 1 time left.
    nothing...
    Washing cloth d ... 5 time left.
    Washing cloth d ... 4 time left.
    Washing cloth d ... 3 time left.
    Washing cloth d ... 2 time left.
    Washing cloth d ... 1 time left.
    Dry cloth wash_a ... complete.
    Dry cloth wash_b ... complete.
    Dry cloth wash_c ... complete.
    Dry cloth wash_d ... complete.
    finished at 2020-10-21 20:13:36.615369
    time for task: 0:00:18.131522

    wash task가 다 끝나고 dry task가 실행되는 것을 볼 수 있고 중간 중간 'nothing...'이 출력된 것을 볼 수 있습니다. nothing...이 출력되는 것은 wash에서 빠져나왔다는 것을 의미하고 다시 next() 함수를 통해 wash 제너레이터를 실행합니다.

     

    여기서 send() 함수를 볼 수 있는데 send()함수를 wash에서 recieved = yield None에서 받습니다. recieved를 받아서 받은 임의의 초(seconds)만큼 후에 다시 yield를 실행합니다.

     

    만일 send()에 파라미터로 100을 주면 현재 시간 + 100초 만큼 yield를 하지 않고 blocking되어 wash의 for문을 돌며 처리합니다.

     

    즉 세탁기로 비유 하자면 중간중간에 세탁기를 열어서 세탁기 다 돌았는지 확인하고 돌지 않았다면 세탁기에게 임의의 초 만큼 뒤에 다시 확인하도록 설정한다고 볼 수 있습니다.

     

    Non-blocking & Asynchronous

    Non-blocking & Asynchronous을 구현하기 위해 세탁기 여러대를 동시에 돌리고 건조시키는 세탁기를 준비했다고 보시면 됩니다. 그래서 옷 마다 세탁기에 돌려 세탁을 동시에 실행하는 것입니다.

     

    코드는 다음과 같습니다.

    import asyncio
    from datetime import datetime
    
    async def wash(cloth):
        cloth_type = {'a' : 2, 'b': 3,  'c': 4}
        return_clothes = []
        execute_time = 0
    
        if cloth_type.get(cloth, None) == None:
            wash_time = 5
        else:
            wash_time = cloth_type[cloth]
    
        while True:
            print(f'Washing cloth {cloth} ... {wash_time - execute_time} time left.')
            await asyncio.sleep(1)
            execute_time += 1
            if execute_time >= wash_time:
                break
        await dry("wash_" + cloth)
    
    
    async def dry(cloth):
        await asyncio.sleep(2)
        print(f'Dry cloth {cloth} ... complete.')
                
    async def cleaner(room_nums):
        for room_idx in range(room_nums):
            await asyncio.sleep(1)
            print(f'Clean room idx {room_idx} ... complete.')
    
    async def main():
        clothes = ['a', 'b', 'c', 'd']
        room_nums = 10
    
        st = datetime.now()
        print(f"started at {st}")
        tasks = []
        
        for cloth in clothes:
            tasks.append(asyncio.create_task(
                wash(cloth)))
    
        tasks.append(asyncio.create_task(
            cleaner(room_nums)))
    
        for tmp_task in tasks:
            await tmp_task
    
        et = datetime.now()
        print(f"finished at {et}")
        print(f"time for task: {et-st}")
    
    if __name__ == "__main__":
        asyncio.run(main())
    

     

    아래는 실행 결과입니다.

    Washing cloth a ... 2 time left.
    Washing cloth b ... 3 time left.
    Washing cloth c ... 4 time left.
    Washing cloth d ... 5 time left.
    Washing cloth a ... 1 time left.
    Washing cloth c ... 3 time left.
    Clean room idx 0 ... complete.
    Washing cloth b ... 2 time left.
    Washing cloth d ... 4 time left.
    Clean room idx 1 ... complete.
    Washing cloth c ... 2 time left.
    Washing cloth b ... 1 time left.
    Washing cloth d ... 3 time left.
    Clean room idx 2 ... complete.
    Washing cloth c ... 1 time left.
    Washing cloth d ... 2 time left.
    Clean room idx 3 ... complete.
    Dry cloth wash_a ... complete.
    Washing cloth d ... 1 time left.
    Dry cloth wash_b ... complete.
    Clean room idx 4 ... complete.
    Dry cloth wash_c ... complete.
    Clean room idx 5 ... complete.
    Clean room idx 6 ... complete.
    Dry cloth wash_d ... complete.
    Clean room idx 7 ... complete.
    Clean room idx 8 ... complete.
    Clean room idx 9 ... complete.
    finished at 2020-10-21 20:25:58.917674
    time for task: 0:00:10.094013

    실행 결과를 보면 wash, dry, cleaner가 동시에 실행되는 것을 볼 수 있습니다. dry를 하기 위해서는 반드시 wash가 선행되어야 합니다. 그렇기 때문에 옷 전체를 세탁하고 dry를 하는 것은 반드시 blocking될 수 밖에 없습니다. 그러나 task를 여러개로 나눠서 wash를 실행하면 각 dry가 옷 마다 실행될 수 있기 때문에 blocking되지 않습니다.

     

    cloth a 세탁 -> cloth a 건조 -> cloth b 세탁 -> cloth b 건조와 같이 순차적으로 실행되어야 하는데 cloth a를 건조하면서 cloth b가 세탁되는 것을 볼 수 있습니다. a의 wash와 dry가 다 실행되지 않았음에도 b가 wash가 되는 것이 non-blocking이라 볼 수 있습니다.

     

     

    References

    [1] luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking

    [2] hamait.tistory.com/930

    [3] musma.github.io/2019/04/17/blocking-and-synchronous.html

    [4] itholic.github.io/python-select/

     

    댓글

Designed by Tistory.