Поиск ошибок в многопоточном приложении (на примере Thread Checker) ЛЕКЦИЯ 9, часть 1
2 ЛИТЕРАТУРА 99% - по материалам тренинга Intel для преподавателей ВУЗов, апрель 2006, Нижний Новгород, «свободный перевод» Калининой А.П.
3 Цели и задачи Научиться Применять Thread Checker для тестирования правильности работы приложений на основе Windows* threads, поиска и ликвидации разнообразных ошибок, связанных с взаимодействием потоков Определять, безопасны ли библиотечные функции для многопоточного выполнения (не возникают ли ошибки при параллельном выполнении нескольких функций: если возникают, то «небезопасны»)
4 Содержание Что такое Intel® Thread Checker... Определение условий возникновения гонок данных ( race conditions ) Thread Checker – помощник «многопоточного» программиста Другие ошибки многопоточного приложения Проверка библиотеки на безопасность многопоточного выполнения Другие возможности Thread Checker
5 Зачем это нужно... Создание многопоточного приложения может оказаться сложной задачей Новый класс проблем возникает из-за взаимодействия одновременно работающих потоков (concurrent threads) Гонки данных или конфликты памяти (Data races or storage conflicts) Больше, чем один поток имеет доступ к изменению данных без синхронизации измененных значений Тупики (Deadlocks ) Потоки ждут события, которое никогда не наступит
6 Intel® Thread Checker Инструмент для отладки многопоточных программ Находит ошибки в многопоточных программах на основе Win32*, POSIX* и OpenMP* Быстро находит такие ошибки, на выявление которых традиционным способом (по результатам работы многопоточной программы) требуется несколько дней Находит источник ошибки, а не ее проявления Ошибка не обязательно должна случиться, чтобы быть выявленной «Вставлен» в среду VTune Performance Analyzer Тот же самый интерфейс среды VTune
7 Как и что можно анализировать с помощью Intel® Thread Checker Поддерживает несколько различных компиляторов Компиляторы Intel® C++ и Fortran, версии v7 и выше Microsoft* Visual* C++, v6 Microsoft* Visual* C++.NET* 2002, 2003 & 2005 Editions Интегрируется в среду Microsoft Visual Studio.NET* Доступна такая форма представления результатов анализа, как «участки кода, ответственные за ошибку» Контекстное меню выявленной ошибки: можно сразу получить помощь - щелкнуть на Diagnostic Help Выявляет причины ошибок и предлагает способы их ликвидации Предлагает на выбор набор API в качестве определяемых пользователем синхронизационных примитивов
8 Помощь Thread Checker Контекстное меню (щелчок правой кнопкой): выбор помощи по результатам диагностики (Diagnostic Help) Выявляет причины ошибок и предлагает способы их ликвидации. Предлагает на выбор набор API в качестве определяемых пользователем синхронизационных примитивов
9 Thread Checker: выполнение анализа Способ выполнения анализа: динамически, во время работы приложения Производится мониторинг: Потоков и используемых синхронизационных примитивов API Последовательности выполнения в каждом потоке Последовательности взаимодействия потоков Доступа потоков к памяти Анализируемый код должен быть выполнен, чтобы анализ стал возможным
10 Thread Checker: перед запуском необходимо учесть, что... Инструментирование: необходимо помнить, что... Добавляется обращение к библиотеке для записи информации О работе потоков и объектов синхронизации API О доступе к памяти Увеличивается время выполнения и объем выполняемого кода В тестируемом приложении рекомендуется применять малый объем обрабатываемых данных (workloads) Время выполнения и объем приложения увеличиваются Несколько запусков с различными потоками выполнения дадут более полную картину Для конечного результата важно, какого рода «загрузку» Вы осуществили
11 Требования к загружаемому приложению Чтобы выделить проблемный код для каждого потока: Действуйте под девизом: чем меньше, тем лучше! Минимизируйте набор обрабатываемых данных Уменьшайте объем изображения... Минимизируйте количество итераций в цикле или временных шагов Моделируйте минуты, а не дни... Уменьшайте скорость обновления значений переменных Меньше кадров в секунду... Тогда найдете ошибки многопоточного приложения быстрее!
12 Подготовка приложения для анализа Thread Checker Компиляция Используйте многопоточно - безопасные динамические библиотеки ( /MD, /MDd ) Включите генерацию символьной информации ( /Zi, /ZI, /Z7 ) Отключите оптимизацию ( /Od ) «Линкование» (Link ) Нужно сохранить символьную информацию ( /debug ) Определить релоцированные секции (Specify relocatable code sections) ( /fixed:no )
13 Бинарное инструментирование (Binary Instrumentation) Выполняется для поддерживаемых компиляторов Запуск приложения Должен быть выполнен из-под Thread Checker Приложение инструментируется во время выполнения Также применяются внешние инструментированные динамические библиотеки (DLLs)
14 Инструментирование исходного кода (Source Instrumentation) Компиляторы Intel® C++ или Fortran Компилировать с /Qtcheck Выполнение приложения Запуск в среде VTune Запуск из-под командной строки Windows* Полученные данные размещаются в файле результатов threadchecker.thr Просмотр результатов (.thr file) в среде VTune Дополнительные динамические библиотеки (DLLs) не инструментируются и не анализируются Это дает более подробную диагностику
15 Intel® Thread Checker Wizard Intel® Thread Profiler Wizard Advanced Activity Configuration Запуск Thread Checker Выбрать Threading Wizards Intel® Thread Checker Wizard Выбрать визард Thread Checker
16 Диагностики Thread Checker
17 Сортировка диагностик по группам
18 Форма представления «участки кода, ответственные за...»
19 Помощь по диагностикам Правой кнопкой мыши... Более подробная помощь
20 Задание 1 Откомпилируйте и выполните последовательную версию поиска простых чисел без Thread Checker Откомпилируйте и выполните многопоточную версию без Thread Checker Выполните приложение в Thread Checker, чтобы определить источники ошибок
21 Анализ зависимостей Пусть S1, S2, S3, S4 – различные задания, которые предполагается отдать на выполнение различным потокам. Но перед тем, как распределить работу, нужно понять, насколько эти задания независимы. «Потоковая зависимость» (flow dependence) между S1 и S2 Значение A изменяется в S1 и только после должно использоваться в S2 (запись должна быть раньше чтения) «Противопотоковая» зависимость (anti dependence ) между S2 и S3 Значение A считывается в S2 раньше, чем изменяется в S3 (чтение должно быть раньше записи) Зависимость «вывода» (output dependence) между S3 и S4 Вычисление значения A в S3 должно быть раньше вычислений в S4 (запись раньше записи) S1: A = 1.0; S2: B = A ; S3: A = 1/3 * (C – D); S4: A = (B * 3.8) / 2.7; Рассмотрим данный последовательный код:
22 Зависимости, обнаруженные Thread Checker Зависимость «вывода» (output dependence) Конфликты «запись-запись» (Write-Write conflict):один из потоков успевает изменить значение переменной раньше, чем согласно последовательному алгоритму ее должен изменить другой поток «Противопотоковая» зависимость (Anti-dependence) Конфликты «чтение-запись» (Read-Write conflict): один из потоков успевает считать значение переменной раньше, чем согласно последовательному алгоритму ее должен изменить другой поток «Потоковая зависимость» (Flow dependence) Конфликты «запись-чтение» (Write-Read conflict): один из потоков успевает изменить значение переменной раньше, чем согласно последовательному алгоритму ее должен считать другой поток
23 Условия (Race Conditions) возникновения гонки данных Не определен жестко порядок выполнения операций Одновременный доступ к одной переменной нескольких потоков Это наиболее популярная ошибка в многопоточных программах Эта ошибка может быть далеко не очевидной и трудно выявляемой
24 Как уничтожить возможность возникновения гонки данных... Решение: Ограничить видимость переменных пределами каждого потока Когда можно ограничить видимость переменных… Для значений, не используемых вне параллельного региона Для промежуточных или «рабочих» (work) переменных Как это сделать… Применять клаузы OpenMP, определяющие пределы видимости (OpenMP scoping clauses ( private, shared )) Описывать переменные в пределах потоковой функции Выделять память в пределах стека потока (Allocate variables on thread stack) Сохранять в потоке API (TLS (Thread Local Storage) API)
25 Как уничтожить возможность возникновения гонки данных...(продолжение) Решение: контролировать разделяемый доступ с помощью выделения критических регионов (critical regions) Когда использовать контролируемый доступ… Для величин, используемых вне параллельного региона Для переменных, которые должны изменять несколько потоков Как это сделать... Взаимное исключение или синхронизация «Замок», семафор, событие, критическая секция, атомическая операция (Lock, semaphore, event, critical section, atomic…) Правило (Rule of thumb): один «замок» (lock) на один элемент данных (здесь и дальше: «замок» - аналогия «блокировки»)
26 Задание 2 – вычисление интеграла Поставить комментарий на клаузу private и изучить реакцию Thread Checker
27 В помощь «многопоточному программисту»... Если создаете многопоточную программу, то обязательно... Явные общие и частные переменные должны быть соответствующим образом распределены между «HANDLEs» потоков Проанализировали ли Вы зависимости между оставшимися переменными? Как быть, если объем параллельного кода больше, чем 100 линий? Разделяемыми или общими являются переменные в вызываемых функциях? Можете ли Вы проконтролировать, когда указатели ссылаются на одну и ту же область памяти? Thread Checker – помощник «многопоточного» программиста Создайте потоковые функции (или OpenMP: параллельные регионы ) Выполните компиляцию и запуск программы в Thread Checker Изучите диагностику Измените директивы и/или структуру Возложите на Thread Checker выполнение черной работы
28 Тупики или бесконечные «зависания» (Deadlock) Возникает, когда поток ждет события, которое никогда не произойдет Чаще всего причины тупиков связаны с нарушением иерархической структуры «замков» (блокировок) Правильный порядок: сперва «наложить замок» затем «снять замок» (lock and un-lock in the same order) Избегать иерархических «замков», если можно
29 Тупики или бесконечные зависания – пример DWORD WINAPI threadA(LPVOID arg) { EnterCriticalSection(&L1); EnterCriticalSection(&L2); processA(data1, data2); LeaveCriticalSection(&L2); LeaveCriticalSection(&L1); return(0); } DWORD WINAPI threadB(LPVOID arg) { EnterCriticalSection(&L2); EnterCriticalSection(&L2); EnterCriticalSection(&L1); EnterCriticalSection(&L1); processB(data1, data2) ; processB(data1, data2) ; LeaveCriticalSection(&L1); LeaveCriticalSection(&L1);LeaveCriticalSection(&L2); return(0); return(0);} Поток A: L1, затем L2 Поток B: L2, затем L1 Поток A завладел L1 и удерживает его в надежде дождаться L2; но это никогда не наступит, так как L2 уже захватил поток B и удерживает в надежде захватить L1 – это тоже никогда не наступит
30 Тупики (Deadlock) - пример Правило: блокировка только одного элемента массива Нарушение правила: функция устанавливает блокировку на две переменные (при вызове – на два элемента массива). Поток 1 захватил Q[34] и ждет Q[986], поток 4 – захватил Q[986] и ждет Q[34] – возник «тупик» void swap (shape_t A, shape_t B) { lock(a.mutex); lock(b.mutex); // Swap data between A & B unlock(b.mutex); unlock(a.mutex); } typedef struct { // some data things SomeLockType mutex; } shape_t; shape_t Q[1024]; swap(Q[986], Q[34]); Thread 4 swap(Q[34], Q[986]); Thread 1 Захват Q[34] Захват Q[986]
31 Поток завис или «застрял»...(Thread Stalls) Поток ожидает неразумное количество времени – слишком долго Обычно он ждет ресурс Как правило, бывают вызваны «зависшими блокировками» Проверьте, что потоки освободили все наложенные блокировки
32 Что здесь неверно? int data; DWORD WINAPI threadFunc(LPVOID arg) { int localData; EnterCriticalSection(&lock); if (data == DONE_FLAG) return(1); localData = data; LeaveCriticalSection(&lock); process(local_data); return(0); } «Замок» никогда не будет снят Вход и выход в критическую секцию должны быть «парной операцией». А если data == DONE_FLAG, происходит возврат без освобождения критической секции
33 Задание 3 – тупик (deadlock – «замок» «намертво», «мертвый lock») В задаче поиска простых чисел установите комментарий на операцию освобождения критической секции Проверьте реакцию Intel® Thread Checker на возникший тупик
34 «Поточно-безопасные» функции Все функции, одновременно вызываемые несколькими потоками, должны быть «поточно-безопасными» Как протестировать на «безопасность» многопоточного выполнения? С помощью OpenMP и Thread Checker Организовать многопоточность с помощью OpenMP Примените конструкцию OpenMP «параллельные секции», чтобы организовать многопоточное выполнение
35 Пример тестирования «безопасности» многопоточного выполнения (Thread Safety) Здесь тестируется безопасность многопоточного выполнения для Одновременной работы нескольких вариантов routine1() Одновременного выполнения routine1() и routine2() Конструкция OPenMP «параллельные секции» - для тестирования всех комбинаций Необходимо только позаботиться о соответствующих наборах данных для каждого участка кода #pragma omp parallel sections { #pragma omp section routine1(&data1); #pragma omp section routine1(&data2); #pragma omp section routine2(&data3); }
36 Лучше сделать функцию используемой повторно, чем добавить синхронизацию Избежите возможного оверхеда Два способа обеспечить безопасность многопоточного выполнения Сделать функцию повторно используемой Любые переменные, изменяемые функцией, должны быть локальными для каждого вызова Не изменять разделяемых переменных Функции могут использовать взаимное исключение, чтобы избежать конфликтов с другими потоками Если только разделяемого доступа совсем невозможно избежать А если функции из третьей секции не поточно-безопасны? Скорее всего, придется управлять доступом потоков к библиотеке
37 Задание 4– тестирование на «безопасность» многопоточного выполнения (Thread Safety) Используйте OpenMP, чтобы организовать одновременное выполнение функций Вызов трех библиотечных функций= 6 комбинаций для тестирования A:A, B:B, C:C, A:B, A:C, B:C
38 Уровни инструментирования УровеньОписание «Полное изображение» Full Image Инструментируется любая инструкция, которая может генерировать диагностическое сообщение «Заказываемое изображение» Custom Image Same as Full Image except user can disable selected functions from instrumentation. All FunctionsTurns on full instrumentation for those parts of a module that were compiled with debugging information. Custom FunctionsSame as All Functions except user can disable selected functions from instrumentation. API ImportsOnly system API functions that are needed to be instrumented by the tool will be instrumented. No user code is instrumented. Module ImportsDisables instrumentation. This is default on system images, images without base relocations, and images not containing debug information. Более высокий уровень инструментирования требует больше памяти и времени для анализа, но дает более подробную картину Бинарное инструментирование понижает уровень от данного по умолчанию до необходимого успешного Устанавливаемый вручную уровень повышает скорость работы и управляет количеством собираемой информации
39 Огромное количество диагностик Что же делать, если у вас 5000 диагностик? В каком месте начинать отладку? Все ли сообщения одинаково важны? Как систематизировать и определить приоритеты Добавьте столбец 1st Access (первый доступ) Создайте группу 1st Access Создайте группы по Short Description (короткое описание)
40 Много диагностик... Add the 1 st Access column if it not already present
41 Много диагностик...
42 Много диагностик... Создайте группы ошибок, вызванных одной и той же строкой кода; каждая из групп может выглядеть так
43 Много диагностик... Сортируйте по Short description
44 Intel® Thread Checker Что он может... Легко выявляет ошибки многопоточного приложения, которые трудно выявить обычным способом Intel® Thread Checker находит Ошибки, которые необязательно должны произойти, чтобы быть выловленными Резко сокращает время отладки Повышает надежность приложения