Какие уроки вы извлекли из проекта, который почти / фактически провалился из-за плохой многопоточности? [закрыто]

11

Какие уроки вы извлекли из проекта, который почти / фактически провалился из-за плохой многопоточности?

Иногда структура навязывает определенную модель потоков, которая усложняет процесс на порядок.

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

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

Я не могу обработать графический интерфейс, в котором некоторую работу можно выполнять только в строго сериализованном потоке («основной поток»), а другую работу можно выполнять только в любом потоке, кроме основного потока («рабочие потоки»), и где данные и сообщения должны перемещаться во всех направлениях между N компонентами (полностью связанный график).

В то время, когда я оставил этот проект для другого, повсюду были проблемы с тупиком. Я слышал, что через 2-3 месяца нескольким другим разработчикам удалось устранить все проблемы взаимоблокировки до такой степени, что они могут быть отправлены клиентам. Мне так и не удалось обнаружить ту недостающую часть знаний, которой мне не хватает.

Кое-что о проекте: число идентификаторов сообщений (целочисленные значения, которые описывают значение события, которое может быть отправлено в очередь сообщений другого объекта, независимо от потоков), исчисляется несколькими тысячами. Уникальные строки (пользовательские сообщения) также встречаются около тысячи.

добавленной

Лучшая аналогия, которую я получил от другой команды (не связанной с моими прошлыми или настоящими проектами), состояла в том, чтобы «поместить данные в базу данных». («База данных» относится к централизации и элементарным обновлениям.) В графическом интерфейсе пользователя, который фрагментирован на несколько представлений, выполняющихся в одном и том же «главном потоке», и вся тяжелая работа без графического интерфейса выполняется в отдельных рабочих потоках, данные приложения должны храниться в одном месте, которое действует как база данных, и позволяет «базе данных» обрабатывать все «атомарные обновления», связанные с нетривиальными зависимостями данных. Все остальные части GUI просто обрабатывают рисование экрана и ничего больше. Части пользовательского интерфейса могут кешировать содержимое, и пользователь не заметит, если он устарел на долю секунды, если он спроектирован правильно. Эта «база данных» также известна как «документ» в архитектуре Document-View. К сожалению - нет, мое приложение фактически хранит все данные в представлениях. Я не знаю, почему это так.

Другие участники:

(Участникам не нужно использовать реальные / личные примеры. Уроки из отдельных примеров, если вы считаете, что они заслуживают доверия, также приветствуются.)

rwong
источник
Я думаю, что умение «мыслить нитями» - это своего рода талант, а не то, чему можно научиться, из-за отсутствия лучшей формулировки. Я знаю многих разработчиков, которые работали с параллельными системами в течение очень долгого времени, но они задыхаются, если данные должны идти в нескольких направлениях.
доуфи

Ответы:

13

Мой любимый урок - очень тяжело победил! - в том, что в многопоточной программе планировщик - это хитрая свинья, которая вас ненавидит. Если что-то может пойти не так, как надо, но неожиданным образом. Получите что-то не так, и вы будете гоняться за странными heisenbugs (потому что любой инструмент, который вы добавите, изменит время и даст вам другой способ запуска).

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

Donal Fellows
источник
+1 за «не делить память (или другие ресурсы) между потоками, за исключением сообщений, которые должны быть асинхронными;»
Неманя Трифунович
1
Только путь? Как насчет неизменных типов данных?
Аарона
is that in a multithreaded program the scheduler is a sneaky swine that hates you.- нет, это не так, он делает именно то, что вы сказали, чтобы сделать :)
Mattnz
@Aaronaught: глобальные значения, передаваемые по ссылке, даже если они неизменны, все еще требуют глобального GC, и это вновь вводит целую кучу глобальных ресурсов. Возможность использовать управление памятью для каждого потока - это хорошо, так как позволяет избавиться от целого ряда глобальных блокировок.
Donal Fellows
Дело не в том, что вы не можете передавать значения неосновных типов по ссылке, а в том, что для этого требуются более высокие уровни блокировки (например, «владелец» удерживает ссылку до тех пор, пока не вернется какое-либо сообщение, что легко испортить при обслуживании) или сложный код в службе сообщений для передачи права собственности. Или вы все упорядочиваете и разархивируете в другом потоке, который намного медленнее (вы все равно должны это делать при переходе в кластер). Происходить в погоне и вообще не делиться памятью.
Donal Fellows
6

Вот несколько основных уроков, которые я могу вспомнить прямо сейчас (не из проектов, терпящих неудачу, а из реальных проблем, наблюдаемых в реальных проектах):

  • Старайтесь избегать любых блокирующих звонков, удерживая общий ресурс. Обычный шаблон взаимоблокировок - поток захватывает мьютекс, выполняет обратный вызов, блокирует обратный вызов на том же мьютексе.
  • Защитите доступ к любым разделяемым структурам данных с помощью мьютекса / критической секции (или используйте блокировку без блокировки - но не изобретайте свою собственную!)
  • Не предполагайте атомарность - используйте атомарные API (например, InterlockedIncrement).
  • RTFM относительно безопасности потоков используемых вами библиотек, объектов или API.
  • Воспользуйтесь преимуществами доступных примитивов синхронизации, например, событий, семафоров. (Но при их использовании обратите особое внимание на то, что вы знаете, что находитесь в хорошем состоянии - я видел много примеров событий, сигнализированных в неправильном состоянии, так что события или данные могут быть потеряны)
  • Предположим, что потоки могут выполняться одновременно и / или в любом порядке, и этот контекст может переключаться между потоками в любое время (если только под ОС, которая дает другие гарантии).
Гай Сиртон
источник
6
  • Весь ваш GUI- проект должен вызываться только из основного потока . По сути, вы не должны помещать один (.net) «вызов» в ваш графический интерфейс. Многопоточность должна зависать в отдельных проектах, которые обрабатывают более медленный доступ к данным.

Мы унаследовали часть, где проект GUI использует дюжину потоков. Это не дает ничего, кроме проблем. Тупики, проблемы с гонками, вызовы GUI с несколькими потоками ...

Карра
источник
Означает ли «проект» «сборка»? Я не понимаю, как распределение классов между сборками может вызвать проблемы с многопоточностью.
nikie
В моем проекте это действительно сборка. Но главное в том, что весь код в этих папках должен вызываться из основного потока, без исключений.
Карра
Я не думаю, что это правило в целом применимо. Да, вы никогда не должны вызывать код GUI из другого потока. Но то, как вы распределяете классы по папкам / проектам / сборкам, является самостоятельным решением.
nikie
1

В Java 5 и более поздних версиях есть Executors, которые призваны облегчить работу с многопоточными программами в стиле fork-join.

Используйте их, это снимет много боли.

(и, да, это я узнал из проекта :))


источник
1
Чтобы применить этот ответ к другим языкам - по возможности используйте высококачественные среды параллельной обработки, предоставляемые этим языком. (Тем не менее, только время покажет , является ли структура действительно большой и очень полезная.)
rwong
1

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

  • Правило № 1: ПОЦЕЛУЙ - Если нить не нужна, не крути ее. Сериализация как можно больше.
  • Правило № 2: не ломайте № 1.
  • # 3 Если вы не можете доказать через обзор, это правильно, это не так.
mattnz
источник
+1 за правило 1. Я работал над проектом, который изначально собирался блокировать, пока не завершился другой поток - по сути, вызов метода! К счастью, мы решили отказаться от такого подхода.
Майкл К
# 3 FTW. Лучше потратить часы на борьбу с временными диаграммами блокировки или чем-то еще, что вы используете, чтобы доказать, что это хорошо, чем месяцы, задаваясь вопросом, почему он иногда разваливается.
1

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

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

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

Карл Билефельдт
источник
0

Попробуйте сделать это снова.

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

Я думаю, что отладка - это то, что действительно затрудняет. Я могу отлаживать многопоточный код, используя VS, но я действительно в полной растерянности, если мне придется использовать GDB. Моя вина, наверное.

Еще одна вещь, о которой узнают больше, - это блокировка структур данных.

Я думаю, что этот вопрос может быть действительно улучшен, если вы укажете рамки. Например, пулы потоков .NET и фоновые рабочие действительно отличаются от QThread. Всегда есть несколько специфичных для платформы ошибок.

Vitor Py
источник
Мне интересно слушать истории из любых рамок, потому что я полагаю, что у каждой среды есть чему поучиться, особенно тем, с которыми я не сталкивался.
rwong
1
Отладчики практически бесполезны в многопоточной среде.
Пемдас
У меня уже есть многопоточные средства отслеживания выполнения, которые сообщают мне, в чем заключается проблема, но не помогают мне решить ее. Суть моей проблемы заключается в том, что «в соответствии с текущим проектом я не могу передать сообщение X объекту Y таким образом (последовательность); оно должно быть добавлено в гигантскую очередь, и оно в конечном итоге будет обработано; но из-за этого , нет никакого способа для сообщения появляться пользователю в нужное время - это всегда будет anachronisticly и сделать пользователь очень, очень . спутать Вы даже можете добавить прогресс бар, отменить кнопки или сообщения об ошибках в места , которые не должны» те не имеют ".
Rwong
0

Я узнал, что обратные вызовы от модулей более низкого уровня к модулям более высокого уровня являются огромным злом, потому что они вызывают получение блокировок в обратном порядке.

Сергей Загурский
источник
обратные вызовы не являются злом ... тот факт, что они делают что-то кроме прерывания потока, вероятно, является корнем зла. Я бы очень подозревал любой обратный вызов, который не просто отправлял токен в очередь сообщений.
Пемдас
Решение проблемы оптимизации (например, минимизация f (x)) часто реализуется путем предоставления указателя на функцию f (x) для процедуры оптимизации, которая «вызывает ее», ища минимум. Как бы вы сделали это без обратного вызова?
Quant_Dev
1
Нет отрицательных голосов, но обратные вызовы не являются злом. Обратный вызов при удержании блокировки - это зло. Не вызывайте ничего внутри замка, если вы не знаете, может ли он заблокировать или подождать. Это включает не только обратные вызовы, но и виртуальные функции, функции API, функции в других модулях («более высокий уровень» или «более низкий уровень»).
nikie
@nikie: Если во время обратного вызова необходимо удерживать блокировку , либо остальная часть API должна быть спроектирована так, чтобы она была реентерабельной (жесткой!), либо тот факт, что вы держите блокировку, должен быть документированной частью API ( К сожалению, но иногда все, что вы можете сделать).
Донал Феллоуз
@Donal Fellows: Если во время обратного вызова необходимо удерживать блокировку, я бы сказал, что у вас есть недостаток дизайна. Если на самом деле другого пути нет, то да, во что бы то ни стало документируйте это! Так же, как вы бы документировали, если обратный вызов будет вызываться в фоновом потоке. Это часть интерфейса.
nikie