[Python] 동기, 비동기, 블럭, 넌블럭 파이썬으로 알아보기
개발자라면 한 번씩 듣게되는 동기(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/