440 likes | 642 Views
Поиск ошибок в многопоточном приложении (на примере Thread Checker ). ЛЕКЦИЯ 9, часть 1. ЛИТЕРАТУРА. 99% - по материалам тренинга Intel для преподавателей ВУЗов, апрель 2006, Нижний Новгород, «свободный перевод» Калининой А.П. Цели и задачи. Научиться
E N D
Поиск ошибок в многопоточном приложении (на примере Thread Checker) ЛЕКЦИЯ 9, часть 1
ЛИТЕРАТУРА • 99% - по материалам тренинга Intelдля преподавателей ВУЗов, апрель 2006, Нижний Новгород, «свободный перевод» Калининой А.П.
Цели и задачи • Научиться • Применять Thread Checker для тестирования правильности работы приложений на основе Windows* threads, поиска и ликвидации разнообразных ошибок, связанных с взаимодействием потоков • Определять, безопасны ли библиотечные функции для многопоточного выполнения (не возникают ли ошибки при параллельном выполнении нескольких функций: если возникают, то «небезопасны»)
Содержание • Что такоеIntel® Thread Checker... • Определение условий возникновения гонок данных (race conditions) • Thread Checker – помощник «многопоточного» программиста • Другие ошибки многопоточного приложения • Проверка библиотеки на безопасность многопоточного выполнения • Другие возможностиThread Checker
Зачем это нужно... • Создание многопоточного приложения может оказаться сложной задачей • Новый класс проблем возникает из-за взаимодействия одновременно работающих потоков (concurrent threads) • Гонки данных или конфликты памяти (Data races or storage conflicts) • Больше, чем один поток имеет доступ к изменению данных без синхронизации измененных значений • Тупики (Deadlocks ) • Потоки ждут события, которое никогда не наступит
Intel® Thread Checker • Инструмент для отладки многопоточных программ • Находит ошибки в многопоточных программах на основе Win32*, POSIX* и OpenMP* • Быстро находит такие ошибки, на выявление которых традиционным способом (по результатам работы многопоточной программы) требуется несколько дней • Находит источник ошибки, а не ее проявления • Ошибка не обязательно должна случиться, чтобы быть выявленной • «Вставлен» в среду VTune™ Performance Analyzer • Тот же самый интерфейс среды VTune™
Как и что можно анализировать с помощью 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 в качестве определяемых пользователем синхронизационных примитивов
Выявляет причины ошибок и предлагает способы их ликвидации. • Предлагает на выбор набор API в качестве определяемых пользователем синхронизационных примитивов Контекстное меню (щелчок правой кнопкой): выбор помощи по результатам диагностики (Diagnostic Help) Помощь Thread Checker
Thread Checker: выполнение анализа • Способ выполнения анализа: динамически, во время работы приложения • Производится мониторинг: • Потоков и используемых синхронизационных примитивов API • Последовательности выполнения в каждом потоке • Последовательности взаимодействия потоков • Доступа потоков к памяти Анализируемый код должен быть выполнен, чтобы анализ стал возможным
Thread Checker: перед запуском необходимо учесть, что... • Инструментирование: необходимо помнить, что... • Добавляется обращение к библиотеке для записи информации • О работе потоков и объектов синхронизации API • О доступе к памяти • Увеличивается время выполнения и объем выполняемого кода • В тестируемом приложении рекомендуется применять малый объем обрабатываемых данных (workloads) • Время выполнения и объем приложения увеличиваются • Несколько запусков с различными потоками выполнения дадут более полную картину Для конечного результата важно, какого рода «загрузку» Вы осуществили
Требования к загружаемому приложению • Чтобы выделить проблемный код для каждого потока: • Действуйте под девизом: чем меньше, тем лучше! • Минимизируйте набор обрабатываемых данных • Уменьшайте объем изображения... • Минимизируйте количество итераций в цикле или временных шагов • Моделируйте минуты, а не дни... • Уменьшайте скорость обновления значений переменных • Меньше кадров в секунду... Тогда найдете ошибки многопоточного приложения быстрее!
Подготовка приложения для анализа Thread Checker • Компиляция • Используйте многопоточно - безопасные динамические библиотеки (/MD, /MDd) • Включите генерацию символьной информации (/Zi, /ZI, /Z7) • Отключите оптимизацию (/Od) • «Линкование» (Link ) • Нужно сохранить символьную информацию (/debug) • Определить релоцированные секции (Specify relocatable code sections) (/fixed:no)
Бинарное инструментирование (Binary Instrumentation) • Выполняется для поддерживаемых компиляторов • Запуск приложения • Должен быть выполнен из-под Thread Checker • Приложение инструментируется во время выполнения • Также применяются внешние инструментированные динамические библиотеки (DLLs)
Инструментирование исходного кода (Source Instrumentation) • Компиляторы Intel® C++ или Fortran • Компилировать с/Qtcheck • Выполнение приложения • Запуск в среде VTune™ • Запуск из-под командной строки Windows* • Полученные данные размещаются в файле результатов threadchecker.thr • Просмотр результатов (.thr file) в среде VTune • Дополнительные динамические библиотеки (DLLs) не инструментируются и не анализируются Это дает более подробную диагностику
Intel® Thread Checker Wizard Intel® Thread Profiler Wizard Advanced Activity Configuration Intel® Thread Checker Wizard Threading Wizards Выбрать Threading Wizards Выбрать визард Thread Checker Запуск Thread Checker
Сортировка диагностик по группам
Форма представления «участки кода, ответственные за...»
Правой кнопкой мыши. . . Более подробная помощь Помощь по диагностикам
Задание 1 • Откомпилируйте и выполните последовательную версию поиска простых чисел без Thread Checker • Откомпилируйте и выполните многопоточную версиюбез Thread Checker • Выполните приложение в Thread Checker, чтобы определить источники ошибок
S1: A = 1.0; S2: B = A + 3.14; S3: A = 1/3 * (C – D); . . . . . . . . . . . . S4: A = (B * 3.8) / 2.7; Анализ зависимостей Рассмотрим данный последовательный код: • Пусть 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(запись раньше записи)
Зависимости, обнаруженные Thread Checker • Зависимость «вывода» (output dependence) • Конфликты «запись-запись» (Write-Write conflict):один из потоков успевает изменить значение переменной раньше,чем согласно последовательному алгоритму ее должен изменить другой поток • «Противопотоковая» зависимость (Anti-dependence) • Конфликты «чтение-запись» (Read-Write conflict): один из потоков успевает считать значение переменной раньше,чем согласно последовательному алгоритму ее должен изменить другой поток • «Потоковая зависимость» (Flow dependence) • Конфликты «запись-чтение» (Write-Read conflict): один из потоков успевает изменить значение переменной раньше,чем согласно последовательному алгоритму ее должен считатьдругой поток
Условия (Race Conditions) возникновения гонки данных • Не определен жестко порядок выполнения операций • Одновременный доступ к одной переменной нескольких потоков • Это наиболее популярная ошибка в многопоточных программах • Эта ошибка может быть далеко не очевидной и трудно выявляемой
Как уничтожить возможность возникновения гонки данных ... • Решение:Ограничить видимостьпеременных пределами каждого потока • Когда можно ограничить видимость переменных… • Для значений, не используемых вне параллельного региона • Для промежуточных или «рабочих» (“work”) переменных • Как это сделать… • Применять клаузы OpenMP, определяющие пределы видимости (OpenMP scoping clauses (private, shared)) • Описывать переменные в пределах потоковой функции • Выделять память в пределах стека потока (Allocate variables on thread stack) • Сохранять в потоке API (TLS (Thread Local Storage) API)
Как уничтожить возможность возникновения гонки данных ...(продолжение) • Решение:контролировать разделяемый доступ с помощью выделения критических регионов (critical regions) • Когда использовать контролируемый доступ… • Для величин, используемых вне параллельного региона • Для переменных, которые должны изменять несколько потоков • Как это сделать... • Взаимное исключение или синхронизация • «Замок», семафор, событие, критическая секция, атомическая операция (Lock, semaphore, event, critical section, atomic…) • Правило (Rule of thumb): один «замок» (lock) на один элемент данных (здесь и дальше: «замок» - аналогия «блокировки»)
Задание 2 – вычисление интеграла • Поставить комментарий на клаузу privateи изучить реакцию Thread Checker
Возложите на Thread Checker выполнение “черной работы” В помощь «многопоточному программисту»... • Если создаете многопоточную программу, то обязательно... • Явные общие и частные переменные должны быть соответствующим образом распределены между «HANDLEs» потоков • Проанализировали ли Вы зависимости между оставшимися переменными? • Как быть, если объем параллельного кода больше, чем 100 линий? • Разделяемыми или общими являются переменные в вызываемых функциях? • Можете ли Вы проконтролировать, когда указатели ссылаются на одну и ту же область памяти? • Thread Checker – помощник «многопоточного» программиста • Создайте потоковые функции (или OpenMP: параллельные регионы ) • Выполните компиляцию и запуск программы в Thread Checker • Изучите диагностику • Измените директивы и/илиструктуру
Тупики или бесконечные «зависания» (Deadlock) • Возникает, когда поток ждет события, которое никогда не произойдет • Чаще всего причины тупиков связаны с нарушением иерархической структуры «замков» (блокировок) • Правильный порядок: сперва «наложить замок» затем «снять замок» (lock and un-lock in the same order) • Избегать иерархических «замков», если можно
Поток A завладел L1 и удерживает его в надежде дождаться L2; но это никогда не наступит, так как L2 уже захватил поток B и удерживает в надежде захватить L1 – это тоже никогда не наступит Тупики или бесконечные зависания – пример • DWORD WINAPI threadA(LPVOID arg) • { • EnterCriticalSection(&L1); • EnterCriticalSection(&L2); • processA(data1, data2); • LeaveCriticalSection(&L2); • LeaveCriticalSection(&L1); • return(0); • } Поток B: L2, затем L1 DWORD WINAPI threadB(LPVOID arg) { EnterCriticalSection(&L2); EnterCriticalSection(&L1); processB(data1, data2) ; LeaveCriticalSection(&L1); LeaveCriticalSection(&L2); return(0); } Поток A: L1, затем L2
Thread 4 swap(Q[986], Q[34]); Thread 1 Захват Q[34] Захват Q[986] swap(Q[34], Q[986]); Тупики (Deadlock) - пример • Правило: блокировка только одного элемента массива • Нарушение правила: функция устанавливает блокировку на две переменные (при вызове – на два элемента массива). Поток 1 захватил Q[34]и ждет Q[986], поток 4 – захватил Q[986] и ждет Q[34] – возник «тупик» typedef struct { // some data things SomeLockType mutex; } shape_t; shape_t Q[1024]; 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); }
Поток завис или «застрял»...(Thread Stalls) • Поток ожидает неразумное количество времени – слишком долго • Обычно он ждет ресурс • Как правило, бывают вызваны «зависшими блокировками» Проверьте, что потоки освободили все наложенные блокировки
«Замок» никогда не будет снят Вход и выход в критическую секцию должны быть «парной операцией». А если data == DONE_FLAG, происходит возврат без освобождения критической секции Что здесь неверно? • intdata; • DWORD WINAPI threadFunc(LPVOID arg) • { • int localData; • EnterCriticalSection(&lock); • if (data == DONE_FLAG) • return(1); • localData = data; • LeaveCriticalSection(&lock); • process(local_data); • return(0); • }
Задание 3 – тупик (deadlock – «замок» «намертво», «мертвый lock») • В задаче поиска простых чисел установите комментарий на операцию освобождения критической секции • Проверьте реакцию Intel® Thread Checkerнавозникший тупик
«Поточно-безопасные» функции • Все функции, одновременно вызываемые несколькими потоками, должны быть «поточно-безопасными» • Как протестировать на «безопасность» многопоточного выполнения? • С помощью OpenMP и Thread Checker • Организовать многопоточность с помощью OpenMP • Примените конструкцию OpenMP «параллельные секции», чтобы организовать многопоточное выполнение
Здесь тестируется безопасность многопоточного выполнения для Одновременной работы нескольких вариантов routine1() Одновременного выполнения routine1()иroutine2() Конструкция OPenMP «параллельные секции» - для тестирования всех комбинаций Необходимо только позаботиться о соответствующих наборах данных для каждого участка кода Пример тестирования «безопасности» многопоточного выполнения (Thread Safety) • #pragma omp parallel sections { #pragma omp section routine1(&data1); #pragma omp section routine1(&data2); #pragma omp section routine2(&data3); }
Лучше сделать функцию используемой повторно, чем добавить синхронизацию • Избежите возможного оверхеда Два способа обеспечить безопасность многопоточного выполнения • Сделать функцию повторно используемой • Любые переменные, изменяемые функцией, должны быть локальными для каждого вызова • Не изменять разделяемых переменных • Функции могут использовать взаимное исключение, чтобы избежать конфликтов с другими потоками • Если только разделяемого доступа совсем невозможно избежать • А если функции из третьей секции не поточно-безопасны? • Скорее всего, придется управлять доступом потоков к библиотеке
Задание 4– тестирование на «безопасность» многопоточного выполнения (Thread Safety) • Используйте OpenMP, чтобы организовать одновременное выполнение функций • Вызов трех библиотечных функций= 6 комбинаций для тестирования • A:A, B:B, C:C, A:B, A:C, B:C
Более высокий уровень инструментирования требует больше памяти и времени для анализа, но дает более подробную картину Бинарное инструментирование понижает уровень от данного по умолчанию до необходимого успешного Устанавливаемый вручную уровень повышает скорость работы и управляет количеством собираемой информации Уровни инструментирования
Огромное количество диагностик • Что же делать, если у вас 5000 диагностик? • В каком месте начинать отладку? • Все ли сообщения одинаково важны? • Как систематизировать и определить приоритеты • Добавьте столбец “1st Access” (первый доступ) • Создайте группу “1st Access” • Создайте группы по “Short Description” (короткое описание)
Много диагностик... Add the “1st Access” column if it not already present
Создайте группы ошибок, вызванных одной и той же строкой кода; каждая из групп может выглядеть так Много диагностик...
Сортируйте по “Short description” Много диагностик...
Intel® Thread CheckerЧто он может... • Легко выявляет ошибки многопоточного приложения, которые трудно выявить обычным способом • Intel® Thread Checker находит • Ошибки, которые необязательно должны произойти, чтобы быть выловленными • Резко сокращает время отладки • Повышает надежность приложения