Tools & Methods of Program Analysis (TMPA-2013)
Tsytelov, D., Trifanov, V., Devexperts LLC, St. Petersburg State University
Search of Race Conditions in Java Programs Based on Synchronization Contracts
BDD. The Outer Limits. Iosif Itkin at Youcon (in Russian)
TMPA-2013 Tsytelov Trifanov Devexperts
1. Динамический поиск гонок в Javaпрограммах на основе
синхронизационных контрактов
Дмитрий Цителов, Devexperts LLC
Виталий Трифанов, Devexperts LLC, мат-мех
СПбГУ
2. Состояния гонки
• 2+ потоков
• к одним и тем же данным
• хотя бы одно из них - запись
• Обычно это ошибка
3. Пример гонки
public class Account {
private int amount = 0;
public void deposit(int x) {amount += x;}
public int getAmount() {return amount;}
}
public class TestRace {
public static void main (String[] args) {
final Account a = new Account();
Thread t1 = depositAccountInNewThread(a, 5);
Thread t2 = depositAccountInNewThread(a, 6);
t1.join();
t2.join();
System.out.println(account.getAmount()); //5? 6? 11?.
}
}
4. Свойства гонок
• Опасны
– Повреждают глобальные данные
– Не приводят к немедленному отказу
программы
• Сложно обнаружить вручную
• Трудно воспроизводимы
5. Статический подход
• Анализ кода программы без её запуска
• Расширение языка, система типов
• Ограниченная глубина анализа
• Есть много утилит для Java
– FindBugs, jChord, etc.
6. Динамический подход
• Анализирует только текущий путь
выполнения
• Поддерживает все средства синхронизации
• Огромные накладные расходы
• Нет готовых детекторов для Java
7. Java Memory Model
• На синхронизационных событиях есть
отношение порядка «synchronized-with»
• Synchronized-with + порядок событий в
одном потоке = частичное отношение
порядка happens-before
• Отслеживаем happens-before с помощью
векторных(логических) часов Ламперта
• Метод точный, но много накладных
расходов
8. Идея
• Приложения используют библиотеки через API
• API хорошо документировано
– Класс XXX потокобезопасен
– Класс YYY непотокобезопасен
– ZZZ.get() синхронизирован с ZZZ.put()
• Опишем поведение API
• Исключим библиотеку из анализа
10. Типы связей
• Явные
• Простые
• Сигнатура метода:
retval owner.method(parameters)
• Любая комбинация простых связей
• Условия на возвращаемое значение
11. Примитивные простые явные связи
Owner
Owner
Param
Return value
Param
Return value
chm.put(k, v)
↓
chm.get(k)
ex.sumbit(task)
↓
task.run()
Наверное, тоже
бывает
chm.put(k, v)
↓
chm.get(k)
Наверное, тоже
бывает
Наверное, тоже
бывает
14. Язык контрактов
• Happens-before контракты
– пары методов
– всех методов класса
• Потокобезопасность метода
– ну, или всех методов класса (умеем «*»)
• Тип непотокобезопасного метода
– Read
– Write (по умолчанию)
16. Алгоритм
• В основе – отслеживание happens-before
• Операции синхронизации в SD:
– synchronized, volatile, thread start/join, …
– happens-before контракты
– не отслеживаем, если в контрактном методе
• Обращения к данным в RD (RD ∈ SD):
– к полям классов из RD
– вызовы непотокобезопасных методов классов,
не принадлежащих RD
18. Детали реализации
• Не сломать сериализацию
• Не давать разрастаться часам
• Хранение контрактов
• Не генерировать garbage
19. Экспериментальные результаты
Приложение
JTT
QD
MARS client
MARS Server
Режим
base
juc
base
juc
base
juc
base
juc
Синхр. оп-ий/мин
115К
28К
15М
7.2М
7.4 М
4.3 М
1650К
800К
Кол-во синх. часов
13К
7К
6.1K
0.2K
85K
72K
15К
14К
Контрактов/мин
2.3K
0.6K
209К
130К
980К
730К
360К
904К
0.75K 1.4М
1.4М
17К
24К
5.5К
5.5К
6
1
5
2
2
Кол-во контр. часов 8.5K
Найдено гонок
8
10
1
20. Ограничения
• Трактуем контракты как атомарные
– это неизбежно для неблокирующих средств
синхронизации – volatile, etc.
• Только контракты на основе простых связей
– Если сложней, то просто не описывать, детектор шагнёт
«внутрь» метода
– Lock.newCondition().await() – печаль
– ConcurrentMap.entrySet().iterator() – печаль
• Только контракты без сайд-эффектов
– Грубо говоря, только контейнеры
– Если Executor вызовет task.foo() вместо run() – печаль.
22. Спасибо!
• Пишите нам:
– drd-support@devexperts.com
– vitaly.trifanov@gmail.com
– tsitelov@acm.org
• Можно скачать и попробовать:
– http://code.devexperts.com/display/DRD/
– Это еще бета-версия
Notas do Editor
То есть без синхронизации конкуррентно кто-то пишет, а кто-то пишет и читаетСтрого говоря, есть race condition и data raceБывают benign races, но их тоже надо искать
Поджойнили 2 потока, смотрим, что будет. Хотели 11, но если перепишут друг друга – может быть 5 или 6
Рассказывали на пленарном докладе, так что краткоХуже дедлоков, те сразу видно (глазами, thread dump)Сложно по факту видимой ошибки отследить место, где она произошла гейзенбаг, очень сильно зависит от интерливинга не найти во время тестирования еще и потому что часто тестовое окружение немногоядерно
Model checking, верификацияСтатический анализРасширение системы типов или иное сообщение информацииБорются за точность и полноту, по факту и с тем и с тем проблемы, потому что NP-трудна и вынуждены ограничивать область анализаПлохо работают с нестатическими конструкциями – как только используем средства синхронизации, отличные от критических секций, начинаются проблемыЕсть готовые утилиты, как в индустрии, так и в ресерче
Не полон по определениюБорется за точность, небезуспешноПоддержка любых средств синхронизацииОгромные накладные расходы – нужно обрабатывать в рантайме дикие объемы данныхНет готовых инструментов. Нашли Tsan 4 Java, IBM MSDK, но они совершенно неадекватны
Есть потоки, они работают с данными и публикуют измененияМожно рассматривать как распределенную систему, где операции «публикации» (синхронизации) – сообщенияНа множестве синхронизационных операций задано отношение порядкаНа множестве всех операций в одном потоке – естественное отношение времениОбъединяем, транзитивно замыкаем, получаем частичное отношение порядка на всех события в программе. Если 2 обращения к данным упорядочены этим отношением, то между ними была синхронизация, и гонки нет. Если не упорядочены – гонка есть (определение из спецификации Java).Отслеживаем это отношение с помощью векторных часов, проверяем все операцииВекторные часы имеют размер O(N) и сложность O(N), отсюда огромные накладные расходыХотим снизить расходы, но точность не потерять
Объем кода приложения, как правило, сопоставим с объемом кода используемых библиотек. Библиотеки выбираются на этапе проектирования системы, поэтому предполагаются надежными. Мы не хотим искать в них гонки, хотим только в нашем коде. Ищем по нашим полям, и по вызовам методов чужих объектов.Не хотим отслеживать в них операции синхронизации. Нужно уметь описать не только факт потокобезопасности, но и передачу hb. Ровно это позволит избежать потери точности. Выкидываем код, описываем поведение на границе.Также опишем все примитивы синхронизации.Итого хотим описать частичную спецификацию. Есть подход контрактов, но там объективные сложности, плюс у нас нет исходников, мы не хотим править код, хотим описать аккуратно в сторонке и без деталей вроде пред\пост-условий и инвариантов.
Что мы хотим? Есть chm, обещает, что put предшествует последующему get. Надо уметь этот факт описать и обработать, а саму map исключить, т.е. Рассматривать как черный ящик. Связь через объект-владелец (одна и та же map) и первый параметр метода (один и тот же ключ)
В рамках нашего подхода работаем только с явными (через сигнатуру метода) простыми (1 к 1) связями.Итого их 6 типов. Умеем описать любую комбинацию + булево условие на возвращаемое значение, потому что некоторые контракты могут сработать и не сработать (CAS), что сообщается через возвращаемое значение
Работаем только с 3 типами, без возвращаемых значений. Для каждого типа есть примеры. С возвращаемыми значениями не сталкивались, знаем, что их мало. Повод для дальнейшего исследования.
Пример описания контракта пары методов. Вообще, контракты надо описывать целиком, так что можно описать контракт класса.
Итого умеем:Контракт пары методовКонтракт классаФакт того, что метод потокобезопасен (умеем wildcard)Тип непотокобезопасного метода (пример с list привести)
Крактко про алгоритм. Обычно RD == весь код. Мы выделяем 2 области, они вот так вклдываются.
В основе алгоритма лежит отслеживаниеhappens-beforeОперации синхронизации отслеживаем только в соответствующей области, в ней же – контракты. Если в контракте – поднимаем флажок и не отслеживаем операции синхронизации.Обращения к данным – только в нашем коде: поля и вызовы foreign-методовНе отслеживаем потокобезопасные вызовы
Чтобы встроиться в программу, мы используем технику инструментирования (трансформирования) байт-кода. Техника аналогична аспектному программированию. Java позволяет подключить к ней агента, который получает в виде массива байт каждый новый загружаемый класс и может его аккуратно модифицировать. Это мы и делаем – идем по байт-коду и находим интересующие нас операции. Видя их, спрашиваем у модуля конфигурации, нужно ли их обрабатывать и как. Если нужно – вставляем соответствующие вызовы модуля поиска гонок, в который вшит алгоритм happens-before.
Как я говорил, динамических детекторов нет. Одна из причин тому – существенные технические сложности, возникающие в процессе. Нам удалось решить многие в достаточной степени, чтоб поставить эксперименты.Так, модифицирование байт-кода сразу же ломает сериализацию. Исправили.Часы имеют размер, пропорциональный количеству потоков в программе, а значит регулярное порождение новых потоков приводит к неконтроллируемому росту потребления памяти, а завершение потоков – к утечкам. Решили.Наконец, контракты нужно хранить в каком-то месте, из которого мы их сможем достать, то есть нужны некоторые ключи. Поскольку контракты в общем случае имеют произвольный вид, то и ключи произвольные – их нужно генерировать на лету, отсюда тоже множество проблем с загрузкой классов.Наконец, важно не плодить новые сущности, потому что из-за количества операций мигом взлетит потребление мусора.
Мы запускались на трёх приложениях, которые на наш взгляд позволили в достаточной степени проверить детектор. JTT – небольшое клиентское приложение к баг-трекеру JIRA, 10 потоков, 400 классов, 2.5-3 мб библиотек.QD – нагрузочный тест адского протокола доставки котировок, весь на волатайлах и ансейфах – 15 потоков, 700 классовMARS – клиент-серверная мониторинговая система, в совокупности порядка 30 потоков, 2000 классов, около 10-11 мб библиотек.2 режима, в обоих гонки искали только в com.devexperts.*В базовом режиме отслеживали только базовые примитивы + unsafe, в juc – описали и принудительно исключили jucОсновные результаты: часов стало меньше, операций стало меньше, точность не потеряли.
Самый неприятный слайдНаш подход скругляет углы у happens-before подхода и безусловно имеет ряд ограничений.Эти ограничения не помешали нам получить прирост производительности и избежать потери точности в наших экспериментах, но, безусловно, мы отдаем себе отчёт в том, что можно придумать класс программ, для которого мы будем терять точность. Работа над классификацией таких программ и постановкой дальнейших экспериментов является одной из наших дальнейших ключевых задач.Во-первых, мы трактуем вызовы контрактных методов как атомарные. С этим связан неотъемлимый вопрос – отслеживать их до вызова или после вызова? Тут возможны потери точности, но это essential проблема обработки любых средств синхронизации, не связанных с блокировками и fork/join.Во-вторых, мы отслеживаем контракты на основе явных простых связей. Поскльку мы умеем отслеживать все низкоуровневые средства синхронизации Java, то можно просто не отслеживать контракты, которые мы не можем описать и тогда детектор шагнёт вглубь и обработает события внутри них. Такой подход не сработает, если контракт класса уже частично описан, потому что контракт надо описать полностью.Наконец, мы трактуем контракты как черный ящик с внешними входами и выходами, но у него могут быть внутренние входы и выходы. Практические наблюдения показывают, что таких мало, но наверняка они есть – какой-нибудь алгоритм, который работает с live view wait-free структуры данных. Это важное направление дальнейших исследований.С другой стороны, по каждому из ограничений есть понимание, что оно подтверждено практическими наблюдениями.
Мы представили модификацию алгоритма happens-before, в которой предлагаем ограничить область отслеживания гонок и область отслеживания операций синхронизации, чтобы получить прирост производительности. Чтобы избежать потери точности мы описываем частичную спецификацию поведения методов в многопоточной среде с помощью специально созданного языка. Это не требует модификации исходного кода программы, поэтому подход применим к любым программам.Контракты можно переиспользовать, и в этом смысле их составление выгодно. Также данный подход может быть применим в модульном тестировании.Эксперименты подтвердили наши ожидания – точность не теряем, производительность растет, накладные расходы падают. В нашем подходе отслеживает меньше операций «случайной» синхронизации, что опосредованно увеличивает точность поиска гонок.Наконец, нам удалось получить решить ряд существенных технических сложностей и получить рабочий детектор.Результаты нас радуют и обнадеживают, однако впереди много работы. Во-первых, необходимо доработать код и открыть исходники. Во-вторых, нужно доработать язык описания контрактов (интерфейсы, описание «внутренних» точек выхода из контрактов) и доработать детектор – внедрить поддержку контрактов с возвращаемым значением.Нужно исследовать неявные контракты – что мы с ними вообще можем сделать.Наконец, необходимо поставить больше экспериментов на крупных опен-сорс проектах (Eclipse).Однако мы понимаем, что наш подход обладает рядом существенных ограничений, и вполне может быть класс программ, на которых мы получим потерю точности.
Спасибо за внимание, надеюсь вам понравилось!Вопросы можно задавать нам или писать на указанные email-адреса.Сейчас бета-версия утилиты находится в открытом доступе и готова к использованию, есть базовая документация. В принципе, по запросу можем предоставить исходники.