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

Для создания переносимых многопоточных приложений предлагаю воспользоваться библиотекой 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;
}


Буду рад, если кому-то статья окажется полезной.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 61

    +9
    Я думаю я тут выскажу основную идею всех последующих комментов.
    А почему не Кьют?
      +5
      А почему куте? На нем, что свет клином сошелся?
        +4
        ну в рамках гтк против кьюта, да, сошелся. Я честно не вижу преимуществ у гтк перед кьютом
          +2
          Я сам не питаю любви к гтк, мне больше по душе wxWidgets. А конкретно по вашему замечанию, то у куте хорошая документация, много примеров лишний раз можно его и не упоминать.
        +11
        Меня на самом деле волновал похожий вопрос но в более глобальном смысле, почему не C++?
        Представленные здесь примеры, это не low level в котором важна скорость или требуется супер быстрая работа с сырыми данными (вопрос о том что С++ в большинстве случаев сделает все тоже самое, ничуть не хуже и не медленнее пока оставим в стороне), это крайне высокоуровневый код для работы с потоками и UI. Так зачем же себя мучить и оказываться от удобной реализации ООП парадигмы в лице С++? Зачем пародировать объекты с помощью структур и методов с монстроподобными названиями?
          +7
          А я бы спросил — почему не boost? Тут вам и кросс-платформенность, и многопоточность, и очень хорошее апи, и частые релизы новых версий.
          Общий недостаток Qt и Gtk- это фреймворки, а когда нужно что-то не столь тяжелое буст имхо подходит лучше.
          Ну а уж если хотите потоки на чистом си то pthread вам в помощь. Для винды есть замечательный порт pthread-win32 а для других платформ они доступны нативно.
            +2
            ну буст отличается тем, что там нет гуя. То есть его тут не очень корретктно притягивать. Буст кстати неплохо сочетается с кьютом в умелых руках
              +4
              Ну так и топик не про гуи. При прочих равных буст лучше кьюта, а чистый с++ лучше буста. Поэтому в тему топика — с++11 и никаких глибов не надо.
                0
                ну честно говоря подумать про использование гтк без гуя мне как то и в голову не приходило. Это был бы очень странный шаг, не правда ли?
                  +2
                  Мне в голову не приходит писать консольные приложения на кьюте, но их от этого писать не перестают. У каждого свои задачи. И автор писал конкретно про потоки, про гуи он не писал
                    0
                    Glib это не GTK, а вполне самостоятельная библиотека, например консольный midnight commander её использует.
              • UFO just landed and posted this here
                  +1
                  Ну вам же не 50 метров с собой таскать придется ;) нашли в чем буст упрекнуть
                  • UFO just landed and posted this here
                      +1
                      А какие аргументы за то, чтобы класть буст в репозиторий?
                      • UFO just landed and posted this here
                          +1
                          Пусть девелоперы сами ставят. Apt-get install boost.
                    • UFO just landed and posted this here
                      +1
                      для буста есть bcp, если нужно вырезать что-то конкретное
                        0
                        Буст хорош модульностью — так для библиотеки с потоками вам потребуется собрать всего две либы — boost.thread и boost.system. Если же позже захотите добавить туда сокеты — то для boost.asio больше ничего собирать не придется. Все это добро будет выражаться в одном мегабайте библиотек. Впрочем, никто не заставлять таскать их с собой, когда можно собрать их как статические либы.
                    0
                    У меня, лично, сложилась чёткая позиция, что некоторые части QT пока малопригодны для интегрирования в кросс-платформенное ПО, но исключительно из-за проблем сообщества, там года два сейчас уйдёт на чистку и верификацию кода, а потом можно и пользоваться. Это только вопрос времени, но уже чётко были выделены ниши и для GTK, и для QT. Лично мне удобнее с GTK. Это точно так же, как Си / Си++: споры бесполезны, а знать надо и то и другое.
                      0
                      лолшто? Можно услышать какие именно части?
                      Или разговор идет про части QuickTime?
                        0
                        Не сложилось у меня, в своё время с QTsql*, для корректной сборки с близкими версиями glibc пришлось перекапывать исходники. Да и с QTcore, qtGUI проблем хватает, особенно, если работать с shared_lib, а не тупо вставлять в приложение для «кросс-платформенности». Кстати, тут тоже по сравнению c GTK сложностей больше (используя GTK, с mingw проще работать). Ну и память, память, память… Но всё же, с qt некоторые вещи можно сделать проще и быстрее.
                          +2
                          знаете, вы один из тех немногих у кого постоянно что то не так с кьютом. Сколько им не пользуюсь никогда проблем не было. Ни под виндой, ни под линуксом, ни под маком, ни даже под линуксом на спарке
                            0
                            Нет, не постоянно, но было чаще, чем в GTK. Возможно это из-за малого опыта использования. Если бы я специализировался на GUI, то говорил бы, возможно, другое. Попытайтесь переписать статью, использовав QT… Вот тогда можно будет говорить о каком-то сравнении, а так это не true холивар, а словоблудие.
                            И из чистого опыта общения: Вы, наверное, зная (возможно очень большую часть) C++ даже не пробовали писать на чистом Cи? А то есть такая тенденция))
                              +1
                              под кьют эту статью переписывать смысла нет. Это все разжевано в ассистанте.

                              Ну в свое время писал на чистом си, в том числе и в kernel-space
                                +2
                                и перестаньте уже постоянно упоминать QuickTime
                                Кьют пишется Qt
                          0
                          Все части qt вполне пригодны. Они не лучшие в своем классе, но вполне пригодны.
                            0
                            Они сейчас сильно перекапываются. Можно, конечно использовать старые версии библиотек, но такой подход совсем не поможет в перспективе пары лет.
                              0
                              Имеется ввиду, что работа будет с shared библиотеками, из репов дистрибутива.
                        +2
                        Внимание: Повторный вызов g_mutex_lock() внутри уже заблокированного мьютекса не заблокирует программу и будет просто проигнорирован, что защищает от повторных блокировок.

                        цитата из документации:
                        void g_mutex_lock(GMutex *mutex);

                        Locks mutex. If mutex is already locked by another thread, the current thread will block until mutex is unlocked by the other thread.

                        Note
                        GMutex is neither guaranteed to be recursive nor to be non-recursive. As such, calling g_mutex_lock() on a GMutex that has already been locked by the same thread results in undefined behaviour (including but not limited to deadlocks).
                          0
                          На практике происходит так: Если в одном и том же потоке заблокировать мьютекс через g_mutex_lock(), то повторные g_mutex_lock() в этом же потоке не приведут к блокировке. Другое дело, что потом придётся два раза разблокировать мьютекс. То есть вложенные мьютескы реально работают, но раз документация говорит, что это приводит к неопределённым результатам, видимо стоит избегать подобных ситуаций.
                            0
                            Это от платформы зависит… так что они верно пишут про неопределенное поведение. Так что мьютексы могут получиться как рекурсивными, так и нет.

                            Если разработка под заранее известные платформы, как это обычно бывает, то не страшно — можно заранее определиться с поведением, и защитить юнит тестами.

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

                          Рекомедую автору ознакомиться с темой condition variables, а то так сильно плавать в теме некрасиво.
                            0
                            А лучше, вместо троллинга(указание ошибки или псевдоошибки без уточнения верного ответа) написать ответ прямо в теме, что бы люди, которых заинтересовало, но которые еще не достаточно много знают, смогли бы найти ответ в комментариях. Что бы иметь общее представление о сути процесса и знали куда копать для более глубокого понимания.
                              0
                              а разве хабр не место для упражнений в троллинге? Чорт, а я то думал...
                                +5
                                Для более глубокого понимания нужно копать по ключевым словам condition variable — это же очевидно)

                                Если вам так нужен мой пересказ текстбука – пожалуйста. В канонической системе три составляюшие: (1) мьютекс, (2) condition variable и (3) объект, целостность которого защищает мьютекс. Condition variable сигнализирует об изменении состояния этого объекта.

                                Если нам нужно дождаться, когда объект примет определенное состояние, делается это следующим образом. Перво-наперво лочим мьютекс, и проверям – может быть объект уже находится в нужном состоянии? Если нет, переходим к ожиданию сигнализации condition variable (мьютекс отпускается автоматически). Когда другой тред меняет состояние объекта, он также сперва лочит мьютекс, затем обновляет состояние объекта, затем сигналит condition variable, и наконец отпускает мьютекс. Первый тред просыпается, захватывает мьютекс, и проверяет новое состояние объекта. Если объект все еще не находится в нужном состоянии, повторяем все сначала.

                                Особое внимание стоит обратить на следующие моменты.
                                1. Мы всегда начинаем с проверки текущего состояния объекта. Поэтому не страшно, что мы потеряли все «сигнализации» condition variable, которые были до захвата мьютекса перед проверкой состояния объекта.
                                2. Когда мы переходим к ожиданию на condition variable, мьютекс отпускается автоматически. Важно, что это происходит атомарно — промежутка, когда мы уже отпустили мьютекс, но еще не ждем сигнализации condition variable, попросту нет.

                                А если бы был, в этот промежуток могли бы вклиниться другие треды — захватить мьютекс, изменить состояние объекта и просигналить, а мы бы ничего не заметили, потому что еще не перешли к ожиданию сигнализации.
                                  +2
                                  Контрольные вопросы аудитории:
                                  1) Можно ли использовать несколько condition variables в связке с единственным мьютексом?
                                  2) Можно ли использовать один condition variable в связке с несколькими мьютексами?
                                    +1
                                    1. да (не нашел в вашем пояснении запрета)
                                    2. нет (атомарно отпустить несколько мьютексов проблематичным мне видится)

                                    А вообще я здесь случайно, буквы знакомые ищу :)
                                      +2
                                      Все верно)
                                0
                                А разве не так происходит?
                                Вот описание из документации: Atomically releases mutex and waits until cond is signalled.
                                А condition variables можно и не использовать.
                                  +1
                                  Сравните ваше описание и мое.
                                    –1
                                    condition variables — дополнительное средство синхронизации, на случай пропуска сигнала перед началом ожидания. Из за того что condition variables не были рассмотрены в статье, не значит что там что-то написано неправильно.
                                      +1
                                      Ну вот вы опять фигню написали.
                                        0
                                        Что конкретно вы считаете неправильным? В противном случае это просто ничем не обоснованная критика. Может я чего-то и не понимаю, но все примеры были проверены практически.
                                          +1
                                          «condition variables — дополнительное средство синхронизации, на случай пропуска сигнала перед началом ожидания» — вот это

                                          GCond*, несправедливо обозванная вами «сигналом», это и есть condition variable.
                                            0
                                            Ясно, я то считал под condition variable — это переменная, изменение которой защищено мьютесом. Обычно g_cond_signal()/g_cond_wait() используется в связке с какой-нибудь переменной, но её может и не быть.

                                            GCond*, несправедливо обозванная вами «сигналом», это и есть condition variable.
                                            Так как нет перевода для средства синхронизации, вот и придумал более менее близкое по действию.
                                0
                                Мне всегда было любопытно — если в GTK чтобы отправить сообщение из рабочего потока в GUI штатным способом является вызвать «gtk_threads_add_idle()» или «gtk_threads_add_timeout()», то каков штатный способ чтобы отправить сообщение из GUI в рабочий поток? Потому как пересоздавать потоки под каждый чих это не по феншую, да и для ряда задач нужно последовательное выполнение команд в одном и том же потоке.
                                  0
                                  К GUI можно обращаться из нескольких потоков одновременно, достаточно обернуть код в gdk_threads_enter() / gdk_threads_leave(). Это позволяет избегать создания лишних функций для таймеров.
                                  0
                                  заверните код в тег (source lang=«ваш_язык») (/source)
                                  вместо круглых скобок конечно же "<>". просто так парсер ест
                                    +2
                                    &lt; и &gt; спасает:
                                    <source lang="ваш_язык"> </source>
                                    ничего не съедено ;)
                                    +1
                                    И все-таки нафига всё это (именно в плане кросплатформенных потоков) с новым С++11?
                                      0
                                      Тут речь о Си. Без плюсов.
                                      0
                                      Интересно.
                                      А каковы требования? Под ARM или freescale gnu-linux-gcc это скомпилирует? И код не придется переписывать под win64?
                                        0
                                        Если гтк поддерживает нужную платформу, то скомпилируется, кросс-платформ же
                                          0
                                          А gtk поддерживает win64?
                                            0
                                            Конечно, исходные коды ведь доступны, поэтому можно сделать порт на любую платформу.
                                            Бинарники GTK+ для win64: ftp.acc.umu.se/pub/gnome/binaries/win64/gtk+
                                          0
                                          > Под ARM или freescale gnu-linux-gcc это скомпилирует?

                                          Подозреваю, что намного проще в этом случае будет стандартными средствами воспользоваться (pthreads).
                                            0
                                            подозреваю, что я не случайно спросил про win вообще и про win64 в частности.
                                            Нет, порт pthread под win существует, но даже в 32 битовой версии ошибок там есть столько, что это не вариант ;)
                                              0
                                              Так насчет win вопростов-то и не было!
                                          +2
                                          Попытаюсь ответить на подобные вопросы:
                                          А почему не Кьют?
                                          Меня на самом деле волновал похожий вопрос но в более глобальном смысле, почему не C++?
                                          А я бы спросил — почему не boost?

                                          Целью статьи не было сравнение способов написания многопоточных кросс-платформанных приложений. Я предложил один из вариантов, который использую сам.
                                          Несомненно, в Qt есть свои средства, и глупо было бы в Qt-шное приложение встраивать Glib, для С++ возможно больше подойдёт boost, но есть люди, которые предпочитают писать на чистом Си, такие как я, вот для них в первую очередь и предназначена эта статья.

                                          Only users with full accounts can post comments. Log in, please.