Вместо того чтобы обращаться за помощью, можете проанализировать свой код самостоятельно. Например, полезно попытаться
Я уже говорил, что рецензенту (неважно, является он автором программы или нет) полезно задавать конкретные вопросы по поводу анализируемой программы. Они позволяют сосредоточиться на деталях кода и выявить потенциальные проблемы. Лично я люблю задавать вопросы, перечисленные ниже, хотя это, конечно, далеко не исчерпывающий список. Вам, возможно, помогут лучше сконцентрироваться совсем другие вопросы.
• Какие данные нужно защищать от одновременного доступа?
• Как вы обеспечиваете защиту этих данных?
• В каком участке программы могут в этот момент находиться другие потоки?
• Какие мьютексы удерживает данный поток?
• Какие мьютексы могут удерживать другие потоки?
• Существуют ли ограничения на порядок выполнения операций в этом и каком-либо другом потоке? Как гарантируется соблюдение этих ограничений?
• Верно ли, что данные, загруженные этим потоком, все еще действительны? Не могло ли случиться, что их изменили другие потоки?
• Если предположить, что другой поток может изменить данные, то к чему это приведёт и как гарантировать, что этого никогда не случится?
Последний вопрос — мой любимый, потому что заставляет думать о взаимосвязях между потоками. Допустив, что в некоторой строке имеется ошибка, вы дальше перевоплощаетесь в сыщика, которому нужно раскрыть преступление. Чтобы убедить себя в отсутствии ошибки, требуется рассмотреть все граничные случаи, приняв во внимание любой возможный порядок операций. Это особенно полезно, если данные в разные моменты времени защищаются разными мьютексами, как, например, обстояло дело в потокобезопасной очереди из главы 6, где мы завели разные мьютексы для головы и хвоста очереди. Чтобы гарантировать безопасность доступа в момент, когда захвачен один мьютекс, нужна уверенность в том, что поток, удерживающий
Предпоследний вопрос из списка также важен, потому что касается очень распространенной ошибки: если вы освобождаете, а затем снова захватываете мьютекс, то должны предполагать, что другие потоки могли изменить разделяемые данные. На первый взгляд, очевидно, но если операции с мьютексами не видны — например, потому что скрыты внутри какого-то объекта, — то вы неосознанно допускаете именно эту ошибку В главе 6 мы видели, как это может привести к гонке и ошибкам, когда функции в потокобезопасной структуре данных слишком детализированы. Если для стека, не безопасного относительно потоков, наличие отдельных операций top()
и pop()
оправдано, то для стека, к которому могут одновременно обращаться несколько потоков, это уже не так, потому что между этими двумя вызовами внутренний мьютекс не захвачен, и, значит, какой-то другой поток может модифицировать стек. В главе 6 мы видели, что для решения этой проблемы нужно объединить обе операции в одну — выполняемую под защитой одной и той же блокировки мьютекса. Тем самым опасность гонки устраняется.
Итак, вы проанализировали код (или это сделал кто-то другой). Вы уверены, что в нем нет ошибок. Но критерием истины, как известно, является практика — как можно протестировать код, подтвердив или опровергнув вашу веру в отсутствие ошибок?
10.2.2. Поиск связанных с параллелизмом ошибок путем тестирования
Тестирование однопоточных приложений — процедура сравнительно простая, хотя, возможно, и длительная. Теоретически можно идентифицировать все возможные наборы входных данных (или, но крайней мере, все интересные случаи) и подать их на вход приложения. Если поведение и выходные данные программы совпадают с ожидаемыми, значит, для соответствующего набора входных данных программа работает корректно. Тестировать такие ситуации, как переполнение диска, сложнее, но идея та же самая — подготовить начальные условия и прогнать приложение.