Кросс-платформенные многопоточные приложения

в 12:31, , рубрики: c language, cross-platform, GTK+, linux, threads, Программирование, метки: , , , , ,

Для создания переносимых многопоточных приложений предлагаю воспользоваться библиотекой Glib.
Glib — это достаточно большая кросс-платформенная библиотека, которая, кроме потоков, включает в себя поддержку интернационализации, работу со строками, массивами, файлами, таймерами, и много чего другого, вплоть до XML парсера и поддерки .ini конфигурационных файлов.
В тоже время эта библиотека достаточно маленькая и почти не имеет зависимостей, что позволяет без особых проблем включать её в Windows-проекты, а в unix-подобных системах Glib уже есть.

Рассматриваемый вариант в первую очередь подойдёт для кросс-платформенных консольных и графических GTK приложений.

Программирование потоков

Для использования потоков в приложении первым делом нужно выполнить функцию инициализации:
g_thread_init(NULL);

Создание нового потока

GThread* g_thread_create (GThreadFunc func, gpointer data, gboolean joinable, GError **error);
Описание параметров:
func – потоковая функция с прототипом вида: gpointer GThreadFunc (gpointer data);
data – указатель на дополнительные данные передаваемые в поток, может быть NULL.
joinable — можно ли будет ожидать завершение потока через g_thread_join()
error — код и описание ошибки при создании потока, может быть NULL.

Узнать GThread текущего потока

GThread* g_thread_self(void);

Сменить приоритет потока:

void g_thread_set_priority(GThread *thread_id, GThreadPriority priority);
Описание параметров:
thread_id — структура, полученная при создании потока через g_thread_create()
priority — приоритет, возможные варианты в порядке возрастания:
G_THREAD_PRIORITY_LOW
G_THREAD_PRIORITY_NORMAL
G_THREAD_PRIORITY_HIGH
G_THREAD_PRIORITY_URGENT

Ждать завершения другого потока:

gpointer g_thread_join (GThread *thread_id);
Описание параметров:
thread_id — структура, полученная при создании потока через g_thread_create()
Функция дождётся завершения потока, только если при его создании joinable=TRUE, и вернёт код завершения потокой функции,
При создании потока с joinable=FALSE, функция сразу завершит своё выполнение и вернёт 0.

Пример:

// потоковая функция;
gpointer thread_func(gpointer data)
{
printf("внутри потокаn");
return 0;
}

int main (int argc, char *argv[])
{
// инициализация потоков
g_thread_init(NULL);
// создаём дополнительный поток
GThread *thread_id = g_thread_create(thread_func,NULL,TRUE,NULL);
//ждём завершения потока
g_thread_join(thread_id);
printf("дождались конца потокаn");
return 0;
}

Синхронизация потоков

Рассмотрим мьютексы (mutex) и сигналы (condition). Существуют ещё другие специфические методы, вроде блокировки битов, но практически все из них можно заменить мьютексами и/или сигналами.

Mutex

Мьютексы служат для защиты кода от совместного доступа. Это аналог критической секции для Windows.
Как только один поток захватил мьютекс, другие потоки, при попытке захвата мьютекса, будут ждать его освобождения, следовательно только один поток имеет доступ к коду внутри мьютекса в любой момент времени.

Создание мьютекса:

GMutex* g_mutex_new();

Блокировка мьютекса:

void g_mutex_lock(GMutex *mutex);

Pазблокировка мьютекса:

void g_mutex_unlock(GMutex *mutex);

Попытка блокировки мьютекса:

gboolean g_mutex_trylock(GMutex *mutex);
Если мьютекс свободен, то он блокируется и функция возвращает TRUE, иначе функция сразу завершает свою работу и возвращает FALSE.

Удаление мьютекса:

void g_mutex_free(GMutex *mutex);

Пример, в котором функция critical_func() с защищаемым от совместного доступа кодом может быть вызвана одновременно из основного и дополнительного потоков:
void critical_func(GMutex *mutex)
{
g_mutex_lock (mutex);
// какие-то вычисления
...
g_mutex_unlock (mutex);
}

// потоковая функция;
gpointer thread_func(gpointer data)
{
GMutex *mutex = (GMutex*)data;
critical_func(mutex);
return 0;
}

int main (int argc, char *argv[])
{
GMutex *mutex = g_mutex_new();
// создаём дополнительный поток
GThread *thread_id = g_thread_create(thread_func,mutex,TRUE,NULL);
critical_func(mutex);
//ждём завершения потока
g_thread_join(thread_id);
return 0;
}

Внимание: Повторный вызов g_mutex_lock() внутри уже заблокированного мьютекса не заблокирует программу и будет просто проигнорирован, что защищает от повторных блокировок.

Condition

Condition переводится как Условие, но по технике дела больше подходит слово Сигнал. Это аналог Событий для Windows, но не полный: Если сигнал послан раньше, чем его начали ждать, то он уйдёт в пустоту и ждать его будет бесполезно. Поэтому здесь нет понятия сброса события, как в Windows.

Создание сигнала (условия):

GCond* g_cond_new(void);

Послать сигнал:

void g_cond_signal (GCond *cond);

Бесконечно ждать сигнала:

void g_cond_wait (GCond *cond, GMutex *mutex);

Ждать сигнала не более заданного времени:

gboolean g_cond_timed_wait (GCond *cond, GMutex *mutex, GTimeVal *abs_time);

Пример

Пример демонстрирует применение сигналов для ожидания начала запуска потока после его создания.
GMutex *mutex = NULL;
GCond *cond = NULL;
// потоковая функция;
gpointer thread_func(gpointer data)
{
// инициализация потока
// ...
// Послать сигнал что инициализация завершена
g_mutex_lock (mutex);
g_cond_signal(cond);
g_mutex_unlock(mutex);// сигнал установится только после покидания мьютекса
// ...
return 0;
}

// Создание потока и ожидание его запуска с помощью условий
int main (int argc, char *argv[])
{
GTimeVal timeval;
gboolean return_val;
// инициализация потоков
g_thread_init(NULL);
// создать мьютекс и сигнал(условие)
mutex = g_mutex_new();
cond = g_cond_new();
// мьютекс заблокирован раньше создания потока, чтобы сигнал не был послан раньше начала его ожидания
g_mutex_lock(mutex);
// запускаем поток
g_thread_create( thread_func,NULL,TRUE,NULL);
// задаём время ожидания
g_get_current_time(&timeval);// узнаём текущее время
g_time_val_add(&timeval,G_USEC_PER_SEC);// добавляем секунду
// ждём запуска потока не более секунды
return_val = g_cond_timed_wait(cond,mutex,&timeval);// внутри mutex будет временно разблокирован, чтобы можно было послать сигнал также внутри
if(!return_val)
printf("так и не дождались создания потокаn");
g_mutex_unlock(mutex);
return 0;
}

Внимание: Ожидание сигнала должно быть внутри заблокированного мьютекса, и посылать сигнал тоже рекомендуется внутри заблокированного мьютекса, хотя это и не обязательно.
Во время ожидания мьютекс временно разблокируется для того чтобы можно было зайти внутрь этого же мьютекса из другого потока и послать сигнал.

Где взять Glib

Linux:

Glib есть в каждом дистрибутиве Linux, проверить это можно следующим образом:
pkg-config --cflags glib-2.0

Windows:

Требуется несколько dll библиотек: libgthread-2.0-0.dll, libglib-2.0-0.dll и intl.dll.
Для Glib 2.28.8 эти файлы занимают 1,4 Мб (в архиве в 2 раза меньше)
Готовую библиотеку с оригинальной документацией и без лишних файлов локализации можно взять здесь.

Последнюю полную версию Glib всегда можно скачать по этой ссылке:
http://ftp.acc.umu.se/pub/gnome/binaries/win32/glib
При этом понадобится ещё файл intl.dll, берётся отсюда:
http://ftp.acc.umu.se/pub/gnome/binaries/win32/dependencies/gettext-runtime_0.18.1.1-2_win32.zip
Кстати, существует 64-разрядная версия библиотеки:
http://ftp.acc.umu.se/pub/gnome/binaries/win64/glib

MacOS:

Как именно обстоят дела в MacOS я не знаю, но Glib там тоже есть.

Подключение к проекту

Подключается Glib библиотека одним h-файлом:
#include <glib.h>

Linux и mingw32:

Узнать пути до h-файлов:
pkg-config --cflags gthread-2.0
Узнать подключаемые библиотеки:
pkg-config --libs gthread-2.0

Пример make-файла:
CC=gcc
DEBUG = -g3
RELEASE = -o3
FLAGS := $(shell pkg-config --cflags gthread-2.0)
LIBS := $(shell pkg-config --libs gthread-2.0)
SOURCES= test_app.c
test_app : $(SOURCES)
$(CC) -o $@ $(SOURCES) $(DEBUG) $(FLAGS) $(LIBS)
all : test_app
clean:
rm -f *.o

Windows:

Прописать в проекте пути, где находятся h-файлы:
include/glib-2.0
lib/glib-2.0/include
Подкючить к проекту два lib-файла:
gthread-2.0.lib
glib-2.0.lib.

Особенности применения с GTK

В многопоточном Gtk приложении обращение к Gtk или Gdk функциям одновременно из нескольких потоков может привести к непредсказуемым результатам, обычно — к искажению графики или зависанию приложения.
Для обхода этой неприятной ситуации нужно размещать код во всех потоках с обращениями к Gtk и Gdk функциям внутри специальной критической секции, т.е. ограничить код следующими функциями:
// вход в критическую секцию
gdk_threads_enter();
// выход из критической секции
gdk_threads_leave();

Пример использования:

// потоковая функция;
gpointer thread_func(gpointer data)
{
// вход в критическую секцию
gdk_threads_enter();
gtk_label_set_text(label,"новый текст метки");
// выход из критической секции
gdk_threads_leave();
return 0;
}

int main (int argc, char *argv[])
{
GtkWidget *window;
// инициализация потоков
if(!g_thread_supported())
{
g_thread_init(NULL);
// чтобы работали функцииgdk_threads_enter() и gdk_threads_leave()
gdk_threads_init();
}
// вход в критическую секцию
gdk_threads_enter ();
gtk_init (&argc, &argv);
// Главное окно создаем;
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
// создание пользовательского интерфейса
// ...

// создаём дополнительный поток
g_thread_create(thread_func,NULL,FALSE,NULL);
gtk_main();
// выход из критической секции
gdk_threads_leave ();
return 0;
}

Буду рад, если кому-то статья окажется полезной.

Автор: dodo

Поделиться

* - обязательные к заполнению поля