Philip Withnall
philip.withnall@collabora.co.uk 2015
메인 스레드 외부의 처리를 작업 스레드로 옮깁니다
조성호
shcho@gnome.org 2016-2018.

# 스레드 처리

#

        요약

          가능한 모든 경우에 스레드를 사용하지 마십시오().
          스레드를 사용해야하면, GTask 또는 GThreadPool를 사용하고, 가능한 한 스레드 처리한 코드를 격리하십시오().
          GThread를 직접 사용한다면 g_thread_join() 함수를 사용하여 스레드 자원 누수를 막으십시오().
          스레드를 사용할 경우 어떤 코드를 실행하든 GMainContext를 주의깊게 다루십시오. 잘못된 컨텍스트에서 코드를 실행하면 레이스 컨티션을 유발하거나 메인 루프가 멈출 수 있습니다().

# 스레드 처리를 활용할 경우

When writing projects using GLib, the default approach should be to never use threads. Instead, make proper use of the GLib main context which, through the use of asynchronous operations, allows most blocking I/O operations to continue in the background while the main context continues to process other events. Analysis, review and debugging of threaded code becomes very hard, very quickly.

GLib 코드에서 호출해야 하는 함수의 동작을 가로 막는 외부 라이브러리를 활용할 경우라면 스레드를 활용해야합니다. 라이브러리에서 동작을 가로막지 않는 대안책을 마련하거나`poll()` 루프와 잘 동작하는 수단이 있다면 취향에 따라 사용해야합니다. 동작을 가로 막는 함수를 꼭 사용해야 한다면, 작업 스레드에서 동작을 가로 막는 처리를 실행하는 `GAsyncResult` 방식 일반 GLib 비동기 함수로 변환하도록 씬 래퍼를 작성해야합니다.

예제:

int some_blocking_function (void *param1, void *param2);

위 코드를 다음과 같이 래핑해야합니다:

void some_blocking_function_async (void *param1, void *param2, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); int some_blocking_function_finish (GAsyncResult *result, GError **error);

그리고 다음과 같은 구현체를 넣으십시오:

/* Closure for the call’s parameters. */ typedef struct { void *param1; void *param2; } SomeBlockingFunctionData;

static void some_blocking_function_data_free (SomeBlockingFunctionData *data) { free_param (data->param1); free_param (data->param2);

g_free (data); }

static void some_blocking_function_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { SomeBlockingFunctionData *data = task_data; int retval;

/* Handle cancellation. */ if (g_task_return_error_if_cancelled (task)) { return; }

/* Run the blocking function. */ retval = some_blocking_function (data->param1, data->param2); g_task_return_int (task, retval); }

void some_blocking_function_async (void *param1, void *param2, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GTask *task = NULL; /* owned */ SomeBlockingFunctionData *data = NULL; /* owned */

g_return_if_fail (validate_param (param1)); g_return_if_fail (validate_param (param2)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

task = g_task_new (NULL, cancellable, callback, user_data); g_task_set_source_tag (task, some_blocking_function_async);

/* Cancellation should be handled manually using mechanisms specific to * some_blocking_function(). */ g_task_set_return_on_cancel (task, FALSE);

/* Set up a closure containing the call’s parameters. Copy them to avoid * locking issues between the calling thread and the worker thread. */ data = g_new0 (SomeBlockingFunctionData, 1); data->param1 = copy_param (param1); data->param2 = copy_param (param2);

g_task_set_task_data (task, data, some_blocking_function_data_free);

/* Run the task in a worker thread and return immediately while that continues * in the background. When it’s done it will call @callback in the current * thread default main context. */ g_task_run_in_thread (task, some_blocking_function_thread_cb);

g_object_unref (task); }

int some_blocking_function_finish (GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, some_blocking_function_async), -1); g_return_val_if_fail (error == NULL || *error == NULL, -1);

return g_task_propagate_int (G_TASK (result), error); }

자세한 내용은 `GAsyncResult` 문서를 참고하십시오. 작업 스레드를 구현하는 간단한 방법은 `GTask` 와 `g_task_run_in_thread()` 함수를 활용하는 방법입니다(추가 참고: ).

# 스레드 활용

작업 스레드를 작성할 때 `GTask`가 적절한 수단이 아니라면, 좀 더 저수준의 접근 방식을 활용해야합니다. 스레드 코드로 하여금 실행 시간에 예기치 못한 버그가 나타난다거나, 데드락이 갑자기 걸린다거나, 자원을 갑자기 많이 소모한다거나, 프로그램이 끝나는 일은 식은죽 먹기니 매우 신중하게 고려해야 합니다.

스레드 코드 작성 완벽 설명서는 이 문서의 범위를 벗어나지만, 스레드 코드 작성시 잠재 버그를 줄이려 따를 몇가지 지침서가 있습니다. 무엇보다 우선하는 원칙은 스레드로 영향을 줄 수 있는 코드 및 데이터 양을 줄이는 방식입니다. 예를 들면, 스레드 수를 줄이거나, 작업 스레드 구현의 복잡도를 줄이거나, 스레드간 공유 데이터 양을 줄이는 방법입니다.

가능하면 직접 `GThread`를 만들기보다는 `GThreadPool`을 사용핫비시오. `GThreadPool`에서는 작업 큐, 가동 스레드 수 제한, 종결 스레드 자동 병합을 지원하여, 스레드가 어디론가 새 나갈 일이 없습니다.

`GThreadPool`을 활용할 수 없다면(드문 경우):

스레드를 만들려면 `g_thread_new()` 함수 대신 `g_thread_try_new()` 함수를 활용하여 프로그램의 상태를 알 수 없는 상황에서 갑자기 멈추게 하기 보단 스레드 외적 영역에서의 시스템 동작상 나타나는 오류를 탁월하게 처리할 수 있게 하십시오.

스레드 자원의 방치를 막으려면 `g_thread_join()` 함수를 사용하여 분명하게 스레드를 병합하십시오.

스레드간 데이터를 전달할 때, 뮤텍스의 동작을 직접 잠그기보다는 메시지 전달 기능을 활용하십시오. `GThreadPool`에서는 `g_thread_pool_push()` 함수로 메시지 전달 기능을 지원합니다.

뮤텍스를 활용해야 한다면:

스레드 코드를 최대한 격리하고, 클래스에서 뮤텍스를 자체 용도로 유지하며, 매우 한정적인 클래스 구성원만 다루십시오.

모든 뮤텍스는 뮤텍스에 보호 접근할 기타 구조체 또는 변수를 나타내는 선언부에 분명하게 설명을 달아두어야합니다. 이와 비슷하게, 해당 뮤텍스의 변수도 뮤텍스에서만 접근해야 한다고 설명을 달아두어야합니다.

메인 컨텍스트와 스레드간 상호 작용을 조심스럽게 취급하십시오. 예를 들면, `g_timeout_add_seconds()` 함수는, 현재 스레드에서 불필요하며, 메인 스레드에서 실행할 전역 메인 컨텍스트의 실행 제한 시간을 설정합니다. 이 부분을 잘못 설정하면 메인 스레드에서 실행한 작업 스레드가 갑자기 끝날 수 있음을 의미합니다(추가 참조: ).

# 디버깅

스레드 문제 디버깅은 상당히 까다로운데 재현하기도 어려울 뿐더러 왜 인지도 이유를 밝히기 어렵기 때문입니다. 이게 바로 제일 먼저 스레드를 활용할 때 피해야 할 가장 큰 이유중 하나입니다.

그러나, 스레드 문제가 터지면 Valgrind의 drd 도구와 helgrind 도구가 도움될 수 있습니다.