2011년 7월 10일 일요일

GLib 메인루프 이용하기

GLib API를 이용한 멀티쓰레드 프로그래밍에서 비동기 메시지 큐를 이용하는 방법은 지난 포스트에서 설명한 적이 있는데, 이번에는 애플 GCD의 libdispatch와 비교되는 GLib의 메인루프를 이용하는 방법을 정리해 보았습니다. 이 방법은 어떤 관점에서 보면 더 쉽고, 이미 많은 기능이 기본적으로 지원되기 때문에 몇몇 경우를 제외하면 더 좋은 방법입니다. 다만 API 사용법을 이해하기가 처음에 조금 까다롭다는 점이 걸림돌입니다.

일반적으로 GLib / GTK 어플리케이션은 메인 쓰레드에서 실행되는 메인 이벤트 루프 기반에서 동작합니다. 키보드 / 마우스 이벤트 처리, 화면 표시, 사용자가 등록한 Idle / Timeout 함수 처리 등이 모두 이 메인 이벤트 루프에서 처리됩니다. 그런데 이 메인 이벤트 루프라는 건 마냥 개념적인게 아니라, 실제로 GMainLoop 객체를 기반으로 동작합니다. 그런데 g_main_loop_*() 계열 함수를 살펴보면 몇 개 안됩니다. 루프 객체를 생성하고, 참조하고, 해제하고, 돌리고[g_main_loop_run()], 종료하고[g_main_loop_quit()], 돌아가는 중인지 확인하기 등의 함수만 있습니다. 아, 하나 더 있군요. 객체를 생성할때 전달하는 GMainContext 객체를 얻어오는 함수[g_main_loop_get_context()]가 있군요.

모든 GMainLoop는 하나의 GMainContext와 함께 사용됩니다. GMainContext 객체는 실행할 소스[GSource] 목록을 관리합니다. 소스는 파일, 파이프, 소켓 등의 디스크립터를 기반으로 한 이벤트 소스일 수도 있고, Idle / Timeout 등과 같은 시간 소스일 수도 있습니다. 컨텍스트는 실행 소스 각각을 검사해서 원하는 이벤트가 발생했는지, 아니면 실행할 시간이 되었는지를 판단해 등록한 콜백함수를 호출합니다. 참고로, 메인 쓰레드에서 동작하기 위한 컨텍스트[g_main_context_default()]는 기본적으로 제공합니다. 이 기본 컨텍스트는 gtk_main() 함수가 사용하는 것은 물론, g_idle_add(), g_timeout_add() 등과 같은 함수도 이 기본 컨텍스트를 사용합니다.

아무튼 조금 더 구체적이고 자세한 내용은 공식 문서를 참고하시고, 이제 이를 이용한 멀티쓰레드 프로그래밍을 해보겠습니다. 말이 길었으니 코드를 먼저 보여드리겠습니다.
#include <glib.h>

static GThread *my_thread;
static GMainLoop *my_loop;

static void
add_idle_to_my_thread (GSourceFunc    func,
gpointer       data)
{
GSource *src;

src = g_idle_source_new ();
g_source_set_callback (src, func, data, NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

static void
add_timeout_to_my_thread (guint          interval,
GSourceFunc    func,
gpointer       data)
{
GSource *src;

src = g_timeout_source_new (interval);
g_source_set_callback (src, func, data, NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

static gpointer
loop_func (gpointer data)
{
GMainLoop *loop = data;

g_main_loop_run (loop);

return NULL;
}

static void
start_my_thread (void)
{
GMainContext *context;

context = g_main_context_new ();
my_loop = g_main_loop_new (context, FALSE);
g_main_context_unref (context);

my_thread = g_thread_create (loop_func, my_loop, TRUE, NULL);
}

static void
stop_my_thread (void)
{
g_main_loop_quit (my_loop);
g_thread_join (my_thread);
g_main_loop_unref (my_loop);
}

함수 먼저 설명하면, start_my_thread() 함수는 쓰레드를 시작하고, stop_my_thread() 함수는 쓰레드를 중지합니다. add_idle_to_my_thread() 함수는 바로 실행되는 Idle 콜백 함수를 추가하고, add_timeout_to_my_thread() 함수는 주기적으로 실행되는 Timeout 콜백 함수를 추가합니다. 마지막 두 함수의 인수는 g_idle_add(), g_timeout_add() 함수와 각각 동일합니다. 따라서, 콜백 함수가 TRUE를 리턴하면 자동으로 반복해서 계속 실행되고, FALSE를 리턴하면 한번만 실행되고 종료합니다.

위 코드의 핵심은 GMainContext 객체를 만들고 이를 기반으로 GMainLoop 객체를 만든 뒤 별도 쓰레드에서 실행하도록 하는 부분입니다. 그리고, 필요한 모든 작업은 Idle / Timeout 소스 객체를 만들어 컨텍스트에 추가(attach)해서 동작하도록 하는 겁니다. 참고로, 관련 API는 모두 쓰레드에 안전합니다.

물론 위 함수를 조금 더 확장하면 콜백함수가 종료될때 자동으로 호출되는 notify 함수도 등록할 수 있고, 우선순위도 조절할 수 있습니다. 또한 여러 쓰레드를 종류별로 만들어 필요한 쓰레드에게 해당 작업만 전달해도 됩니다. 하지만 그 정도는 응용하는데 별로 어려움이 없을 거라 생각하고 한가지만 더 설명하겠습니다.

예를 들어 네트워크 소켓(socket)을 하나 만들고 이 소켓에 읽을 데이터가 도착했을 경우에만 호출되는 함수를 등록하고 싶은 경우, 다음과 같은 코드를 사용하면 됩니다.
static gboolean
socket_read (GIOChannel *source,
GIOCondition condition,
gpointer data)
{
/* Use g_io_channel_read_chars() to read data... */

return TRUE;
}

static void
add_socket_to_my_thread (gint sock_fd)
{
GIOChannel *channel;
GSource *src;

channel = g_io_channel_unix_new (sock_fd);
src = g_io_create_watch (channel, G_IO_IN);
g_source_set_callback (src,
(GSourceFunc) read_socket,
NULL,
NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

자세한 내용은 위 코드와 비슷하지만 기본 메인 이벤트 루프에서 동작하도록 하는 g_io_add_watch() API 설명 부분을 참고하시기 바랍니다. 어쨌든, 기본적으로 GMainContext 객체는 유닉스 시스템의 폴링(polling) 메카니즘을 사용하기 때문에 이론적으로는 거의 모든 파일 디스크립터를 사용할 수 있습니다. 물론 비슷한 방식으로 윈도우 운영체제에서 이벤트 핸들이나 소켓 핸들도 사용할 수도 있습니다.

글머리에서 적은 것처럼 비동기 메시지 큐를 이용하는 방식보다 아주 약간의 오버헤드는 있겠지만, 훨씬 더 많은 기능을 제공하는 것 같지 않나요?

(이 글은 개인 블로그에 함께 게재되어 있습니다)

댓글 없음:

댓글 쓰기