-
[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/
'Software Development > Python' 카테고리의 다른 글
CPython Internals (0) 2024.06.01 [Python] TimeoutError: [WinError 10060] (0) 2020.11.02 [Python] 파이썬 - asyncio callback 알아보기 (0) 2020.09.24 [Python] Pylint - 정의와 예제를 통해 Python 린트 툴 알아보기 (1) 2020.09.03 Python traceback -오류를 역추적 하기 (0) 2020.06.24 - Block : 호출된 함수가 자신이 할 일을 모두 마칠 때까지 제어권을 계속 가지고서 호출한 함수에게 바로 돌려주지 않는 것.[2]