Подтвердить что ты не робот

Каковы общие ловушки concurrency?

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

Существуют ли другие шаблоны проектирования, которые не являются потокобезопасными?

Update

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

4b9b3361

Ответ 1

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

НЕ СООТВЕТСТВУЮТ НА ТЕСТИРОВАНИИ, ЧТОБЫ НАЙТИ УСЛОВИЯ И МЕРТВЫЕ РАСЧЕТЫ

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

Все хорошо и хорошо, но это приводит к тому, что "хорошо, он прошел тестовый набор, поэтому он не может быть ошибкой в ​​моем коде". который не поможет вам в параллельном программировании. Ошибки в режиме реального времени concurrency жутко трудно воспроизвести. Вы можете запустить кусок кода с условием гонки в миллиард раз, прежде чем он потерпит неудачу.

Вам нужно будет настроить свой процесс, чтобы уделять больше внимания обзорам кода, проводимым вашими лучшими умами. Имея отдельный обзор кода только для concurrency проблем, это не плохая идея.

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

Вам нужно будет уделять больше внимания проверкам параноидального здравомыслия в вашем коде, чтобы ошибка обнаруживалась как можно ближе к проблеме, а не 50 000 строк кода.

Будьте параноидальными. очень параноидально.

Ответ 2

Один из них - состояние гонки, которое в основном предполагает, что один кусок кода будет работать до/после другого параллельного фрагмента кода.

Есть также deadlocks, то есть код A ждет кода B, чтобы освободить ресурс Y, а код B ждет, чтобы A освободил ресурс X.

Ответ 3

Я многому учу моим друзьям и коллегам. Вот некоторые из больших ошибок:

  • Предполагая, что переменная, которая в основном читается во многих потоках и записывается только в одном потоке, не требуется блокировать. (В Java эта ситуация может привести к тому, что потоки чтения никогда не будут видеть новое значение.)
  • Предполагая, что потоки будут выполняться в определенном порядке.
  • Предполагая, что потоки будут выполняться одновременно.
  • Предполагая, что потоки НЕ будут выполняться одновременно.
  • Предполагая, что все потоки будут продвигаться вперед до того, как закончится любой из потоков.

Я также вижу:

  • Большие путаницы между thread_fork() и fork().
  • Конфузии, когда память выделяется в одном потоке и free() d в другом потоке.
  • Конфузии в результате того, что некоторые библиотеки являются потокобезопасными, а некоторые нет.
  • Люди, использующие спин-блокировки, когда они должны использовать сон и бодрствовать, или выбрать или любой механизм блокировки, поддерживаемый вашим языком.

Ответ 4

Concurrency не имеет много ошибок.

Синхронизация доступа к общим данным, однако, сложна.

Вот некоторые вопросы, на которые кто-либо может написать код синхронизации данных общего доступа:

  • Что такое InterlockedIncrement?
  • Почему InterlockedIncrement должен существовать на уровне ассемблера?
  • Что читается переупорядочением записи?
  • Что такое ключевое слово volatile (в С++) и когда вам нужно его использовать?
  • Что такое иерархия синхронизации?
  • Что такое проблема ABA?
  • Что такое когерентность кэша?
  • Что такое барьер памяти?

"Все обошлось" concurrency - это чрезвычайно непроницаемая абстракция. Adopt вместо этого передается сообщение.

Ответ 5

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

Concurrency, безусловно, неизбежен в некоторых доменах, но когда этого избежать, избегайте его.

Ответ 6

Как указано в других ответах, две наиболее вероятные проблемы: взаимоблокировки и условия гонки. Однако мой главный совет был бы в том, что если вы хотите тренировать команду по теме concurrency, я настоятельно рекомендую получить обучение самостоятельно. Получите хорошую книгу по этому вопросу, не полагайтесь на несколько абзацев с сайта. Хорошая книга будет зависеть от языка, который вы используете: "Java concurrency in Practice" Брайана Гетца хорош для этого языка, но есть много других.

Ответ 7

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

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

Как и множественное наследование, если вы хотите создать новый поток/задачу, предположите, что вы ошибаетесь, пока не доказали обратное. Я даже не могу подсчитать количество раз, когда Ive видел шаблон Thread A, который вызывает Thread B thenThread B вызывает Thread C, тогда Thread C вызывает D, все ждут ответа от предыдущего потока. Все, что делает код, это делать вызовы с длинной ветвью через разные потоки. Не используйте потоки, когда вызовы функций работают нормально.

Всегда помните, что очереди сообщений являются вашим лучшим другом, если вы хотите работать одновременно.

Я обнаружил, что создание основной инфраструктуры, которая обрабатывает почти все проблемы concurrency, работает лучше всего. Если есть какие-то потоки вне базовой инфраструктуры, которые должны разговаривать с другой частью программного обеспечения, тогда они должны пройти через основную инфраструктуру. Таким образом, остальная часть системы может оставаться concurrency не осознавать, а проблемы concurrency могут обрабатываться лицами, которые, надеюсь, понимают concurrency.

Ответ 8

Все сводится к общему состоянию данных/общего состояния. Если у вас нет данных или состояний, у вас нет проблем concurrency.

Большинство людей, когда они думают о concurrency, думают о многопоточности в одном процессе.

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

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

(Остальная часть этого не относится к потоку Java, который я не использую и поэтому мало знаю).

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

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

Удачи!

Ответ 9

По моему опыту, многие (опытные) разработчики не имеют основополагающих знаний о теории concurrency. Классические учебники по операционным системам Tanenbaum или Stallings хорошо помогают объяснить теорию и последствия concurrency: взаимное исключение, синхронизацию, взаимоблокировки и голод. Хорошая теоретическая основа обязательна для успешной работы с concurrency.

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

Ответ 10

Ловушка №1, которую я видел, слишком много для обмена данными.

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

Ответ 11

Вот отличный ресурс о concurrency, особенно в Java: http://tech.puredanger.com/ Алекс Миллер перечисляет много разных проблем, с которыми можно столкнуться при работе с concurrency. Очень рекомендуется:)

Ответ 12

Вызов открытых классов из блокировки, вызывающей DeadLock

public class ThreadedClass
{
    private object syncHandle = new object();

    public event EventHandler Updated = delegate { };
    public int state = 0;

    public void DoSmething()
    {
        lock(syncHandle)
        {
            // some locked code
            state = 1;

            Updated(this, EventArgs.Empty);
        }
    }

    public int State { 
        get
        {
            int returnVal;
            lock(syncHandle)
                returnVal = state;
            return returnVal;            
        }
    }
}

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

public void DoSmething()
{
    lock(syncHandle)
    {
        // some locked code
        state = 1;
    }
    // this should be outside the lock
    Updated(this, EventArgs.Empty);
}

Ответ 13

Двойная проверка блокировки сломана, по крайней мере, на Java. Понимая, почему это так, и как вы можете это исправить, вы глубоко понимаете проблемы concurrency и модель памяти Java.

Ответ 14

Некоторые эмпирические правила:

(1) Следите за контекстом, когда объявляете переменную

  • Запись в атрибуты класса (статический) должен быть синхронизирован
  • Запись атрибутов экземпляра должна быть синхронизирована
  • Держите все переменные как можно более локальными (не помещайте их в член контекст, если это не имеет смысла)
  • Отметить переменные, которые считаются неизменяемыми.

(2) Блокировать доступ к изменяемым атрибутам класса или экземпляра: Переменные, которые являются частью одного и того же invariant должны быть защищены одной и той же блокировкой.

(3) Избегайте Double Checked Locking

(4) Сохраняйте блокировки при запуске распределенной операции (подпрограммы вызова).

(5) Избегайте ожидание ожидания

(6) Сохранение рабочей нагрузки в синхронизированных секциях

(7) Не разрешайте принимать клиентский элемент управления, пока вы находитесь в синхронизированном блоке.

(8) Комментарии! Это действительно помогает понять, что другой парень имел в виду, объявив этот раздел синхронизированным или эта переменная неизменяема.

Ответ 15

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

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

  • Предоставлять блокировки без необходимости. Если нет необходимости открывать блокировку, то она считает, что она скрыта. В противном случае клиенты могут использовать его и создавать блокировки, которые вы могли бы предотвратить.

Вот простой пример # 1 выше, который довольно близок к тому, что я видел в производственном коде:

public class CourseService {
    private CourseDao courseDao;
    private List courses;

    public List getCourses() {
        this.courses = courseDao.getCourses();
        return this.courses;
    }
}

В этом примере нет необходимости в переменной экземпляра courses, и теперь одновременные вызовы getCourses() могут иметь расы.

Ответ 17

Некоторые канонические ловушки deadlocks (два конкурирующих процесса застревают, ожидая друг друга, чтобы освободить некоторый ресурс) и условия гонки (когда время и/или зависимость событий могут привести к неожиданному поведению). Здесь также стоит видео о "многопоточном Gotchas" .

Ответ 18

Кроме того, вы можете посмотреть на проблемный тип concurrency проблемы
Например:
Процесс записи, создающий файл и потребительский процесс, требующие файла.

Ответ 19

Concurrency проблемы невероятно сложно отлаживать. В качестве превентивной меры можно полностью запретить доступ к общим объектам без использования мьютексов таким образом, чтобы программисты могли легко следовать правилам. Я видел это, сделав обертки вокруг предоставленных OS мьютексов и семафоров и т.д.

Вот некоторые запутанные примеры из моего прошлого:

Я использовал для разработки драйверов для Windows. Чтобы предотвратить одновременную запись нескольких потоков на принтер, наш монитор порта использовал такую ​​конструкцию: // псевдокод, потому что я не могу вспомнить API BOOL OpenPort() {GrabCriticalSection(); } BOOL ClosePort() {ReleaseCriticalSection(); } BOOL WritePort() {writestuff(); }

К сожалению, каждый вызов WritePort был из другого потока в пуле потоков спулера. В итоге мы попали в ситуацию, когда OpenPort и ClosePort вызывались разными потоками, вызывая тупик. Решение для этого остается как упражнение, потому что я не могу вспомнить, что я сделал.

Я также использовал для работы с прошивкой принтера. Принтер в этом случае использовал RTOS под названием uCOS (произносится как "слизь" ), поэтому каждая функция имела свою задачу (двигатель печатающей головки, последовательный порт, параллельный порт, сетевой стек и т.д.). Одна версия этого принтера имела внутреннюю опцию, которая подключалась к последовательному порту материнской платы принтера. В какой-то момент было обнаружено, что принтер дважды читал один и тот же результат из этого периферийного устройства, и каждое значение после него было бы непоследовательным. (например, периферийное считывание последовательности 1,7,3,56,9,230, но мы бы увидели 1,7,3,3,56,9,230. Это значение доводилось до сведения компьютера и помещалось в базу данных так, чтобы иметь кучу документов с неправильными идентификационными номерами было очень плохо). Основной причиной этого было несоблюдение мьютекса, защищающего буфер чтения для устройства. (Отсюда мой совет в начале этого ответа)

Ответ 20

Это не ловушка, а больше подсказка, основанная на других ответах..NET framework readerwriterlockslim значительно улучшит вашу производительность во многих случаях по сравнению с оператором "lock", будучи реентерабельным.

Ответ 21

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

Лично я конвертирую в модель актера параллельного вычисления (асинхронное разнообразие).