Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Программирование с использованием gtkmm 3. Многопоточные программы

Оригинал: Programming with gtkmm 3
Авторы: Murray Cumming, Bernhard Rieder, Jonathon Jongsma, Ole Laursen, Marko Anastasov, Daniel Elstner, Chris Vine, David King, Pedro Ferreira, Kjell Ahlstedt
Дата публикации: 15 Октября 2013 г.
Перевод: А.Панин
Дата перевода: 10 Апреля 2014 г.

29. Многопоточные программы

29.1. Ограничения

glibmm предоставляет достаточный набор функций для создания потоков, примитивов взаимных исключений (мьютексов), условных переменных и классов для локальных блокировок, необходимых для реализации многопоточных программ с помощью языка программирования C++.

Однако, требуется особая осторожность при разработке многопоточных программ на основе gtkmm, так как библиотека libsigc++, а также сам класс отслеживания sigc::trackable не являются потокобезопасными. Это означает, что ни одно из сложных взаимодействий, происходящих на заднем плане при использовании библиотеки libsigc++, не защищено с помощью взаимного исключения или какого-либо другого примитива синхронизации.1

29.1.1. Правила разработки

Эти обстоятельства приводят к необходимости принятия к сведению множества правил при разработке многопоточных программ на основе gtkmm. Правила приведены ниже, причем следует особо учесть необходимость проявления дополнительной осторожности при наследовании классов от класса отслеживания sigc::trackable, так как результаты работы созданных классов могут быть непредсказуемыми (следует обратить особое внимание на пункты 4 и 5 ниже).
  1. Используйте экземпляр класса обработчика событий Glib::Dispatcher для вызова функций gtkmm из рабочих потоков (более подробно об этом будет сказано в следующем разделе).
  2. Объект сигнала на основе класса sigc::signal должен рассматриваться как объект, принадлежащий создавшему его потоку. Только этот поток должен соединять объект слота на основе класса sigc::slot с объектом сигнала и только этот поток должен генерировать сигналы с помощью метода emit() или использовать operator()() для перегрузки операторов объекта сигнала, а также удалять любой присоединенный объект слота на основе класса sigc::slot. Из этого следует (помимо остальных вещей) то, что любой объект сигнала, предоставленный виджетом из состава gtkmm, должен обслуживаться исключительно в рамках главного потока приложения с графическим интерфейсом и любой объект на основе унаследованного от класса отслеживания sigc::trackable класса, имеющий нестатические методы со ссылками из присоединенных к объекту сигнала слотов, должен уничтожаться из этого потока.
  3. Любой объект соединения на основе класса sigc::connection должен рассматриваться как принадлежащий потоку, в рамках которого был осуществлен вызов метода, возвращающего этот объект типа sigc::connection. Только в рамках этого потока должны вызываться методы данного объекта соединения типа sigc::connection.
  4. Объект слота на основе класса sigc::slot, создаваемый с помощью вызова метода sigc::mem_fun() и ссылающийся на метод унаследованного от класса отслеживания sigc::trackable класса, никогда не должен копироваться в другой поток, а также уничтожаться в рамках того потока, который его не создавал. (Одно из последствий этого ограничения заключается в том, что метод Glib::Threads::Thread::create() не должен вызываться с передачей в качестве аргумента слота, созданного с помощью вызова метода sigc::mem_fun(), представляющего метод такого унаследованного класса. Однако, методу Glib::Threads::Thread::create() можно безопасно передавать объект функции, представляющий такой метод с помощью, скажем, экземпляра класса boost::bind, std::bind в C++11 или лямбда-выражения в C++11.)
  5. В том случае, если класс, лежащий в основе определенного объекта, наследуется от класса отслеживания sigc::trackable, объекты слотов на основе класса sigc::slot, представляющие любой из нестатических методов класса, должны создаваться в рамках одного потока с помощью вызова sigc::mem_fun(). Первый создавший такой слот поток должен рассматриваться как владелец соответствующего объекта, предназначенный для создания дополнительных слотов, ссылающихся на любой из нестатических методов с помощью данной функции, деактивации этих слотов путем разрыва соединения, а также для уничтожения объекта отслеживания.
  6. Хотя библиотека glib и является потокобезопасной сама по себе, любые использующие библиотеку libsigc++ обертки из состава glibmm не являются таковыми. Таким образом, к примеру, вызовы методов для управления циклом обработки событий Glib::SignalIdle::connect(), Glib::SignalIO:connect(), Glib::SignalTimeout::connect(), Glib::SignalTimeout::connect_seconds, а также манипуляции с объектами соединений на основе класса sigc::connection, возвращаемыми в результате вызовов этих методов, должны осуществляться из одного потока, в рамках которого функционирует цикл обработки событий.
    Варианты методов connect*_once(): Glib::SignalIdle::connect_once(), Glib::SignalTimeout::connect_once(), Glib::SignalTimeout::connect_seconds_once() являются потокобезопасными в любых случаях, когда слот не создается с помощью вызова метода sigc::mem_fun(), который представляет метод класса, унаследованного от класса отслеживания sigc::trackable. Эта ситуация аналогична ситуации с вызовом метода Glib::Threads::Thread::create(), описанной в пункте 4.

29.2. Использование класса обработчика событий Glib::Dispatcher

Слоты, соединенные с объектами сигналов на основе класса sigc::signal исполняются в потоке, в рамках которого осуществляется вызов метода emit() или используется operator()() по отношению к объекту сигнала. Объект обработчика событий на основе класса Glib::Dispatcher ведет себя отличным образом: соединенные слоты исполняются в том потоке, в котором объект обработчика событий был создан (в потоке, который должен содержать главный цикл обработки событий glib). В том случае, если объект обработчика событий создается в главном потоке обработки событий графического интерфейса (который также будет главным потоком приема событий), любой рабочий поток сможет с помощью него генерировать сигналы, причем все соединенные слоты будут безопасно исполнять функции gtkmm.

Также существует несколько правил безопасного использования потоков при работе с объектом обработчика событий на основе класса Glib::Dispatcher. Как упоминалось ранее, объект обработчика событий должен создаваться в потоке приема событий (потоке, с помощью главного цикла обработки событий которого будут исполняться соединенные слоты). Обычно это главный поток программы, хотя существует и конструктор класса Glib::Dispatcher, способный принимать объект контекста обработки событий на основе класса Glib::MainContext любого потока, который содержит главный цикл обработки событий. Только поток приема событий может вызывать метод connect() объекта обработчика событий на основе класса Glib::Dispatcher или производить манипуляции с любым связанным объектом соединения на основе класса sigc::connection при отсутствии дополнительного механизма синхронизации. Однако, любой рабочий поток может безопасно генерировать сигналы с помощью объекта обработчика событий на основе класса Glib::Dispatcher без использования любых механизмов блокировки после того, как поток приема событий подсоединит слоты при условии, что этот поток был создан перед запуском рабочего потока (в том случае, если он был создан после запуска рабочего потока, для гарантии видимости потребуются дополнительные механизмы синхронизации).

Помимо того, что соединенные слоты всегда исполняются в рамках потока приема событий, объекты обработчика событий на основе класса Glib::Dispatcher аналогичны объектам sigc::signal<void>. Следовательно, они не позволяют ни передать неограниченное количество аргументов, ни вернуть значение. Лучшим способом передачи неограниченного количества аргументов является потокобезопасная (асинхронная) очередь. Хотя на момент написания этой книги в составе glibmm не имеется реализации подобного механизма, большинство создающих код для работы с множеством потоков разработчиков должно располагать реализацией такой очереди (эти реализации создаются относительно просто, хотя иногда и возникают сложности при комбинировании механизмов безопасного использования потоков и механизмов строгой безопасности генерации исключений).

Сигналы с помощью объекта обработчика событий на основе класса Glib::Dispatcher могут генерироваться как из потока приема событий, так и из рабочих потоков, хотя это и должно делаться в разумных пределах. В Unix-подобных системах объекты обработчиков событий делят единственный неименованный канал, который в теории будет заполняться данными только в очень нагруженной системе при исполнении программы с очень большим количеством объектов обработчиков событий. В том случае, если канал заполнится данными перед тем, как как главный цикл обработки событий потока приема событий получит возможность прочитать из него все данные, а в рамках потока приема событий будет осуществлена попытка генерации нового сигнала с записью данных в канал в текущем состоянии, поток приема событий навсегда заблокируется на операции записи. При генерации событий из потока приема событий, конечно же, может использоваться и обычный объект sigc::signal<void>.

29.3. Пример

Это пример программы с двумя потоками, причем один поток используется для обработки событий графического пользовательского интерфейса, как и во всех программах на основе gtkmm, а второй является рабочим потоком. Рабочий поток создается в момент нажатия вами кнопки "Начать работу". Он уничтожается тогда, когда выполнение работы завершается, тогда, когда вы нажимаете кнопку "Закончить работу", а также тогда, когда вы нажимаете кнопку "Выход".

Объект обработчика событий на основе класса Glib::Dispatcher используется для отправки уведомлений из рабочего потока в поток обработки событий пользовательского интерфейса. Класс ExampleWorker содержит данные, которые доступны обоим потокам. Эти данные защищены с помощью объекта взаимного исключения на основе класса Glib::Threads::Mutex. Обновление состояния пользовательского интерфейса происходит исключительно из потока обработки событий пользовательского интерфейса.

Рисунок 29-1: Многопоточная программа
Многопоточная программа

Исходный код

Файл: exampleworker.h (Для использования совместно с gtkmm 3, а не с gtkmm 2)

Файл: examplewindow.h (Для использования совместно с gtkmm 3, а не с gtkmm 2)

Файл: examplewindow.cc (Для использования совместно с gtkmm 3, а не с gtkmm 2)

Файл: main.cc (Для использования совместно с gtkmm 3, а не с gtkmm 2)

Файл: exampleworker.cc (Для использования совместно с gtkmm 3, а не с gtkmm 2)


Эти взаимодействия происходят из-за того, что, помимо других вещей, наследуемый от класса отслеживания sigc::trackable класс благодаря механизму наследования будет содержать объект типа std::list, с помощью которого будут отслеживаться слоты, созданные с помощью вызовов метода sigc::mem_fun() и представляющие любые из нестатических методов (точнее, он содержит список методов обратного вызова, который позволяет отсоединить соединенные слоты при уничтожении объекта). Каждый объект слота на основе класса sigc::slot также сохраняет с помощью объектов на основе класса sigc::slot_rep собственный объект отслеживания на основе класса sigc::trackable для отслеживания любых объектов соединений на основе класса sigc::connection, которые необходимы для выполнения операции его уничтожения, а также позволяют разорвать соединение с объектом отслеживания на основе класса sigc::trackable при отсоединении или при уничтожении. Объекты сигналов на основе класса sigc::signal также содержат список слотов, который будет обновляться при вызове их метода connect() или при вызове методов любого объекта соединения на основе класса sigc::connection, связанного с таким соединением.


Следующий раздел : Рекомендуемые техники.