async event loop 기반 framework 인 python tornado 를 사용하여 웹소켓 서버에 데이터를 보내던 중에 문제가 발생했다.
tornado 는 멀티스레딩 사용 시 main event loop 에서 실행할 기능을 콜백에 담아 처리한다.
그래서 for loop 을 돌며 client 에게 메시지를 전달하는 로직을 구현했다.
# 중략
for client in self.clients:
data = json.dumps({self.stock_code: stock_price})
self.loop.add_callback(lambda client:client.write_message(data))
# time.sleep(0.0000001)
self.clients 는 웹소켓에 접속한 클라이언트이다.
다음과 같은 코드를 돌렸을 때 맨 마지막에 접속한 클라이언트에게만 메시지를 보내는 버그가 발생했다.
그런데 아주 임시방편으로 아주 짧은 시간동안 print() 함수를 실행했더니 또 작동하는 것이였다.
왜 그런지 고민끝에 이유를 발견했다.
다음 예제를 보자.
funcs = []
for i in range(5):
funcs.append(lambda : print(i))
# i 값은 이 때 4이다.
for f in funcs:
f()
결과는 4만 다섯번 출력된다. 이유가 뭘까?
바로 lambda 가 evaluate 되서야 비로서 코드가 생성되기 때문에 for loop에서 마지막 값인 4가 출력되는 것이다.
그리고 time.sleep 함수가 실행됐을 때 제대로 작동하는 것 처럼 보였던 이유는, add_callback()
에 함수가 담기고 나서 time.sleep()
이 실행되는 동안 add_callback()
에 들어있는 함수가 해당 for loop 안에서 실행이 될 준비가 되었기 때문이였다.
하지만 time.sleep()
이 없었을 떈 for loop 가 너무 빨리 돌아버려서 loop 이 끝나버린 뒤 함수가 실행준비가 되었기 때문에 마지막 client 에게만 데이터가 간 것이였다.
그렇다면 이런 이슈를 해결하기 위해 우리는 어떻게 할 수 있을까?
함수가 생성될 때 evaluate 되는 고차함수 partial
을 쓰면 된다.
from functools import partial
funcs = []
for i in range(5):
funcs.append(partial(print, i))
for f in funcs:
f()
결과는 차례대로 0 1 2 3 4 가 출력된다.
하지만 여기서 방법이 끝이아니다.
lambda 의 default argument 를 사용하는 방법이 있다.
Using a default argument works because default arguments are evaluated when the function is created, not when it is called.
default args 를 사용하면 해당 args 는 함수가 실행될 때가 아니라 생성될 때 evaluate 되기 때문에 이 방법도 해답이 될 수 있다.
중요한 것은 lambda 의 변수가 evaluate 되는 시점이다.
우리가 이 문제를 해결하기 위한 최선의 선택은 lambda 에 들어가는 함수가 얼마나 복잡한지에 따라 결정되는 것 같다.