Вместо того чтобы обращаться за помощью, можете проанализировать свой код самостоятельно. Например, полезно попытаться во всех деталях объяснить кому-нибудь, как он работает. Это даже необязательно должен быть человек — вполне подойдёт плюшевый медвежонок или надувной цыплёнок. Лично мне очень помогает написание подробных заметок. По ходу объяснения думайте над каждой строкой, рассказывайте, что может произойти, к каким данным происходят обращения и т.д. Задавайте себе вопросы о программе и объясняйте свои ответы. Мне кажется, что это очень действенная методика — задавая себе вопросы и тщательно продумывая ответы, зачастую удается выявить проблемы. Причем задавать вопросы полезно при анализе любого кода, а не только своего собственного.

Над какими вопросами следует задуматься при анализе многопоточного кода

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

• Какие данные нужно защищать от одновременного доступа?

• Как вы обеспечиваете защиту этих данных?

• В каком участке программы могут в этот момент находиться другие потоки?

• Какие мьютексы удерживает данный поток?

• Какие мьютексы могут удерживать другие потоки?

• Существуют ли ограничения на порядок выполнения операций в этом и каком-либо другом потоке? Как гарантируется соблюдение этих ограничений?

• Верно ли, что данные, загруженные этим потоком, все еще действительны? Не могло ли случиться, что их изменили другие потоки?

• Если предположить, что другой поток может изменить данные, то к чему это приведёт и как гарантировать, что этого никогда не случится?

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

Предпоследний вопрос из списка также важен, потому что касается очень распространенной ошибки: если вы освобождаете, а затем снова захватываете мьютекс, то должны предполагать, что другие потоки могли изменить разделяемые данные. На первый взгляд, очевидно, но если операции с мьютексами не видны — например, потому что скрыты внутри какого-то объекта, — то вы неосознанно допускаете именно эту ошибку В главе 6 мы видели, как это может привести к гонке и ошибкам, когда функции в потокобезопасной структуре данных слишком детализированы. Если для стека, не безопасного относительно потоков, наличие отдельных операций top() и pop() оправдано, то для стека, к которому могут одновременно обращаться несколько потоков, это уже не так, потому что между этими двумя вызовами внутренний мьютекс не захвачен, и, значит, какой-то другой поток может модифицировать стек. В главе 6 мы видели, что для решения этой проблемы нужно объединить обе операции в одну — выполняемую под защитой одной и той же блокировки мьютекса. Тем самым опасность гонки устраняется.

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

<p>10.2.2. Поиск связанных с параллелизмом ошибок путем тестирования</p>

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

Перейти на страницу:

Похожие книги