Курс "Параллельные вычислительные технологии" (ПВТ), осень 2014 Сибирский государственный университет телекоммуникаций и информатики
преподаватель: Пазников Алексей Александрович к.т.н., доцент кафедры вычислительных систем СибГУТИ
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Лекция 3. POSIX Threads. Жизненный цикл потоков. Планирование. Синхронизация
Пазников Алексей АлександровичКафедра вычислительных систем СибГУТИ
tid - идентификатор потока (для завершения, синхронизации и т.д.)attr - параметры потока (например, минимальный размер стека)fun - функция для запуска (принимает и возвращает указатель void*)arg - аргументы функции
if (err = pthread_join(ta, &status)) {printf(“pthread_join failed: %s”,
strerror(err));exit(1);
}
sleep(2);
if (err = pthread_join(tc, &status)) {printf(“pthread_join failed: %s”,
strerror(err));exit(1);
}
Порождение и завершение потоков - пример (прод.)
if (err = pthread_join(td, &status)) {printf(“pthread_join failed: %s”,
strerror(err));exit(1);
}
sleep(1);
pthread_exit((void *) 88);}
Порождение и завершение потоков - пример (прод.)
int main(){
main_thr = pthread_self();
sleep(1);
pthread_create(&ta, &attr, func_a, NULL);
sleep(1);
pthread_create(&tc, &attr, func_c, NULL);
sleep(2);
pthread_cancel(td);
sleep(1);
pthread_exit((void *) NULL);}
Порождение и завершение потоков - пример
работа ожидание
Ejoin(A) join(C)
“зомби”
join(D)
D
A
main
C
B
create cancel join
cancel(D)
join(main)
Планирование выполнения потоков
Планирование на основе квантования (round-robin, sliced)
T1
T2
работа ожидание
Точное планирование с приоритетами
T1
T2
работа ожиданиезапрос ответ
Чем может быть вызвана активация потока
1. Синхронизация (synchronization). T1 запрашивает мьютекс, и если он занят потоком T2, то Т1 встаёт в очередь, тем самым давая возможность другим потокам запуститься.
2. Вытеснение (preemption). Происходит событие, в результате которого высокоприоритетный поток Т2 может запуститься. Тогда поток Т1 с низким приоритетом вытесняется, и Т2 запускается.
3. Уступание (yielding). Программист явно вызывает sched_yield() во время работы Т1, и тогда планировщик ищет другой поток Т2, который может запуститься, и запускает его.
4. Квантование (time-slicing). Квант времени для потока Т1 истёк. Тогда поток Т2 получает квант и запускается.
Состояния потока
1. Активный (active). Выполняется на процессоре.
2. Готов запуститься (runnable). Может и хочет запуститься, но пока нет ресурсов.
3. Сон (sleeping). Ождиает изменения состояния переменной синхронизации.
4. Зомби (zombie). Поток закончил своё выполнение и ждёт, когда его ресурсы заберут.
Sync. Variabe
Схема перехода состояний потока
Runnable
Active
ZombieSleeping
Dispatch
Preempt
ExitSleep
Wakeup
Пример планирования потоковT1 T2 T3
lock
unlocklock
Held 1
Sleepers ⚫
Time
0
lock
unlocklock1
T2 ⚫
Held 1
Sleepers ⚫
lock
unlocklock2
Held 0
Sleepers ⚫
lock
unlocklock3
Held 1
Sleepers ⚫
SIG
Пример планирования потоковT1 T2 T3
lock
unlocklock
Held 1
Sleepers ⚫
Time
0
lock
unlocklock1
T2 ⚫
Held 1
Sleepers ⚫
lock
unlocklock2
Held 0
Sleepers ⚫
lock
unlocklock3
Held 1
Sleepers ⚫
SIG
pthread_mutex_lock();
/* критическая секция */
pthread_mutex_unlock()
lock
unlock
Пример планирования потоковT1 T2 T3
lock
unlocklock
Held 1
Sleepers ⚫
Time
0
lock
unlocklock1
T2 ⚫
Held 1
Sleepers ⚫
lock
unlocklock2
Held 0
Sleepers ⚫
lock
unlocklock3
Held 1
Sleepers ⚫
SIG
Как задать политику планирования в pthread
struct sched_param {
int sched_priority;
};
int pthread_setschedparam(pthread_t thread,
int policy,
const struct sched_param *param);
int pthread_getschedparam(pthread_t thread,
int *policy,
struct sched_param *param);
Real-time policies:
● SCHED_FIFO
● SCHED_RR
● SCHED_DEADLINE
Normal policies:
● SCHED_OTHER
● SCHED_BATCH
● SCHED_IDLE
0
1..99
Нормальное планирование: SCHED_OTHER
SCHED_OTHER
● Планирование с разделением времени по умолчанию в Linux.
● Используется динамический приоритет, основанный на значении nice value (nice, setpriority, sched_setattr), которое повышается каждый раз, когда поток может запуститься (runnable), но откладывается планировщиком.
● Обеспечивает справедливое (fair) планирование.
Нормальное планирование: SCHED_BATCH
SCHED_BATCH (начиная с 2.6.16)● Похоже на SCHED_OTHER (планирует потоки в
соответствии с динамическим приоритетом)
● Планировщик всегда предполагает, что поток требователен к ресурсу процессора (CPU-intensive).
● Планировщик назначает штрафы (penalty) на активацию потока.
● Для неинтерактивных задач, выполняющих большой объем вычислений без уменьшение значения nice или
● В задачах, для которых требуется детерминированное поведение при планировании (пакетная обработка задач - batch processing).
Нормальное планирование: SCHED_BATCH
SCHED_IDLE (начиная с 2.6.23)
● Планирование низкоприоритетных задач.
● Приоритет со значением nice ниже +19.
Real-time политики планирования: SCHED_FIFO
⬤
⬤
⬤
При
орит
ет
T3 ⬤ T4 ⬤
T1 ⬤
T2
Sleeping Runnable
Real-time политики планирования: SCHED_RR
⬤
⬤
⬤
При
орит
ет
T3 ⬤ T4 ⬤
T1 ⬤
T2
Sleeping Runnable
Real-time политики планирования: SCHED_DEADLINE
SCHED_DEADLINE (начиная с 3.14)● Спорадическая модель планирования.
● Основан на алгоритмах GEDF (Global Earliest Deadline First) и CBS (Constant Bandwidth Server)
● Спорадическое пакет задач (sporadic task) - последовательность задач, где каждая задача запускается не более 1 раза за период.
● Каждая задача также имеет относительный дедлайн (relative deadline), до которого она должна завершиться, и время вычислений, в течение которого она будет занимать процессор.
Real-time политики планирования: SCHED_DEADLINE
SCHED_DEADLINE (начиная с 3.14)● Момент, когда пакет задач должен начаться
выполняться из-за того, что пришла новая задача, называется время поступления.
● Время начала выполнения - это время, когда пакет начинает выполняться.
● Абсолютный дедлайн = относительный дедлайн + время поступления.
Real-time политики планирования: SCHED_DEADLINE
T1время выполнения
время поступлениявремя начала
абсолютный дедлайн
относительный дедлайн
период
Real-time политики планирования: SCHED_DEADLINE
▪ CBS гарантирует невмешательствуо пакетов между собой путём остановки потоков, которые пытаются выполняться больше отведённого им времени (runtime)
▪ Ядро предотвращает ситуации, когда нельзя выполнить планирование потоков с политикой SCHED_DEADLINE (например, проверяется, достаточно ли будет имеющихся процессоров)
▪ Потоки с политикой SCHED_DEADLINE имеют наивысший приоритет! (среди всех других политик)
T1время выполнения
время поступлениявремя начала
абсолютный дедлайн
дедлайн (sched_deadline)
период (sched_period)
sched_runtime
Правила использования real-time политик планирования
▪ По возможности, никогда не использовать real-time политики.
▪ Если вам требуется полное внимание пользователя на чём-то постоянно изменяющемся (движение курсора, поток видео или аудио)
▪ Осуществление обратной связи и контроль (управление машинами, роботами)
▪ Сбор и обработка статистики в реальном времени.
Советы
▪ Оптимизируйте работу кэша
▫ Переключения контекста вызывают копирование кэша - это очень долго.
▫ Используйте привязку выполнения потоков к ядрам процессора (processor affinity).
▪ Если вы много думаете об планировании - вероятно, вы делаете что-то не так.
Синхронизация
Проблема синхронизации и атомарные операции
Записать значение переменной i в регистр (регистр = 5)
Поток A: i = i + 1t
Увеличить содержимое регистра (регистр = 6)
Сохранить значение регистра в перменную i (регистр = 6)
Записать значение переменной i в регистр (регистр = 5)
Поток B: i = i + 1
Увеличить содержимое регистра (регистр = 6)
Сохранить значение регистра в перменную i (регистр = 6)
i
5
5
6
6
Проблема синхронизации и атомарные операции
bal = GetBalance(account);
bal += bal * InterestRate;
PutBalance(account, bal);
bal = GetBalance(account);
bal += deposit;
PutBalance(account, bal);
Поток 1 Поток 2
● Критическая секция - участок кода, доступ к которого должен обеспечиваться полностью без прерываний.
● ВСЕ общие данные должны быть защищены.
● Общие данные - те, к которым могут иметь доступ несколько потоков (глобальные, статические переменные и др.).
Проблема синхронизации и атомарные операции
bal = GetBalance(account);
bal += bal * InterestRate;
PutBalance(account, bal);
bal = GetBalance(account);
bal += deposit;
PutBalance(account, bal);
Поток 1 Поток 2
Реализация синхронизация требует аппаратной поддержки атомарной операции test and set (проверить и установить).
test_and_set(address){
result = Memory[address]Memory[address] = 1return result;
}
Установить новое значение (обычно 1) в ячейку и
возвратить старое значение.
Простейшая реализация взаимного исключения
// инициализация (к моменту вызова CriticalSection // не гарантируется, что skip == false)skip = false
function CriticalSection() {
while test_and_set(lock) = true
skip // ждать, пока не получится захватить
// только один поток может быть в критической секцииcritical section
// освободить критическую секциюlock = false
}
Простейшая реализация взаимного исключения
enter_region: ; Начало функции tsl reg, flag ; Выполнить test_and_set. ; flag - разделяемая переменная, ; которая копируется в регистр ; и затем устанавливается в 1. cmp reg, #0 ; Был flag равен 0 в entry_region? jnz enter_region ; Перейти на начало, если reg ≠ 0 ; то есть flag ≠ 0 на входе ret ; Выход. Флаг был равен 0 на входе. ; Если достигли этой точки, значит ; мы внутри критической секции!
leave_region: move flag, #0 ; записать 0 во флаг ret ; вернуться в вызывающую функцию
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
T1Priority:0
T2Priority:1
T3Priority:2
T3 ⚫
Held 1
Sleepers ⚫ T2 ⚫
lock
lock lock
Диаграмма выполнения потоков - пример 1
T1
T2
работа ожидание
запереть мьютекс(попытаться)
отпереть мьютекскритическая секция
Диаграмма выполнения потоков - пример 2
T1
T2
работа ожидание
запереть мьютекс(попытаться)
отпереть мьютекскритическая секция
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock
(&lock);
}
item_t *remove()
{
pthread_mutex_lock(&lock);
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Поток 1 Поток 2 t
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Поток 1 Поток 2 t
Item 4 add(item4)
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Поток 1 Поток 2 t
Item 4
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Поток 1 Поток 2 t
Item 4
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Item 2 Item 1Item 3List
Поток 1 Поток 2 t
Item 4
Поток 3:
remove()
Вставка и удаление из списка
void add(list_t *item)
{
pthread_mutex_lock(&lock);
item->next = request;
list = item;
pthread_mutex_unlock(&lock);
}
list_t *remove()
{
pthread_mutex_lock(&lock);
/* sleeping */
/* sleeping */
item = item->next;
list = list->next;
pthread_mutex_unlock(&lock);
return request;
}
Поток 1 Поток 2 t
Поток 3:
remove()
Item 2 Item 1Item 3List
Тупиковые ситуации (deadlocks)
● Поток пытается дважды захватить один и тот же мьютекс
● Два мьютекса. Один поток удерживает первый мьютекс и пытается запереть второй мьютекс, в то время как второй поток удерживает второй мьютекс и пытается запереть первый мьютекс.
Пример работы двух мьютексов/* добавить новый объект */struct elem *elem_alloc(void) {
struct elem *ep;
int idx;
if ((ep = malloc(sizeof(struct elem))) != NULL) {
ep->count = 1;
if (pthread_mutex_init(&fp->lock, NULL) != 0) {
free(ep);
return NULL;
}
idx = hash_func(ep);
pthread_mutex_lock(&hashlock);
ep->next = ht[idx];
ht[idx] = ep->next;
pthread_mutex_lock(&ep->lock);
pthread_mutex_unlock(&hashlock);
/* продолжение инициализации */pthread_mutex_unlock(&fp->lock);
}
return ep;
}
Пример работы двух мьютексов
/* добавить ссылку на объект */void add_reference(struct elem *ep) {
pthread_mutex_lock(&ep->lock);
ep->count++;
pthread_mutex_unlock(&ep->lock);
}
/* найти существующий объект */struct elem *find_elem(int id) {
struct elem *ep;
int idx;
idx = hash_func(ep);
pthread_mutex_lock(&hashlock);
for (ep = ht[idx]; ep != NULL; ep = ep->next) {
if (ep->id == id) {
add_reference(ep); break;
}
}
pthread_mutex_unlock(&hashlock);
return ep;
}
Пример работы двух мьютексовvoid elem_release(struct elem *ep) { /* осободить ссылку на объект */
int idx;
pthread_mutex_lock(&ep->lock);
if (ep->count == 1) { /* последняя ссылка */pthread_mutex_unlock(&ep->lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&ep->lock);
/* необходима повторная проверка условия */if (ep->count != 1) {
ep->count--;
pthread_mutex_unlock(&ep->lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* … найти и удалить из списка … */
pthread_mutex_unlock(&ep->lock);
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&ep->lock);
} else {
ep->count--; pthread_mutex_unlock(&ep->lock);}
}
Пример работы двух мьютексов - упрощениеvoid elem_release(struct elem *ep) { /* осободить ссылку на объект */
int idx;
pthread_mutex_lock(&hashlock);
if (--ep->count == 0) { /* последняя ссылка */pthread_mutex_unlock(&ep->lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&ep->lock);
/* необходима повторная проверка условия */if (ep->count != 1) {
ep->count--;
pthread_mutex_unlock(&ep->lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* … найти и удалить из списка … */
pthread_mutex_unlock(&ep->lock);
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&ep->lock);
} else {
ep->count--; pthread_mutex_unlock(&ep->lock);}
}
Выбор критических секций
При разработке многопоточных программ необходимо учитывать балланс между эффективностью блокировки и её сложностью, которые определяются детализацией (“зернистостью”) критической секции.
● Грубая детализация (coarse-grained) критических секций - низкая эффективность, но простота разработки и поддержания кода.
● Мелкая детализация (fine-grained) критических секция - высокая эффективность (которая может снизиться из-за избыточного количества критических секций), но сложность кода
Поток Т1 с низким приоритетом удерживает мьютекс, который необходим Т2 с высоким приоритетом. Во время удержания поток Т1 вытесняется потоком Т3 со средним приоритетом. В результате поток Т2 зависит от освобождения мьютекса потоком Т1.
T1Prior: 0
Runnable
T2Prior: 2Sleep
T3Prior: 1Active
lock(M1)
lock(M1)
unlock(M1)
T2 ⚫
Held 1
Sleepers ⚫
Проблема: Инверсия приоритетов. Решения
■ Priority Ceiling MutexУстанавливается максимальный приоритет для потока, который захватывает мьютекс. Каждый поток, который захватывает мьютек, автоматически получает этот приоритет (даже если у него был ниже).
int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t
*restrict attr, int *restrict prioceiling);
int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *attr,
int prioceiling);
int pthread_mutex_getprioceiling(const pthread_mutex_t *restrict mutex,
int *restrict prioceiling);
int pthread_mutex_setprioceiling(pthread_mutex_t *restrict mutex,
int prioceiling, int *restrict old_ceiling);
■ Priority Inheritance MutexesПоток Т1 захватывает мьютекс без изменения своего приоритета. Когда второй поток Т2 пытается захватить, владелец Т1 мьютекса получает приоритет потока Т2.
Проблема: Последовательный захват
▪ Потоки Т1 и Т2 используют мьютекс М1 для работы, удерживая его значительное время. При этом основую часть работы они выполняют в критической секции.
▪ Допустим Т1 захватывает М1. Т2, попытавшись захватить мьютекс, засыпает. Тогда, после того, как Т1 освободит мьютекс, он может успеть снова его захватить до того, как проснётся Т1.
T1
работаожидание
запереть(попытаться) отпереть
критическая секцияпробуждение
T2
Обычный случай
Проблема: Последовательный захват
▪ Потоки Т1 и Т2 используют мьютекс М1 для работы, удерживая его значительное время. При этом основую часть работы они выполняют в критической секции.
▪ Допустим Т1 захватывает М1. Т2, попытавшись захватить мьютекс, засыпает. Тогда, после того, как Т1 освободит мьютекс, он может успеть снова его захватить до того, как проснётся Т1.
T1
работаожидание
запереть(попытаться) отпереть
критическая секцияпробуждение
T2
Т1 повторно захватывает мьютекс
Проблема: Последовательный захват. Решение
Решение - использование FIFO-мьютекса: владелец мьютекса (Т1) после освобождения мьютекса автоматически передаёт права на захват мьютекса первому потоку в очереди, который ожидает освобождения мьютекса.
T1
работаожидание
запереть(попытаться) отпереть
критическая секцияпробуждение
T2
Использование FIFO-мьютекса
Семафоры (Semaphores)
▪ Ограничивает число потоков, которые могут зайти в заданный участок кода.
▪ Семафор - это счетчик s = 0, …, ∞
▪ Операции:
▫ sem_post - увеличивает значение семафора
▫ sem_wait - пытается уменьшить значение семафора (и это удаётся сделать, если s > 0).
▫ sem_getvalue - вернуть текущее значение семафора (используется редко)
Семафоры (Semaphores) в pthreads
/* проинициализировать семафор */int sem_init(sem_t *sem, int pshared,
unsigned int value);
/* уничтожить семафор */int sem_destroy(sem_t *sem);
Производитель/потребитель с ограничением длины очереди
/* производитель (на основе условных переменных) */void *producer(void *arg)
{
request_t *request;
for (;;) {
pthread_mutex_lock(&request_lock);
while (length >= 10)
pthread_cond_wait(&request_producer,
&requests_lock);
add(request);
lenght++;
pthread_mutex_unlock(&request_lock);
pthread_cond_signal(&request_consumer);
}
}
Производитель/потребитель с ограничением длины очереди
/* потребитель (на основе условных переменных) */void *consumer(void *arg)
{
request_t *request;
for (;;) {
pthread_mutex_lock(&request_lock);
while (length == 0)
pthread_cond_wait(&request_consumer,
&requests_lock);
request = remove();
length--;
pthread_mutex_unlock(&request_lock);
pthread_cond_signal(&request_producer);
process_request(request);
}
}
Мониторы
● Потокобезопасная инкапсюляция общих данных
● В С - путём объявления всех переменных внутри функции, которая выполняет доступ
● В С++ - инкапсюляция путем создания объекта
● Мониторы “заставляют” пользователя совершать действия над общими переменным через код монитора.
● Мониторы, однако, не позволяют реализовать все типы блокировок (например, пересекующиеся блокировки или использование неблокирующих “trylock” блокировок).
Мониторы. Реализация на С
/* реализация счётчика count - суммы всех i */void count(int i)
{
static int count = 0;
static pthread_mutex_t countlock =
PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&countlock);
count += i;
i = count;
pthread_mutex_unlock(&countlock);
return i;
}
Мониторы. Реализация на С++
class Monitor
{
pthread_mutex_t *mutex;
public:
Monitor(pthread_mutex_t *m);
virtual ~Monitor();
};
Monitor::Monitor(pthread_mutex_t *m) {
mutex = m;
pthread_mutex_lock(mutex);
}
Monitor::~Monitor() {
pthread_mutex_unlock(mutex);
}
Мониторы. Реализация на С++
void foo()
{
Monitor m(&data_lock);
int temp;
// ...
func(temp);
// ...
// Вызов деструктора снимает блокировку}
Спинлоки
▪ Спинлоки подобны мьютексам, но
▪ Это более простой и быстрый механизм синхронизации (“test&set” или что-то ещё)
▪ Поток, пытаясь запереть спинлок, не блокируется (если спинлок занят) - вместо этого он циклически пробует запереть спинлок, пока это у него не получится.
▪ Подходит для мелкозернистого параллелизма (КС небольшая и блокируется на короткий срок).
▪ Критическая секция является местом частого возникновения конфликтов.