Эксперт
Сергей
Сергей
Задать вопрос
Мы готовы помочь Вам.

Цель работы

Знакомство с многопоточным программированием и методами синхронизации потоков средствами POSIX.

Задание

  1. С помощью таблицы вариантов заданий выбрать граф запуска потоков в соответствии с номером варианта. Вершины графа являются точками запуска/завершения потоков, дугами обозначены сами потоки. Длину дуги следует интерпретировать как ориентировочное время выполнения потока. В процессе своей работы каждый поток должен в цикле выполнять два действия:
    1. выводить букву имени потока в консоль;
    2. вызывать функцию computation() для выполнения вычислений, требующих задействования ЦП на длительное время. Эта функция уже написана и подключается из заголовочного файла lab2.h, изменять ее не следует.
  2. В соответствии с вариантом выделить на графе две группы с выполняющимися параллельно потоками. В первой группе потоки не синхронизированы, параллельное выполнение входящих в группу потоков происходит за счет планировщика задач (см. примеры 1 и 2). Вторая группа синхронизирована семафорами и потоки внутри группы выполняются в строго зафиксированном порядке: входящий в групу поток передает управление другому потоку после каждой итерации цикла (см. пример 3 и задачу производителя и потребителя). Таким образом потоки во второй группе выполняются в строгой очередности.
  3. С использованием средств POSIX реализовать программу для последовательно-параллельного выполнения потоков в ОС Linux или Mac OS X. Запрещается использовать какие-либо библиотеки и модули, решающие задачу кроссплатформенной разработки многопоточных приложений (std::thread, Qt Thread, Boost Thread и т.п.). Для этого необходимо написать код в файле lab2.cpp:
    1. Функция unsigned int lab2_thread_graph_id() должна возвращать номер графа запуска потоков, полученный из таблицы вариантов заданий.
    2. Функция const char* lab2_unsynchronized_threads() должна возвращать строку, состоящую из букв потоков, выполняющихся параллельно без синхронизации (см. примеры в файлах lab2.cpp и lab2_ex.cpp).
    3. Функция const char* lab2_sequential_threads() должна возвращать строку, состоящую из букв потоков, выполняющихся параллельно в строгой очередности друг за другом (см. примеры в файлах lab2.cpp и lab2_ex.cpp).
    4. Функция int lab2_init() заменяет собой функцию main(). В ней необходимо реализовать запуск потоков, инициализацию вспомогательных переменных (мьютексов, семафоров и т.п.). Перед выходом из функции lab2_init() необходимо убедиться, что все запущенные потоки завершились. Возвращаемое значение: 0 – работа функции завершилась успешно, любое другое числовое значение – при выполнении функции произошла критическая ошибка.
    5. Добавить любые другие необходимые для работы программы функции, переменные и подключаемые файлы.
    6. Создавать функцию main() не нужно. В проекте уже имеется готовая функция main(), изменять ее нельзя. Она выполняет единственное действие: вызывает функцию lab2_init().
    7. Не следует изменять какие-либо файлы, кроме lab2.cpp. Также не следует создавать новые файлы и писать в них код, поскольку код из этих файлов не будет использоваться во время тестирования.

Последовательное выполнение потоков может обеспечиваться как за счет использования семафоров, так и с помощью функции pthread_join(). Запускать потоки можно все сразу в функции lab2_init(), а можно и по одному (или группами) из других потоков.

В процессе своей работы каждый поток выводит свою букву в консоль. Оценка правильности выполнения лабораторной работы осуществляется следующим образом. Если потоки a и b согласно графу должны выполняться одновременно (параллельно), то в консоли должна присутствовать последовательность вида abababab (или схожая, например, aabbba); если потоки выполняются последовательно, то в консоли присутствует последовательность вида aaaaabbbbbb, причем после появления первой буквы b, буква a больше не должна появиться в консоли.

Количество букв, выводимых каждым потоком в консоль, должно быть пропорционально числу интервалов (длине дуги), соответствующей данному потоку на графе. При этом количество символов, выводимых в консоль каждым из потоков, должно быть не меньше чем 3Q и не больше чем 5Q, где Q – количество интервалов на графе, в течении которых выполняется поток. Множитель перед величиной Q следует выбрать одинаковым для всех потоков, задав его равным 3, 4 или 5.

Пример работы с графом потока

Рассмотрим граф запуска потоков, приведенный на рисунке ниже.

Программа, реализующая указанную на графе последовательность запуска потоков, должна запустить 5 потоков: abcd и e. Работу программы можно разбить на три временных интервала:

  1. С момента времени T0 до T1 работает только поток a.
  2. С T1 до T2 параллельно работают потоки bc и d.
  3. С T2 до T3 параллельно работают потоки d и e.

Пусть потоки bc и d должны выполняться параллельно без использования средств синхронизации, а потоки d и e должны выполняться параллельно в строгой очередности друг за другом за счет использования семафоров. В этом случае возможны следующие варианты последовательностей символов на выходе программы:

В файле lab2_ex.cpp приведен пример выполнения лабораторной работы для рассмотренного графа. Скомпилировать и запустить данный пример можно с помощью последовательности команд

g++ lab2_ex.cpp main.cpp -lpthread -o lab2_ex

./lab2_ex

Сборка и тестирование

Скомпилировать программу из консоли без использования линковщика можно следующим образом. Сначала необходимо перейти в директорию, в которой находятся исходные файлы lab2.h, lab2.cpp и main.cpp. Далее все команды будут приводиться относительно этой директории.

Компиляция программы в файл a.out в текущей папке:

g++ lab2.cpp main.cpp -lpthread

При использовании старой версии компилятора GCC может потребоваться дополнительно указать ключ -std=c++11. В этом случае компилятор выведет соответствующее сообщение в консоль.

Запуск скомпилированной программы: ./a.out. При желании можно указать ключ -o при компиляции, в этом случае можно будет задать более осмысленное имя итогового файла с программой, нежели a.out. Например,

g++ lab2.cpp main.cpp -lpthread -o lab2

./lab2

Тестирование

Для запуска тестов на локальной машине предварительно потребуется собрать библиотеку gtest. Для этого, перед первой компиляцией тестов, необходимо выполнить следующую последовательность команд:

cd test/gtest

GTEST_DIR=$(pwd)

g++ -isystem “${GTEST_DIR}”/include -I”${GTEST_DIR}” -pthread -c “${GTEST_DIR}”/src/gtest-all.cc

ar -rv libgtest.a gtest-all.o

В консоли должен появиться текст

ar: creating libgtest.a

a – gtest-all.o

Также можно убедиться с помощью команды ls -l, что в текущей папке появился файл libgtest.a и его размер больше нуля байт. Если все прошло успешно, в дальнейшем вызывать эти команды более не потребуется.

Далее необходимо вернуться в директорию test, расположенную в корневой директории репозитория. Для компиляции тестов необходимо выполнить команду

g++ ../lab2.cpp tests.cpp -lpthread -lgtest -o runTests -I gtest/include -L gtest

При необходимости следует добавить ключ -std=c++11. Запустить тесты можно командой ./runTests. Если не все тесты завершились успешно, необходимо внести изменения в файл lab2.cpp, добившись правильного выполнения задания, затем повторно скомпилировать тесты командой g++ ../lab2.cpp tests.cpp -lpthread -lgtest -o runTests -I gtest/include -L gtest, после чего вновь запустить процесс тестирования.

Рекомендуется локально запускать тесты несколько раз даже в случае их успешного выполнения, поскольку последовательность выполнения потоков может отличаться от запуска к запуску программы и ошибка в решении задачи синхронизации потоков может проявляться не всегда. Если тесты пройдены успешно, можно выполнить команды git add lab2.cpp, git commit и git push, после чего убедиться, что тесты также успешно пройдены и в репозитории.

Примеры

Перед выполнением лабораторной работы рекомендуется ознакомиться с  синхронизации потоков. Также возможный подход к выполнению лабораторной работы показан в файле . В данном примере потоки b, c и d не синхронизированы, а потоки d и e синхронизированы парой семафоров и выполняются в строгой последовательности (чередуются) по аналогии с задачей производителя-потребителя.

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

pthread_mutex_t lock;

 

void* thread_a_(void *ptr)

{

for (int i = 0; i < 3; ++i) {

pthread_mutex_lock(&lock);

std::cout << “a” << std::flush;

pthread_mutex_unlock(&lock);

computation();

}

return ptr;

}

Здесь три раза выводится буква потока (a), что соответствует дуге графа длиной в один интервал. Команда std::flush используется для немедленного вывода буквы потока в консоль, чтобы избежать буферизации на уровне операционной системы. В качестве альтернативного решения можно полностью отключить буферизацию стандартного потока вывода с помощью функции, например, так: std::setbuf(stdout, NULL).

После вывода каждой буквы в консоль обязательно вызывается функция computation() для имитации проведения вычислений и выполнения ресурсо-затратных операций. В большинстве случаев вызов этой функции приводит к переключению контекста и началу выполнения другого потока. Это происходит прежде, чем данным потоком в консоль будет выведена следующая буква.

В зависимости от реализации операций ввода-вывода в операционной системе, а также от версий стандартной библиотеки и компилятора языка C/C++, простой вызов std::cout << “a” может быть как атомарной операцией, так и не атомарной. В любом случае, команда std::cout << “a” << std::flush не является атомарной, поскольку каждый оператор << гарантировано является отдельной операцией. Поэтому, чтобы избежать проблем с выводом в консоль, при работе с общим ресурсом (стандартным потоком вывода) используется мьютекс. Подробнее см. соответствующие.

Содержание отчета

  • Титульный лист
  • Цель работы
  • Задание на лабораторную работу
  • Граф запуска потоков
  • Результат выполнения работы
  • Исходный код программы с комментариями
  • Выводы

 

Была ли полезна данная статья?
Да
67.47%
Нет
32.53%
Проголосовало: 83

или напишите нам прямо сейчас:

Написать в WhatsApp Написать в Telegram