tkinter에서 쓰레드를 사용하려면 tkinter가 어떻게 동작하는지 어느 정도 이해가 필요합니다.
우선 tkinter 프로그램을 실행하면 메인 쓰레드에서 GUI로 동작하게 될 객체의 mainloop를 호출해야 합니다. 이 호출로 인해 메인 쓰레드에서의 코드는 더 이상 실행되지 않고 멈추게 되면서 새로운 쓰레드가 시작되어 이벤트 루프와 GUI의 update가 계속 진행됩니다. 우리가 보통 프로그램을 실행할 때 보이는 단추나 텍스트 등이 보이면서 사용자의 입력에 반응하는 것이죠.
사용자가 프로그램 닫기 단추를 누르면 이 루프가 종료되면 메인쓰레드의 코드가 다시 진행되게 됩니다. 이 후의 코드가 있다면 실행되겠죠.
프로그램 실행 중에는 버튼이 눌리면 해당 리스너가 호출되고, GUI 요소를 옮기거나 하면 새롭게 GUI를 그려주게 됩니다. GUI프로그램은 이런 형식으로 동작합니다.
이 와중에 새롭게 쓰레드를 사용해야할 경우가 있습니다. 예를 들어 버튼을 누른 경우 채팅 서버에 연결을 하는 경우에는 연결을 하는 동안에도 이벤트 루프와 GUI의 업데이트는 계속 되어야 할 것입니다.
또는 시간을 보여주는 프로그램이라면 1초마다 자동으로 시간이 변해야 합니다. 상식적으로 이런 동작들은 mainloop외에 또 다른 loop에서 동작될 필요가 있습니다.
이 외에도 많은 경우가 있고 GUI 프로그램에서는 쓰레드를 제대로 활용하는 것이 관건입니다.
간단히 1초마다 초가 갱신되는 간단한 프로그램을 만들어 보기 전에 실수하기 쉬운 경우에 대해서 알아보겠습니다.
잘못된 예
이 코드는 draw_timer() 내부에 무한 루프가 있습니다. 타이머를 1초 증가시키는 루프인데 실행을 해보면 프로그램이 실행되는 않는 것 같습니다. 사실 코드는 실행은 되지만 root.mainloop()가 실행되지 않습니다. root.draw_timer()까지만 실행된 후 무한 루프에 빠져버리기 때문입니다.
이를 해결하려면 draw_timer의 호출을 또 다른 루프 즉, 다른 쓰레드에서 실행시킬 필요가 있습니다. 이를 위한 가장 간단한 방법은 after메소드를 사용하는 것입니다.
after 메소드
draw_timer 내부의 루프를 없애고 after메소드를 사용했습니다. after 메소드의 첫 번째 인자는 밀리세컨드, 두 번째 인자는 실행할 함수입니다. 위 코드는 1000밀리세컨드 후에 self.draw_timer를 실행하라는 것입니다.
after 메소드는 독립적인 하나의 쓰레드를 만들어 냅니다. 따라서 이 코드는 draw_timer 메소드가 재귀적으로 호출되는 것이 아니며 메인 쓰레드의 루프를 정지시키지도 않습니다.
쓰레드 사용
타이머의 경우는 after 메소드가 딱 들어맞는 케이스입니다.
그러면 타이머 말고 언제 데이터가 갱신될지 모르는 경우를 생각해 봅시다. 예를들어 이메일의 도착알림이나 특정 시간에 알람을 울리는 경우를 생각할 수 있겠습니다.
이메일이 왔는지 안왔는지에 대한 검사나 특정 시간이 되었는지에 대한 검사를 백그라운드에서 실행하다가 조건이 맞으면 알림을 해줘야 할 겁니다. after메소드를 사용하여 주기적으로 검사를 해줘야 겠지만 다른 쓰레드에서 데이터를 가져오는데는 경쟁관계에 대한 것도 생각해야합니다.
이런 경우 Lock을 걸어 임계영역(critical section)을 만드는 대신에 queue 모듈을 사용할 수도 있습니다. queue는 자료구조 큐와 같은 것이며 쓰레드에 대해서 안정성을 보장해 줍니다. 즉 큐 안에 넣은 데이터는 쓰레드들의 경쟁관계(race condition)에 대해 안전합니다.
다음 예는 메일 알림을 가상적으로 구현한 코드입니다. 외부 쓰레드가 돌아가면서 랜덤한 숫자를 생성하다 조건에 맞는 숫자가 생성되면 임의의 값을 메인 쓰레드로 전달하는 코드입니다. 값을 전달하기 위해 queue를 사용하였습니다.
실행하고 버튼을 누르면 랜덤한 숫자가 랜덤한 시간 간격으로 점점 늘어납니다. 메일함에 메일이 도착하는 것을 가상적으로 구현한 코드입니다.
프로그램을 실행한 후 Start버튼을 누르면 Mailbox 쓰레드를 생성하고 check_Mailbox 메소드를 3초간격으로 실행합니다. Mailbox쓰레드는 1초 간격으로 메일을 생성하는데 1/5확률로 메일을 생성하여 랜덤한 숫자를 넣어줍니다. 숫자가 생성되면 queue에 이 숫자를 넣어줍니다. 이렇게 queue에 데이터가 들어가면 check_Mailbox가 queue를 확인하여 처리하는 구조입니다.