os_labs/lab11/README.md

6.9 KiB
Raw Blame History

Лабораторная работа №11

Задание 1

vector-deadlock.c, main-common.c и др.:

  • Выполнить ./vector-deadlock -n 2 -l 1 -v которая инициирует 2 потока (-n 2) и каждый из них осуществляет одно сложение (-l 1) с опцией (-v). Объяснить результат. Меняется ли он от вызова к вызову?
  • Добавить флаг -d и изменить количество циклов -l . Всегда ли возникает состояние взаимной блокировки потоков (deadlock)?
  • Теперь меняем число потоков -n. Есть ли такое число потоков, при котором блокировка не возникает?

Программа печатает в каждом потоке начальные данные перед vector_add и результат выполнения данной функции. Результат может меняться от вызова к вызову, так как весь worker не покрыт мьютексами, но на практике на такой небольшой программе это маловероятно достижимо.

Обычно программа возвращает следующее:

->add(0, 1)
<-add(0, 1)
              ->add(0, 1)
              <-add(0, 1)

Спустя примерно 30 запусков, удалось получить другой результат:

->add(0, 1)
              ->add(0, 1)
              <-add(0, 1)
<-add(0, 1)

При добавлении -d взаимная блокировка возникает не всегда, а только при попадании переключения потоков между мьютексами.

Удалось достичь стабильный дедлок при запуске с ключами -t -n 2 -l 100 -d (без -v)

В случае, если число потоков = 1, взаимной блокировки не возникнет.

Задание 2

vector-global-order.c:

  • За счет чего программа избегает блокировок?
  • Для чего предусмотрен специальный случай в vector add(), когда исходный и результирующий вектор совпадают?
  • Флаги: -t -n 2 -l 100000 -d. Как меняется время в зависимости от числа циклов и числа потоков?
  • Что происходит, когда включается ключ -p (при сложении различных векторов и одного и того же)?

Программа избегает мёртвой блокировки за счёт упорядочивания по адресам, что позволяет постоянно сохранять порядок блокировки.

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

В случае увеличения числа циклов и потоков, время выполнения растёт.

В случае включения -p время уменьшается, так как разрешается параллелизм.

Задание 3

vector-try-wait.c:

  • Нужен ли первый вызов pthread_mutex_trylock()?
  • Как меняется число повторных попыток, когда растет число потоков?

Вызовы pthread_mutex_trylock необходимы для создания порядка блокировки, для того чтобы избежать дедлока.

$ ./vector-try-wait -t -n 2 -l 100 -d
Retries: 0
Time: 0.00 seconds

$ ./vector-try-wait -t -n 4 -l 100 -d
Retries: 847
Time: 0.00 seconds

С увеличением числа потоков происходит рост повторных попыток, что является логичным, так как переключение между потоками становится более частым.

При использовании -p повторных попыток не возникает.

Задание 4

vector-avoid-hold-and-wait.c:

  • Сравнить с другими подходами.
  • Как меняется производительность в зависимости от наличия флага -p?

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

При использовании -p время уменьшается.

Задание 5

vector-nolock.c:

  • Сравнить семантику и производительность с другими вариантами при работе с двумя одинаковыми векторами и в случае, когда каждый поток работает на своем векторе -p.

Указав memory, программа дожидается завершения всех операцией с памятью, что позволяет заменить мьютексы в данном случае. (https://ru.wikipedia.org/wiki/GCC_Inline_Assembly)

Фактически в программе производится атомарное сложение, и в стандарте C11 для этих целей есть особые типы: (https://en.cppreference.com/w/c/language/atomic)

Также в C11 ввели поддержку потоков в стандартную библиотеку, что позволяет писать кроссплатформенный код: (https://en.cppreference.com/w/c/thread)

Но атомарные операции очень дорого стоят. Сравним время выполнения следующих команд:

$ ./vector-nolock -t -n 2 -l 1000000 -d 
Time: 7.20 seconds

$ ./vector-nolock -t -n 2 -l 1000000 -d -p
Time: 1.07 seconds
$ ./vector-avoid-hold-and-wait -t -n 2 -l 1000000 -d
Time: 4.46 seconds

$ ./vector-avoid-hold-and-wait -t -n 2 -l 1000000 -d -p
Time: 0.40 seconds
$ ./vector-try-wait -t -n 2 -l 1000000 -d
Retries: 5979033
Time: 2.55 seconds

$ ./vector-try-wait -t -n 2 -l 1000000 -d -p
Retries: 0
Time: 0.25 seconds

$ ./vector-global-order -t -n 2 -l 1000000 -d
Time: 1.23 seconds

$ ./vector-global-order -t -n 2 -l 1000000 -d -p
Time: 0.26 seconds

Таким образом видно, что vector-nolock работает медленнее других в любом случае.