Проблемы с потоками и их отладка - PullRequest
6 голосов
/ 18 августа 2010

Это мое продолжение предыдущего поста по вопросам управления памятью. Ниже приведены проблемы, которые я знаю.

1) гонки данных (нарушения атомарности и повреждение данных)

2) проблемы с заказом

3) неправильное использование замков, приводящее к мертвым замкам

4) гейзенбагов

Есть ли другие проблемы с многопоточностью? Как их решить?

Ответы [ 7 ]

2 голосов
/ 18 августа 2010

К сожалению, нет хорошей таблетки, которая помогает автоматически решить большинство / все проблемы с потоками. Даже модульные тесты, которые так хорошо работают с однопоточными фрагментами кода, могут никогда не обнаружить чрезвычайно тонкое состояние гонки.

Одна вещь, которая поможет, - это сохранить данные о взаимодействии потоков в объектах. Чем меньше интерфейс / область действия объекта, тем легче будет обнаружить ошибки при проверке (и, возможно, при тестировании, но условия гонки могут быть проблемой для обнаружения в тестовых случаях). Сохраняя простой интерфейс, который можно использовать, клиенты, использующие этот интерфейс, также будут корректны по умолчанию. Создавая более крупную систему из множества более мелких элементов (лишь немногие из которых на самом деле взаимодействуют с потоками), вы можете в первую очередь избежать ошибок при обработке потоков.

2 голосов
/ 19 августа 2010

Список Эрика из четырех выпусков в значительной степени заметен. Но отладить эти проблемы сложно.

Для тупика я всегда предпочитал "выровненные замки". По сути, вы даете каждому типу блокировки номер уровня. А затем потребуйте, чтобы поток aquire блокировал монотонно.

Чтобы сделать выровненные блокировки, вы можете объявить такую ​​структуру:

typedef struct {
   os_mutex actual_lock;
   int level;
   my_lock *prev_lock_in_thread;
} my_lock_struct;

static __tls my_lock_struct *last_lock_in_thread;

void my_lock_aquire(int level, *my_lock_struct lock) {
    if (last_lock_in_thread != NULL) assert(last_lock_in_thread->level < level)
    os_lock_acquire(lock->actual_lock)
    lock->level = level
    lock->prev_lock_in_thread = last_lock_in_thread
    last_lock_in_thread = lock
}

Что хорошо в выровненных замках, так это вероятность того, что тупик вызывает утверждение. И с некоторой дополнительной магией с FUNC и LINE вы точно знаете, какую злобу сделал ваш поток.

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

В компании, в которой я работаю (http://www.corensic.com), появился новый продукт под названием Jinx, который активно ищет случаи, когда могут быть выявлены условия гонки. Это достигается с помощью технологии виртуализации для управления чередованием потоков на различных процессорах и увеличения связи между процессорами.

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

Jinx особенно хорош в поиске ошибок в структурах данных без блокировки. Это также очень хорошо при поиске других условий гонки. Круто, что нет ложных срабатываний. Если тестирование вашего кода приближается к состоянию гонки, Jinx помогает коду пойти по неверному пути. Но если плохой путь не существует, вы не будете получать ложные предупреждения.

1 голос
/ 19 августа 2010

-> Добавить инверсию приоритета к этому списку.

Как еще один плакат, к которому обращаются, файлы журналов - замечательные вещи.Для взаимоблокировок использование LogLock вместо Lock может помочь точно определить, когда ваши объекты перестают работать.То есть, как только вы узнаете, что у вас есть тупик, журнал сообщит вам, когда и где были созданы и выпущены блокировки.Это может быть чрезвычайно полезно для отслеживания этих вещей.

Я обнаружил, что условия гонки при использовании модели актера после того же сообщения -> подтвердить-> подтвердить полученный стиль, похоже, исчезают.Тем не менее, YMMV.

1 голос
/ 19 августа 2010
  1. Остерегайтесь глобальных переменных, даже если они const, в частности в C ++. Только POD, которые статически инициализированные "а-ля" C здесь хороши. Как только конструктор во время выполнения вступает в игру, быть чрезвычайно осторожный. Порядок инициализации AFAIR переменных со статической связью, которые находятся в разные блоки компиляции вызывается в неопределенном порядке. Может быть C ++ классы, которые инициализируют все их члены правильно и имеют пустое тело функции, может быть в порядке в наше время, но у меня когда-то был плохой опыт с этим тоже.

    Это одна из причин, почему на POSIX сторона pthread_mutex_t очень проще программировать, чем sem_t: это имеет статический инициализатор PTHREAD_MUTEX_INITIALIZER.

  2. Критические разделы должны быть такими короткими, как возможно по двум причинам: возможно быть более эффективным в конце, но что более важно, это легче поддерживать и отлаживать.

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

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

  3. Регистрация может быть затруднена, если много темы начинают писать одновременно время. Если каждый поток делает разумный объем работы стараюсь пусть каждый из них напишет файл своего собственный, чтобы они не блокировались друг друга.

    Но будьте осторожны, регистрация изменений поведения кода. Это может быть плохо, когда ошибки исчезают или полезны, когда ошибки кажется, что иначе заметил.

    сделать посмертный анализ такой беспорядок вы должны иметь точные метки времени на каждой строке так что все файлы могут быть объединить и дать вам связное представление исполнения.

1 голос
/ 18 августа 2010

Сделайте ваши темы максимально простыми.

Старайтесь не использовать глобальные переменные.Глобальные константы (фактические константы, которые никогда не меняются) - это хорошо.Когда вам нужно использовать глобальные или общие переменные, вам нужно защитить их с помощью мьютекса / блокировки (семафор, монитор, ...).

Убедитесь, что вы действительно понимаете, как работают ваши мьютексы.Существует несколько различных реализаций, которые могут работать по-разному.

Постарайтесь организовать свой код так, чтобы критические секции (места, где вы держите какой-либо тип блокировки) были как можно быстрее.Имейте в виду, что некоторые функции могут блокироваться (спят или ждут чего-либо и не позволяют ОС продолжать работу этого потока в течение некоторого времени).Не используйте их во время удержания каких-либо блокировок (за исключением случаев, когда это абсолютно необходимо или во время отладки, поскольку иногда могут отображаться другие ошибки).

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

Предотвращение тупиковых ситуаций требует планирования.Старайтесь избегать приобретения более одного замка одновременно.Если это неизбежно, выберите порядок, который вы будете использовать для получения и снятия блокировок для всех потоков.Убедитесь, что вы знаете, что на самом деле означает тупик.

Отладка многопоточных или распределенных приложений затруднена.Если вы можете выполнять большую часть отладки в однопоточной среде (возможно, даже просто заставляя другие потоки спать), вы можете попытаться устранить ошибки, не связанные с многопоточностью, прежде чем переходить к многопоточной отладке.

Всегда думайтео том, что другие темы могут быть до.Прокомментируйте это в своем коде.Если вы делаете что-то определенным образом, потому что знаете, что в то время никакие другие потоки не должны получать доступ к определенному ресурсу, напишите большой комментарий, говорящий так.

Возможно, вы захотите заключить вызовы в блокировки / разблокировки мьютекса в другихфункции, такие как:

int my_lock_get (блокировка lock_type, файл const char *, строка без знака, const char * msg) {

 thread_id_type me = this_thread();

 logf("%u\t%s (%u)\t%s:%u\t%s\t%s\n", time_now(), thread_name(me), me, "get", msg);

 lock_get(lock);

 logf("%u\t%s (%u)\t%s:%u\t%s\t%s\n", time_now(), thread_name(me), me, "in", msg);

} ​​

И аналогичная версия дляразблокировать.Обратите внимание, что функции и типы, используемые в этом, все составлены и не слишком основаны на каком-либо одном API.

Используя что-то подобное, вы можете вернуться в случае ошибки и использовать Perl-скрипт или что-то подобноезапускать запросы к вашим журналам, чтобы выяснить, где что-то пошло не так (например, при сопоставлении блокировок и разблокировок).

Обратите внимание, что функциональность печати или ведения журналов может также нуждаться в блокировках.Во многих библиотеках это уже встроено, но не во всех.Эти блокировки не должны использовать печатную версию функций lock_ [get | release], иначе у вас будет бесконечная рекурсия.

1 голос
/ 18 августа 2010

Как решить [проблемы с многопоточностью]?

Хороший способ «отладки» MT-приложений - ведение журнала. Хорошая библиотека журналов с широкими возможностями фильтрации делает это проще. Конечно, сама регистрация влияет на время, поэтому у вас все еще могут быть «heisenbugs», но это гораздо менее вероятно, чем когда вы действительно врываетесь в отладчик.

Подготовьтесь и спланируйте это. Включите в ваше приложение хорошее средство регистрации с самого начала.

1 голос
/ 18 августа 2010

Четырьмя наиболее распространенными проблемами с продюсированием являются

1-тупик
2-живой замок
3-ходовые условия
4-голодание

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...