Sunday, October 21, 2012

POSIX thread (pthread)

Основы

Набор pthread библиотек - это API предоставляющий средства для работы с трэдами: создание, запуск, управление(в т.ч. синхронизация). Трэд - это по сути "кусок" кода, который может выполняться параллельно с потоком программы. Эффективность повышается на порядок, когда есть условия для выполнения кода треэда на отдельном просессоре(т.е. на многопроцессорных системах). Для расспараллеливания задач в UNIX есть старый как мир способ - запуск процесса, но по сравнию с ним трэды имеют ряд приимуществ: простота использования и комуникации(все потоки выполняются в рамках одного процесса), время переключением контекста меньше чем у процессов(что сказывается на производительсти).

Трэды одного процесса разделяют следующие ресурсы: открытые файлы, глобальные переменные, сигналы и их обработчики, UID(user) и GID(group). Так же каждый трэд обладает набором уникальных атрибутов: индетификатор, указатель стэка и значение регистров, локальные переменные, приоритет, значение глобальной переменной errno. Во время компиляции своих программ необходимо подлкючить библиотеку pthread. Простенький Makefile, которым будем собирать наши примеры:

И пример, который запускает на выполнение два трэда печатающих строку на консоль:

После запуска программы, мы должны увидеть следующий вывод:

pthread_create

  • thread указатель на переменную специального типа, которая хранит служебную информацию о трэде после его запуска
  • attr доп. аттрибуты запуска(в нашем случае NULL, т.к. используем значение по умолчанию)
  • start_routine указатель на функцию код которой будет выполнятся в трэде. Функция должна иметь строгоопределенную сигнатуру. Принимать и возвращать указатель на (void*)
  • arg параметры функции start_routine
  • pthread_join

    Функция ожидает завершения работы треда. Прототип ее выглядит следующим образом:

  • thread указатель на переменную типа thread_t
  • thread_return указатель на перменную, которая хранит значение кода возврата
  • Если не делать вызов метода pthread_join, то программа благополучно крэшает. Из-за того, что основной трэд программы(main) завершается в тот момент, когда есть активные трэды(выполняющиеся в фоновом режиме). Так что принебригать не стоит :).

    Синхронизация

    Синхронизация необходима в тех случаях, когда трэды разделяют общий ресурс. Поменяем нашу функцию thread_method таким побразом, чтобы она выполняла инкремент переменной count: По задумке на консоль должно быть напечатно значение переменной с приращением в еденицу в каждой строке. А это то что получаем в действительности:

    Синхронизация по мьютексу

    Чтобы избежать описанных выше проблем, используют синхронизацию. Есть множество способов, но пожалуй самый простой - синхронизация по мьютексу. Мьютекс - это ресурс, который можно захватывать и освобождать. Если thread1 захватил мьютекс, и thread2 попытается сделать тоже самое, то он будет заблокирован до момента освобождения мьютекса. Жизненная аналогия - кабина туалета, в ней может находится лишь один человек, закрывший дверь изнутри. Как только она освобождается, занять ее может другой кандидат, ожидающий снаружи. Теперь пример: Исправленный код выдает нам тот результат, который мы планировали получить вначале: Добавилась пара новых методов. Думаю с pthread_mutex_lock и pthread_mutex_unlock все понятно. Ножно сказать, что мы можем лочить только переменные определенного типа - pthread_mutex_t. Еще один интересный момент: Вызывая данный метод, мы отдаем процессорное время другому трэду. Для нашей задачи это необходимо, чтобы дать возможность заблокированному потоку выполнить инкремент.

    Синхронизация по условным переменным

    Запустив предыдущий пример, можно заметить, что значение переменной правильное(инкремент в каждом шаге), но порядок следования треэдов непредсказуем. Т.е. невозможно предугадать какой трэд будет отрабатывать в следующий момент времени. А что если я хочу, чтобы доступ к переменной осуществлялся поочередно из каждого потока? Реализоровать такой алгоритм на мьютексах, думаю, можно, но код вряд ли будет отличаться эффективностью и тем более читабельностью. Для решения задачи, будем использовать другой способо синхронизации - condition variables. Используя услвную переменную, можно заблокировать трэд на неопределенно долгое время, а затем, послав сигнал, пробудить его. Касательно примера, наш код притерпит следующие изменения: И это то, что я получил на выходе: pthread_cond_t - специальный тип переменной используемый для услвной синхронизации. Методы pthread_cond_wait и pthread_mutex_lock говорят сами за себя.

    Хороший пример использования услвных перменных: механизм буферизации видео плеера на youtube. Он не кэширует клип целиком, а лишь хранит фрагмент примерно для нескольких минут просмотра. По мере проигрывания, читающий трэд просыпается(услвная переменная накачки буфера) и начинает декодировать следующию порцию видео данных.

    Взаимная блокировка

    Разрабатывая многопоточные приложения, рано или поздно сталкиваешься с проблемой под названием взаимная блокировка. Представим себе следующую ситуацию: Т.е. на момент запуска thread2, thread1 будет заблокирован на ожидании lock2, а т.к. thread2 ждет lock1, то оба треда взаимно блокируют друга без возможности выхода из данной ситуации. Проблема кажется простой и очевидной, особенно когда пример простой (как здесь), но когда дело касается большого и сложного проекта, то можно провести долгие, а главное увлекательные часы отлаживая код. Помимо внимательности есть упреждающий способ борьбы с блокировками.

    Первый способ с помощью pthread_mutex_trylock. Пример ожидание ресурса: Пример разовой попытки захватить ресурс.

    No comments:

    Post a Comment